Coverage for src/swiift/lib/physics.py: 35%

1 from __future__ import annotations (empty)

2

3 import functools (empty)

4 import typing (empty)

5

6 import attrs (empty)

7 import numpy as np (empty)

8

9 from . import numerical (empty)

10 from ._ph_utils import _wavefield (empty)

11 from .constants import PI_D4, SQR2 (empty)

12

13 if typing.TYPE_CHECKING: (empty)

14 from ..model import model

15

16

17 def _package_wuf( (empty)

18 wuf: model.WavesUnderFloe, growth_params: tuple | None

19) -> tuple[tuple, tuple, tuple | None]:

20 floe_params = wuf.wui.ice._red_elastic_number, wuf.length

21 wave_params = wuf.edge_amplitudes, wuf.wui._c_wavenumbers

22 if growth_params is not None:

23 growth_params = growth_params[0] - wuf.left_edge, growth_params[1]

24 return floe_params, wave_params, growth_params

25

26

27 def _dis_par_amps(red_num: float, wave_params: tuple[np.ndarray, np.ndarray]): (empty)

28 """Complex amplitudes of individual particular solutions"""

29 c_amplitudes, c_wavenumbers = wave_params

30 return c_amplitudes / (1 + 0.25 * (c_wavenumbers / red_num) ** 4)

31

32

33 def _dis_hom_coefs( (empty)

34 floe_params: tuple[float, float], wave_params: tuple[np.ndarray, np.ndarray]

35) -> np.ndarray:

36 """Coefficients of the four orthogonal homogeneous solutions"""

37 return _dis_hom_mat(*floe_params) @ _dis_hom_rhs(floe_params, wave_params)

38

39

40 def _dis_hom_mat(red_num: float, length: float): (empty)

41 """Linear application to determine, from the BCs, the coefficients of

42 the four independent solutions to the homo ODE"""

43 adim = red_num * length

44 adim2 = 2 * adim

45 denom = (

46 -2

47 * red_num**2

48 * (np.expm1(-adim2) ** 2 + 2 * np.exp(-adim2) * (np.cos(adim2) - 1))

49 )

50 mat = np.array(

51 [

52 [

53 SQR2 * np.exp(-adim) * np.sin(adim2 + PI_D4) - np.exp(-3 * adim),

54 -SQR2 * np.cos(adim + PI_D4)

55 - 3 * np.exp(-adim2) * np.sin(adim)

56 + np.exp(-adim2) * np.cos(adim),

57 np.exp(-adim) * np.sin(adim2) - np.exp(-adim) + np.exp(-3 * adim),

58 np.cos(adim)

59 - 2 * np.exp(-adim2) * np.sin(adim)

60 - np.exp(-adim2) * np.cos(adim),

61 ],

62 [

63 -SQR2 * np.exp(-adim) * np.cos(adim2 + PI_D4)

64 + 2 * np.exp(-adim)

65 - np.exp(-3 * adim),

66 -SQR2 * np.sin(adim + PI_D4)

67 + SQR2 * np.exp(-adim2) * np.cos(adim + PI_D4),

68 -np.exp(-adim) * np.cos(adim2) + np.exp(-adim),

69 np.sin(adim) - np.exp(-adim2) * np.sin(adim),

70 ],

71 [

72 -1 + SQR2 * np.exp(-adim2) * np.cos(adim2 + PI_D4),

73 (3 * np.sin(adim) + np.cos(adim)) * np.exp(-adim)

74 - SQR2 * np.exp(-3 * adim) * np.sin(adim + PI_D4),

75 (np.sin(adim2) + 1) * np.exp(-adim2) - 1,

76 (-2 * np.sin(adim) + np.cos(adim)) * np.exp(-adim)

77 - np.exp(-3 * adim) * np.cos(adim),

78 ],

79 [

80 (SQR2 * np.sin(adim2 + PI_D4) - 2) * np.exp(-adim2) + 1,

81 -SQR2 * np.exp(-adim) * np.sin(adim + PI_D4)

82 + SQR2 * np.exp(-3 * adim) * np.cos(adim + PI_D4),

83 (-np.cos(adim2) + 1) * np.exp(-adim2),

84 np.exp(-adim) * np.sin(adim) - np.exp(-3 * adim) * np.sin(adim),

85 ],

86 ]

87 )

88 mat[:, 2:] /= red_num

89

90 return mat / denom

91

92

93 def _dis_hom_rhs( (empty)

94 floe_params: tuple[float, float], wave_params: tuple[np.ndarray, np.ndarray]

95):

96 """Vector onto which apply the matrix, to extract the coefficients"""

97 red_num, length = floe_params

98 _, c_wavenumbers = wave_params

99 exp_arg = 1j * c_wavenumbers * length

100

101 r1 = c_wavenumbers**2 * _dis_par_amps(red_num, wave_params)

102 r2 = np.imag(r1 @ np.exp(exp_arg))

103 r3 = 1j * c_wavenumbers * r1

104 r4 = np.imag(r3 @ np.exp(exp_arg))

105 r1 = np.imag(r1).sum()

106 r3 = np.imag(r3).sum()

107

108 return np.array((r1, r2, r3, r4))

109

110

111# Can be used to decorate methods to make them return a scalar instead of

112# a 1d-array of length 1, if the argument is a scalar OR a 0d array (that is, a

113# scalar). If the argument is a tuple or a list or a 1d array of length 1,

114# a 1d-array of length 1 is returned.

115# This emulate the behaviour of standard numpy ufuncs.

116 def _demote_to_scalar(f): (empty)

117 @functools.wraps(f) (empty)

118 def wrapper(*args): (empty)

119 instance, x = args 2 ctx 1bc

120 x = np.asarray(x) 2 ctx 1bc

121 res = f(instance, x) 2 ctx 1bc

122 if len(res) == 1 and x.ndim == 0: 2 ctx 1bc

123 return res.item() 2 ctx 1bc

124 return res 1 ctx 1b

125

126 return wrapper (empty)

127

128

129@attrs.define (empty)

130 class FluidSurfaceHandler: (empty)

131 wave_params: tuple[np.ndarray, np.ndarray] (empty)

132 growth_params: tuple[np.ndarray, float] | None = None (empty)

133

134 @classmethod (empty)

135 def from_wuf(cls, wuf: model.WavesUnderFloe, growth_params=None): (empty)

136 return cls(*(_package_wuf(wuf, growth_params)[1:]))

137

138 @classmethod (empty)

139 def from_domain(cls, domain: model.Domain, growth_params: tuple | None = None): (empty)

140 return cls(

141 (

142 domain.spectrum.amplitudes * np.exp(1j * domain.spectrum.phases),

143 domain.fsw.wavenumbers,

144 ),

145 growth_params,

146 )

147

148 @_demote_to_scalar (empty)

149 def compute(self, x, /): (empty)

150 return numerical.free_surface(x, self.wave_params, self.growth_params)

151

152

153@attrs.define (empty)

154 class DisplacementHandler: (empty)

155 floe_params: tuple[float, float] (empty)

156 wave_params: tuple[np.ndarray, np.ndarray] (empty)

157 growth_params: tuple[np.ndarray, float] | None = None (empty)

158

159 @classmethod (empty)

160 def from_wuf(cls, wuf: model.WavesUnderFloe, growth_params=None): (empty)

161 return cls(*_package_wuf(wuf, growth_params))

162

163 def _dis_hom(self, x: np.ndarray): (empty)

164 """Homogeneous solution to the displacement ODE"""

165 red_num, length = self.floe_params

166 arr = red_num * x

167 cosx, sinx = np.cos(arr), np.sin(arr)

168 expx = np.exp(-red_num * (length - x))

169 exmx = np.exp(-arr)

170 return np.vstack(

171 ([expx * cosx, expx * sinx, exmx * cosx, exmx * sinx])

172 ).T @ _dis_hom_coefs(self.floe_params, self.wave_params)

173

174 def _dis_par(self, x: np.ndarray): (empty)

175 """Sum of the particular solutions to the displacement ODE"""

176 return _wavefield(

177 x, _dis_par_amps(self.floe_params[0], self.wave_params), self.wave_params[1]

178 )

179

180 @_demote_to_scalar (empty)

181 def _dis(self, x, /): (empty)

182 return self._dis_hom(x) + self._dis_par(x)

183

184 def compute( (empty)

185 self,

186 x,

187 an_sol: bool | None = None,

188 num_params: dict | None = None,

189 ):

190 if numerical._use_an_sol(

191 an_sol, self.floe_params[1], self.growth_params, linear_curvature=None

192 ):

193 return self._dis(np.asarray(x))

194 return numerical.displacement(

195 x, self.floe_params, self.wave_params, self.growth_params, num_params

196 )

197

198

199@attrs.define (empty)

200 class CurvatureHandler: (empty)

201 floe_params: tuple[float, float] (empty)

202 wave_params: tuple[np.ndarray, np.ndarray] (empty)

203 growth_params: tuple[np.ndarray, float] | None = None (empty)

204

205 @classmethod (empty)

206 def from_wuf(cls, wuf: model.WavesUnderFloe, growth_params=None): (empty)

207 return cls(*_package_wuf(wuf, growth_params))

208

209 def _cur_wavefield(self, x): (empty)

210 """Second derivative of the interface"""

211 red_num = self.floe_params[0]

212 _, c_wavenumbers = self.wave_params

213

214 return -np.imag(

215 (_dis_par_amps(red_num, self.wave_params) * c_wavenumbers**2)

216 @ np.exp(1j * c_wavenumbers[:, None] * x)

217 )

218

219 def _cur_hom(self, x): (empty)

220 """Second derivative of the homogeneous part of the displacement"""

221 red_num, length = self.floe_params

222 arr = red_num * x

223 cosx, sinx = np.cos(arr), np.sin(arr)

224 expx = np.exp(-red_num * (length - x))

225 exmx = np.exp(-arr)

226 return (

227 2

228 * red_num**2

229 * np.vstack(([-expx * sinx, expx * cosx, exmx * sinx, -exmx * cosx])).T

230 @ _dis_hom_coefs(self.floe_params, self.wave_params)

231 )

232

233 def _cur_par(self, x): (empty)

234 """Second derivative of the particular part of the displacement"""

235 return self._cur_wavefield(x)

236

237 @_demote_to_scalar (empty)

238 def _cur(self, x, /): (empty)

239 return self._cur_hom(x) + self._cur_par(x)

240

241 def compute( (empty)

242 self,

243 x,

244 an_sol: bool | None = None,

245 num_params: dict | None = None,

246 is_linear: bool = True,

247 ):

248 """Curvature of the floe.

249

250 A linear approximation is given by the second derivative of the

251 displacement. The exact definition involves the first derivative of the

252 displacement.

253

254 Parameters

255 ----------

256 x : float or 1d array_like of float

257 Along-floe coordinates at which to compute the curvature, in m

258 an_sol : bool

259 Whether to use an analytical formulation of the curvature

260 num_params : dict

261 Optional arguments passed to the numerical solver if `not an_sol`

262 is_linear : bool

263 Whether to use the linear approximation; ignored if `an_sol`

264

265 Returns

266 -------

267 float or 1d array_like of float

268

269 """

270 if numerical._use_an_sol(

271 an_sol, self.floe_params[1], self.growth_params, is_linear

272 ):

273 return self._cur(x)

274 return numerical.curvature(

275 x,

276 self.floe_params,

277 self.wave_params,

278 self.growth_params,

279 num_params,

280 is_linear,

281 )

282

283

284@attrs.define (empty)

285 class StrainHandler: (empty)

286 curv_handler: CurvatureHandler (empty)

287 thickness: float (empty)

288

289 @classmethod (empty)

290 def from_wuf(cls, wuf: model.WavesUnderFloe, growth_params=None): (empty)

291 return cls(CurvatureHandler.from_wuf(wuf, growth_params), wuf.wui.ice.thickness)

292

293 def compute(self, x, an_sol: bool | None = None, num_params: dict | None = None): (empty)

294 return -self.thickness / 2 * self.curv_handler.compute(x, an_sol, num_params)

295

296

297@attrs.define (empty)

298 class EnergyHandler: (empty)

299 floe_params: tuple[float, float] (empty)

300 wave_params: tuple[np.ndarray, np.ndarray] (empty)

301 growth_params: tuple[np.ndarray, float] | None = None (empty)

302 factor: float = 1 (empty)

303

304 @classmethod (empty)

305 def from_wuf(cls, wuf: model.WavesUnderFloe, growth_params=None): (empty)

306 factor = cls._compute_factor(wuf)

307 return cls(*_package_wuf(wuf, growth_params), factor)

308

309 @staticmethod (empty)

310 def _compute_factor(wuf: model.WavesUnderFloe): (empty)

311 # NOTE: this could also be a WUF property?

312 return wuf.wui.ice.flex_rigidity / (2 * wuf.wui.ice.thickness)

313

314 def _egy_hom(self): (empty)

315 """Energy from the homogen term of the displacement ODE"""

316 red_num, length = self.floe_params

317 adim = red_num * length

318 adim2 = 2 * adim

319 c_1, c_2, c_3, c_4 = _dis_hom_coefs(self.floe_params, self.wave_params)

320

321 return red_num**3 * (

322 +(c_1**2) * (-SQR2 * np.sin(adim2 + PI_D4) + 2 - np.exp(-adim2)) / 2

323 + c_2**2 * (SQR2 * np.sin(adim2 + PI_D4) + 2 - 3 * np.exp(-adim2)) / 2

324 + c_3**2 * ((SQR2 * np.cos(adim2 + PI_D4) - 2) * np.exp(-adim2) + 1) / 2

325 + c_4**2 * (3 - (SQR2 * np.cos(adim2 + PI_D4) + 2) * np.exp(-adim2)) / 2

326 + c_1 * c_2 * (SQR2 * np.cos(adim2 + PI_D4) - np.exp(-adim2))

327 - 2

328 * red_num

329 * c_1

330 * c_3

331 * (2 * length - np.sin(adim2) / red_num)

332 * np.exp(-adim)

333 + 4 * c_1 * c_4 * np.exp(-adim) * np.sin(adim) ** 2

334 + 4 * c_2 * c_3 * np.exp(-adim) * np.sin(adim) ** 2

335 - 2

336 * red_num

337 * c_2

338 * c_4

339 * (2 * length + np.sin(adim2) / red_num)

340 * np.exp(-adim)

341 + c_3 * c_4 * (-1 + SQR2 * np.exp(-adim2) * np.sin(adim2 + PI_D4))

342 )

343

344 def _egy_par_vals(self): (empty)

345 red_num = self.floe_params[0]

346 comp_amps = _dis_par_amps(red_num, self.wave_params)

347 _, c_wavenumbers = self.wave_params

348

349 return comp_amps * (1j * c_wavenumbers) ** 2

350

351 def _egy_par_pow2(self): (empty)

352 """Energy contribution from individual forcings"""

353 red_num, length = self.floe_params

354 _, c_wavenumbers = self.wave_params

355 wavenumbers = np.real(c_wavenumbers)

356 attenuations = np.imag(c_wavenumbers)

357

358 comp_curvs = self._egy_par_vals()

359 wn_moduli, curv_moduli = map(np.abs, (c_wavenumbers, comp_curvs))

360 wn_phases, curv_phases = map(np.angle, (c_wavenumbers, comp_curvs))

361

362 # Because of a division by `attenuations`, this term must me handled

363 # with some extra care

364 attenuation_mask = attenuations != 0

365 non_oscillatory_term = np.full(attenuations.shape, np.nan)

366 non_oscillatory_term[attenuation_mask] = (

367 -np.expm1(-2 * attenuations[attenuation_mask] * length)

368 / attenuations[attenuation_mask]

369 )

370 # NOTE: corresponds to the limit of the previous term, when attenuation -> 0

371 non_oscillatory_term[~attenuation_mask] = 2 * length

372

373 return (

374 curv_moduli**2

375 @ (

376 non_oscillatory_term

377 + (

378 np.sin(2 * curv_phases - wn_phases)

379 - np.sin(2 * (wavenumbers * length + curv_phases) - wn_phases)

380 * np.exp(-2 * attenuations * length)

381 )

382 / wn_moduli

383 )

384 ) / 4

385

386 def _egy_par_m(self): (empty)

387 """Energy contribution from forcing interactions"""

388 red_num, length = self.floe_params

389 _, c_wavenumbers = self.wave_params

390 wavenumbers = np.real(c_wavenumbers)

391 attenuations = np.imag(c_wavenumbers)

392 comp_curvs = self._egy_par_vals()

393

394 # Binomial coefficients, much quicker than itertools

395 idx1, idx2 = np.triu_indices(c_wavenumbers.size, 1)

396

397 mean_attenuations = attenuations[idx1] + attenuations[idx2]

398 comp_wns = (

399 wavenumbers[idx1] - wavenumbers[idx2],

400 wavenumbers[idx1] + wavenumbers[idx2],

401 )

402 comp_wns += 1j * mean_attenuations

403

404 curv_moduli = np.abs(comp_curvs)

405 _curv_phases = np.angle(comp_curvs)

406 curv_phases = (

407 _curv_phases[idx1] - _curv_phases[idx2],

408 _curv_phases[idx1] + _curv_phases[idx2],

409 )

410

411 def _f(comp_wns, curv_phases):

412 wn_moduli = np.abs(comp_wns)

413 wn_phases = np.angle(comp_wns)

414 return (

415 np.sin(curv_phases - wn_phases)

416 - (

417 np.exp(-np.imag(comp_wns) * length)

418 * np.sin(np.real(comp_wns) * length + curv_phases - wn_phases)

419 )

420 ) / wn_moduli

421

422 return (

423 curv_moduli[idx1]

424 * curv_moduli[idx2]

425 @ (_f(comp_wns[1], curv_phases[1]) - _f(comp_wns[0], curv_phases[0]))

426 )

427

428 def _egy_par(self): (empty)

429 """Energy from the forcing term of the displacement ODE"""

430 return self._egy_par_pow2() + self._egy_par_m()

431

432 def _egy_m(self): (empty)

433 red_num, length = self.floe_params

434 _, c_wavenumbers = self.wave_params

435 adim = red_num * length

436 wavenumbers = np.real(c_wavenumbers)

437 attenuations = np.imag(c_wavenumbers)

438

439 wn_abs, wn_phases = (_f(c_wavenumbers) for _f in (np.abs, np.angle))

440 comp_curvs = self._egy_par_vals()

441 curv_moduli, curv_phases = np.abs(comp_curvs), np.angle(comp_curvs)

442 num_add = red_num + wavenumbers

443 num_sub = red_num - wavenumbers

444 att_add = red_num + attenuations

445 att_sub = red_num - attenuations

446 q_pp = att_add**2 + num_add**2

447 q_mp = att_sub**2 + num_add**2

448 q_pm = att_add**2 + num_sub**2

449 q_mm = att_sub**2 + num_sub**2

450

451 phase_deltas = wn_phases - curv_phases

452 sin_phase_deltas = np.sin(phase_deltas)

453 cos_phase_deltas = np.cos(phase_deltas)

454 phase_quads = curv_phases + PI_D4

455 sin_phase_quads = np.sin(phase_quads)

456 cos_phase_quads = np.cos(phase_quads)

457 energ_1_K = np.array(

458 (

459 sin_phase_deltas * (1 / q_mp - 1 / q_mm),

460 cos_phase_deltas * (1 / q_mp + 1 / q_mm),

461 -sin_phase_deltas * (1 / q_pp - 1 / q_pm),

462 -cos_phase_deltas * (1 / q_pp + 1 / q_pm),

463 )

464 )

465 energ_1_b = np.array(

466 (

467 -(sin_phase_quads / q_mp - cos_phase_quads / q_mm),

468 (cos_phase_quads / q_mp - sin_phase_quads / q_mm),

469 -(cos_phase_quads / q_pp - sin_phase_quads / q_pm),

470 -(sin_phase_quads / q_pp - cos_phase_quads / q_pm),

471 )

472 )

473 energ_1 = wn_abs * energ_1_K + SQR2 * red_num * energ_1_b

474 energ_1[:2, :] *= np.exp(-adim)

475

476 arg_add = num_add * length

477 arg_add_phase = arg_add - phase_deltas

478 sin_arg_ap = np.sin(arg_add_phase)

479 cos_arg_ap = np.cos(arg_add_phase)

480 arg_sub = num_sub * length

481 arg_sub_phase = arg_sub + phase_deltas

482 sin_arg_sp = np.sin(arg_sub_phase)

483 cos_arg_sp = np.cos(arg_sub_phase)

484 arg_add_quad = arg_add + phase_quads

485 sin_arg_aq = np.sin(arg_add_quad)

486 cos_arg_aq = np.cos(arg_add_quad)

487 arg_sub_quad = arg_sub - curv_phases + PI_D4

488 sin_arg_sq = np.sin(arg_sub_quad)

489 cos_arg_sq = np.cos(arg_sub_quad)

490 energ_exp_K = np.array(

491 (

492 sin_arg_ap / q_mp + sin_arg_sp / q_mm,

493 -(cos_arg_ap / q_mp + cos_arg_sp / q_mm),

494 -(sin_arg_ap / q_pp + sin_arg_sp / q_pm),

495 cos_arg_ap / q_pp + cos_arg_sp / q_pm,

496 )

497 )

498 energ_exp_b = np.array(

499 (

500 sin_arg_aq / q_mp - sin_arg_sq / q_mm,

501 -cos_arg_aq / q_mp + cos_arg_sq / q_mm,

502 cos_arg_aq / q_pp - cos_arg_sq / q_pm,

503 sin_arg_aq / q_pp - sin_arg_sq / q_pm,

504 )

505 )

506 energ_exp = wn_abs * energ_exp_K + SQR2 * red_num * energ_exp_b

507 energ_exp[2:, :] *= np.exp(-adim)

508 energ_exp *= np.exp(-attenuations * length)

509

510 energ = (

511 (energ_1 + energ_exp)

512 @ curv_moduli

513 @ _dis_hom_coefs(self.floe_params, self.wave_params)

514 )

515 return red_num**2 * energ

516

517 def compute( (empty)

518 self,

519 an_sol: bool | None = None,

520 num_params: dict | None = None,

521 integration_method: str | None = None,

522 linear_curvature: bool | None = None,

523 **kwargs,

524 ) -> float:

525 do_use_an_sol = numerical._use_an_sol(

526 an_sol, self.floe_params[1], self.growth_params, linear_curvature

527 )

528 if do_use_an_sol:

529 unit_energy = self._egy_hom() + 2 * self._egy_m() + self._egy_par()

530 else:

531 linear_curvature = True if linear_curvature is None else False

532 # TODO: documenter le comportement par défault.

533 # Inclure un comportement différent en mono (systématique) et poly

534 # (stochastic), avec un warning si l'utilisation d'une méthode

535 # systématique est utilisée en poly, car ça pourrait prendre un

536 # temps infini.

537 unit_energy = numerical.unit_energy(

538 self.floe_params,

539 self.wave_params,

540 self.growth_params,

541 num_params,

542 integration_method,

543 linear_curvature,

544 **kwargs,

545 )

546

547 return self.factor * unit_energy