capytaine 3.0.0a1__cp310-cp310-macosx_15_0_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. capytaine/.dylibs/libgfortran.5.dylib +0 -0
  3. capytaine/.dylibs/libquadmath.0.dylib +0 -0
  4. capytaine/__about__.py +21 -0
  5. capytaine/__init__.py +32 -0
  6. capytaine/bem/__init__.py +0 -0
  7. capytaine/bem/airy_waves.py +111 -0
  8. capytaine/bem/engines.py +321 -0
  9. capytaine/bem/problems_and_results.py +601 -0
  10. capytaine/bem/solver.py +718 -0
  11. capytaine/bodies/__init__.py +4 -0
  12. capytaine/bodies/bodies.py +630 -0
  13. capytaine/bodies/dofs.py +146 -0
  14. capytaine/bodies/hydrostatics.py +540 -0
  15. capytaine/bodies/multibodies.py +216 -0
  16. capytaine/green_functions/Delhommeau_float32.cpython-310-darwin.so +0 -0
  17. capytaine/green_functions/Delhommeau_float64.cpython-310-darwin.so +0 -0
  18. capytaine/green_functions/__init__.py +2 -0
  19. capytaine/green_functions/abstract_green_function.py +64 -0
  20. capytaine/green_functions/delhommeau.py +522 -0
  21. capytaine/green_functions/hams.py +210 -0
  22. capytaine/io/__init__.py +0 -0
  23. capytaine/io/bemio.py +153 -0
  24. capytaine/io/legacy.py +228 -0
  25. capytaine/io/wamit.py +479 -0
  26. capytaine/io/xarray.py +673 -0
  27. capytaine/meshes/__init__.py +2 -0
  28. capytaine/meshes/abstract_meshes.py +375 -0
  29. capytaine/meshes/clean.py +302 -0
  30. capytaine/meshes/clip.py +347 -0
  31. capytaine/meshes/export.py +89 -0
  32. capytaine/meshes/geometry.py +259 -0
  33. capytaine/meshes/io.py +433 -0
  34. capytaine/meshes/meshes.py +826 -0
  35. capytaine/meshes/predefined/__init__.py +6 -0
  36. capytaine/meshes/predefined/cylinders.py +280 -0
  37. capytaine/meshes/predefined/rectangles.py +202 -0
  38. capytaine/meshes/predefined/spheres.py +55 -0
  39. capytaine/meshes/quality.py +159 -0
  40. capytaine/meshes/surface_integrals.py +82 -0
  41. capytaine/meshes/symmetric_meshes.py +641 -0
  42. capytaine/meshes/visualization.py +353 -0
  43. capytaine/post_pro/__init__.py +6 -0
  44. capytaine/post_pro/free_surfaces.py +85 -0
  45. capytaine/post_pro/impedance.py +92 -0
  46. capytaine/post_pro/kochin.py +54 -0
  47. capytaine/post_pro/rao.py +60 -0
  48. capytaine/tools/__init__.py +0 -0
  49. capytaine/tools/block_circulant_matrices.py +275 -0
  50. capytaine/tools/cache_on_disk.py +26 -0
  51. capytaine/tools/deprecation_handling.py +18 -0
  52. capytaine/tools/lists_of_points.py +52 -0
  53. capytaine/tools/memory_monitor.py +45 -0
  54. capytaine/tools/optional_imports.py +27 -0
  55. capytaine/tools/prony_decomposition.py +150 -0
  56. capytaine/tools/symbolic_multiplication.py +161 -0
  57. capytaine/tools/timer.py +90 -0
  58. capytaine/ui/__init__.py +0 -0
  59. capytaine/ui/cli.py +28 -0
  60. capytaine/ui/rich.py +5 -0
  61. capytaine-3.0.0a1.dist-info/LICENSE +674 -0
  62. capytaine-3.0.0a1.dist-info/METADATA +755 -0
  63. capytaine-3.0.0a1.dist-info/RECORD +65 -0
  64. capytaine-3.0.0a1.dist-info/WHEEL +6 -0
  65. capytaine-3.0.0a1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,601 @@
1
+ """Definition of the problems to solve with the BEM solver, and the results of this resolution."""
2
+ # Copyright (C) 2017-2023 Matthieu Ancellin
3
+ # See LICENSE file at <https://github.com/capytaine/capytaine>
4
+
5
+ import logging
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from scipy.optimize import newton
10
+
11
+ from capytaine.tools.deprecation_handling import _get_water_depth
12
+ from capytaine.bem.airy_waves import airy_waves_velocity, froude_krylov_force
13
+ from capytaine.bodies.dofs import AbstractDof
14
+ from capytaine.tools.symbolic_multiplication import SymbolicMultiplication
15
+
16
+ LOG = logging.getLogger(__name__)
17
+
18
+ _default_parameters = {'rho': 1000.0, 'g': 9.81, 'omega': 1.0,
19
+ 'free_surface': 0.0, 'water_depth': np.inf,
20
+ 'wave_direction': 0.0, 'forward_speed': 0.0}
21
+
22
+
23
+
24
+ class LinearPotentialFlowProblem:
25
+ """General class of a potential flow problem.
26
+
27
+ At most one of the following parameters must be provided:
28
+ omega, freq, period, wavenumber or wavelength.
29
+
30
+ Parameters
31
+ ----------
32
+ body: FloatingBody, optional
33
+ The body interacting with the waves
34
+ free_surface: float, optional
35
+ The position of the free surface (accepted values: 0 and np.inf)
36
+ water_depth: float, optional
37
+ The depth of water in m (default: np.inf)
38
+ sea_bottom: float, optional
39
+ The position of the sea bottom (deprecated: please prefer setting water_depth)
40
+ omega: float, optional
41
+ The angular frequency of the waves in rad/s
42
+ freq: float, optional
43
+ The frequency of the waves in Hz
44
+ period: float, optional
45
+ The period of the waves in s
46
+ wavenumber: float, optional
47
+ The angular wave number of the waves in rad/m
48
+ wavelength: float, optional
49
+ The wave length of the waves in m
50
+ forward_speed: float, optional
51
+ The speed of the body (in m/s, in the x direction, default: 0.0)
52
+ rho: float, optional
53
+ The density of water in kg/m3 (default: 1000.0)
54
+ g: float, optional
55
+ The acceleration of gravity in m/s2 (default: 9.81)
56
+ boundary_condition: np.ndarray of shape (body.mesh.nb_faces,), optional
57
+ The Neumann boundary condition on the floating body
58
+ """
59
+
60
+ def __init__(self, *,
61
+ body=None,
62
+ free_surface=_default_parameters['free_surface'],
63
+ water_depth=None, sea_bottom=None,
64
+ omega=None, freq=None, period=None, wavenumber=None, wavelength=None,
65
+ forward_speed=_default_parameters['forward_speed'],
66
+ rho=_default_parameters['rho'],
67
+ g=_default_parameters['g'],
68
+ wave_direction=_default_parameters['wave_direction'],
69
+ boundary_condition=None):
70
+
71
+ self.body = body
72
+ self.free_surface = float(free_surface)
73
+ self.rho = float(rho)
74
+ self.g = float(g)
75
+ self.forward_speed = float(forward_speed)
76
+ self.wave_direction = float(wave_direction) # Required for (diffraction problem) and (radiation problems with forward speed).
77
+
78
+ self.boundary_condition = boundary_condition
79
+
80
+ self.water_depth = _get_water_depth(free_surface, water_depth, sea_bottom, default_water_depth=_default_parameters["water_depth"])
81
+ self.omega, self.freq, self.period, self.wavenumber, self.wavelength, self.provided_freq_type = \
82
+ self._get_frequencies(omega=omega, freq=freq, period=period, wavenumber=wavenumber, wavelength=wavelength)
83
+
84
+ self._check_data()
85
+
86
+ if forward_speed != 0.0:
87
+ dopplered_omega = self.omega - self.wavenumber*self.forward_speed*np.cos(self.wave_direction)
88
+ self.encounter_omega, self.encounter_freq, self.encounter_period, self.encounter_wavenumber, self.encounter_wavelength, _ = \
89
+ self._get_frequencies(omega=abs(dopplered_omega))
90
+
91
+ if dopplered_omega >= 0.0:
92
+ self.encounter_wave_direction = self.wave_direction
93
+ else:
94
+ self.encounter_wave_direction = self.wave_direction + np.pi
95
+ else:
96
+ self.encounter_omega = self.omega
97
+ self.encounter_freq = self.freq
98
+ self.encounter_period = self.period
99
+ self.encounter_wavenumber = self.wavenumber
100
+ self.encounter_wavelength = self.wavelength
101
+ self.encounter_wave_direction = self.wave_direction
102
+
103
+
104
+ def _get_frequencies(self, *, omega=None, freq=None, period=None, wavenumber=None, wavelength=None):
105
+ frequency_data = dict(omega=omega, freq=freq, period=period, wavenumber=wavenumber, wavelength=wavelength)
106
+ nb_provided_frequency_data = len(frequency_data) - list(frequency_data.values()).count(None)
107
+
108
+ if nb_provided_frequency_data > 1:
109
+ raise ValueError("Settings a problem requires at most one of the following: omega (angular frequency) OR freq (in Hz) OR period OR wavenumber OR wavelength.\n"
110
+ "Received {} of them: {}".format(nb_provided_frequency_data, {k: v for k, v in frequency_data.items() if v is not None}))
111
+
112
+ if nb_provided_frequency_data == 0:
113
+ provided_freq_type = 'omega'
114
+ frequency_data = {'omega': _default_parameters['omega']}
115
+ else:
116
+ provided_freq_type = [k for (k, v) in frequency_data.items() if v is not None][0]
117
+
118
+ if ((float(frequency_data[provided_freq_type]) == 0.0 and provided_freq_type in {'omega', 'freq', 'wavenumber'})
119
+ or (float(frequency_data[provided_freq_type]) == np.inf and provided_freq_type in {'period', 'wavelength'})):
120
+ omega = SymbolicMultiplication("0")
121
+ freq = SymbolicMultiplication("0")
122
+ wavenumber = SymbolicMultiplication("0")
123
+ period = SymbolicMultiplication("∞")
124
+ wavelength = SymbolicMultiplication("∞")
125
+ elif ((float(frequency_data[provided_freq_type]) == 0.0 and provided_freq_type in {'period', 'wavelength'})
126
+ or (float(frequency_data[provided_freq_type]) == np.inf and provided_freq_type in {'omega', 'freq', 'wavenumber'})):
127
+ omega = SymbolicMultiplication("∞")
128
+ freq = SymbolicMultiplication("∞")
129
+ wavenumber = SymbolicMultiplication("∞")
130
+ period = SymbolicMultiplication("0")
131
+ wavelength = SymbolicMultiplication("0")
132
+ else:
133
+
134
+ if provided_freq_type in {'omega', 'freq', 'period'}:
135
+ if provided_freq_type == 'omega':
136
+ omega = frequency_data['omega']
137
+ period = 2*np.pi/omega
138
+ freq = omega/2/np.pi
139
+ elif provided_freq_type == 'freq':
140
+ freq = frequency_data['freq']
141
+ omega = 2*np.pi*freq
142
+ period = 1/freq
143
+ else: # provided_freq_type is 'period'
144
+ period = frequency_data['period']
145
+ omega = 2*np.pi/period
146
+ freq = 1/period
147
+
148
+ if self.water_depth == np.inf:
149
+ wavenumber = omega**2/self.g
150
+ else:
151
+ wavenumber = newton(lambda k: k*np.tanh(k*self.water_depth) - omega**2/self.g, x0=1.0)
152
+ wavelength = 2*np.pi/wavenumber
153
+
154
+ else: # provided_freq_type is 'wavelength' or 'wavenumber'
155
+ if provided_freq_type == 'wavelength':
156
+ wavelength = frequency_data['wavelength']
157
+ wavenumber = 2*np.pi/wavelength
158
+ else: # provided_freq_type is 'wavenumber'
159
+ wavenumber = frequency_data['wavenumber']
160
+ wavelength = 2*np.pi/wavenumber
161
+
162
+ omega = np.sqrt(self.g*wavenumber*np.tanh(wavenumber*self.water_depth))
163
+ period = 2*np.pi/omega
164
+ freq = 1/period
165
+
166
+ return omega, freq, period, wavenumber, wavelength, provided_freq_type
167
+
168
+ def _check_data(self):
169
+ """Sanity checks on the data."""
170
+
171
+ if self.free_surface not in {0.0, np.inf}:
172
+ raise NotImplementedError(
173
+ f"Free surface is {self.free_surface}. "
174
+ "Only z=0 and z=∞ are accepted values for the free surface position."
175
+ )
176
+
177
+ if not (-2*np.pi-1e-3 <= self.wave_direction <= 2*np.pi+1e-3):
178
+ LOG.warning(f"The value {self.wave_direction} has been provided for the wave direction, and it does not look like an angle in radians. "
179
+ "The wave direction in Capytaine is defined in radians and not in degrees, so the result might not be what you expect. "
180
+ "If you were actually giving an angle in radians, use the modulo operator to give a value between -2π and 2π to disable this warning.")
181
+
182
+ if self.free_surface == np.inf and self.water_depth != np.inf:
183
+ raise NotImplementedError(
184
+ "Problems with a sea bottom but no free surface have not been implemented."
185
+ )
186
+
187
+ if self.water_depth < 0.0:
188
+ raise ValueError("`water_depth` should be strictly positive (provided water depth: {self.water_depth}).")
189
+
190
+ if float(self.omega) in {0, np.inf}:
191
+ if self.forward_speed != 0.0:
192
+ raise NotImplementedError(
193
+ f"omega={float(self.omega)} is only implemented without forward speed (provided forward speed: {self.forward_speed})."
194
+ )
195
+
196
+
197
+ if self.body is not None:
198
+ if self.body.mesh.nb_faces == 0:
199
+ raise ValueError(f"The mesh of the body {self.body.__short_str__()} is empty.")
200
+
201
+ self.body._check_dofs_shape_consistency()
202
+
203
+ panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface + 1e-8
204
+ panels_below_sb = self.body.mesh.faces_centers[:, 2] <= -self.water_depth
205
+ if (any(panels_above_fs) or any(panels_below_sb)):
206
+
207
+ if not any(panels_below_sb):
208
+ issue = f"{np.count_nonzero(panels_above_fs)} panels above the free surface"
209
+ elif not any(panels_above_fs):
210
+ issue = f"{np.count_nonzero(panels_below_sb)} panels below the sea bottom"
211
+ else:
212
+ issue = (f"{np.count_nonzero(panels_above_fs)} panels above the free surface " +
213
+ f"and {np.count_nonzero(panels_below_sb)} panels below the sea bottom")
214
+
215
+ LOG.warning(
216
+ f"The mesh of the body {self.body.__short_str__()} has {issue}.\n" +
217
+ "It has been clipped to fit inside the domain.\n" +
218
+ "To remove this warning, clip the mesh manually with the `immersed_part()` method."
219
+ )
220
+
221
+ self.body = self.body.immersed_part(free_surface=self.free_surface,
222
+ water_depth=self.water_depth)
223
+
224
+ if self.boundary_condition is not None:
225
+ if len(self.boundary_condition.shape) != 1:
226
+ raise ValueError(f"Expected a 1-dimensional array as boundary_condition. Provided boundary condition's shape: {self.boundary_condition.shape}.")
227
+
228
+ if self.boundary_condition.shape[0] != self.body.mesh_including_lid.nb_faces:
229
+ raise ValueError(
230
+ f"The shape of the boundary condition ({self.boundary_condition.shape})"
231
+ f"does not match the number of faces of the mesh ({self.body.mesh.nb_faces})."
232
+ )
233
+
234
+ @property
235
+ def body_name(self):
236
+ return self.body.name if self.body is not None else 'None'
237
+
238
+ def _asdict(self):
239
+ return {"body_name": self.body_name,
240
+ "water_depth": self.water_depth,
241
+ "free_surface": self.free_surface,
242
+ "omega": float(self.omega),
243
+ "freq": float(self.freq),
244
+ "encounter_omega": float(self.encounter_omega),
245
+ "encounter_freq": float(self.encounter_freq),
246
+ "period": float(self.period),
247
+ "wavelength": float(self.wavelength),
248
+ "wavenumber": float(self.wavenumber),
249
+ "forward_speed": self.forward_speed,
250
+ "wave_direction": self.wave_direction,
251
+ "encounter_wave_direction": self.encounter_wave_direction,
252
+ "rho": self.rho,
253
+ "g": self.g}
254
+
255
+ @staticmethod
256
+ def _group_for_parallel_resolution(problems):
257
+ """Given a list of problems, returns a list of groups of problems, such
258
+ that each group should be executed in the same process to benefit from
259
+ caching.
260
+ """
261
+ problems_params = pd.DataFrame([pb._asdict() for pb in problems])
262
+ groups_of_indices = problems_params.groupby(["body_name", "water_depth", "omega", "rho", "g"]).groups.values()
263
+ groups_of_problems = [[problems[i] for i in grp] for grp in groups_of_indices]
264
+ return groups_of_problems
265
+
266
+ def __str__(self):
267
+ """Do not display default values in str(problem)."""
268
+ parameters = [f"body={self.body.__short_str__() if self.body is not None else None}",
269
+ f"{self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type)):.3f}",
270
+ f"water_depth={self.water_depth}"]
271
+
272
+ if not self.forward_speed == _default_parameters['forward_speed']:
273
+ parameters.append(f"forward_speed={self.forward_speed:.3f}")
274
+
275
+ try:
276
+ parameters.extend(self._str_other_attributes())
277
+ except AttributeError:
278
+ pass
279
+
280
+ if not self.free_surface == _default_parameters['free_surface']:
281
+ parameters.append(f"free_surface={self.free_surface}")
282
+ if not self.g == _default_parameters['g']:
283
+ parameters.append(f"g={self.g}")
284
+ if not self.rho == _default_parameters['rho']:
285
+ parameters.append(f"rho={self.rho}")
286
+
287
+ return self.__class__.__name__ + "(" + ', '.join(parameters) + ")"
288
+
289
+ def __repr__(self):
290
+ return self.__str__()
291
+
292
+ def _repr_pretty_(self, p, cycle):
293
+ p.text(self.__str__())
294
+
295
+ def __rich_repr__(self):
296
+ yield "body", self.body, None
297
+ yield self.provided_freq_type, self.__getattribute__(self.provided_freq_type)
298
+ yield "water_depth", self.water_depth, _default_parameters["water_depth"]
299
+ try:
300
+ yield from self._specific_rich_repr()
301
+ except:
302
+ pass
303
+ yield "g", self.g, _default_parameters["g"]
304
+ yield "rho", self.rho, _default_parameters["rho"]
305
+
306
+ def _astuple(self):
307
+ return (self.body, self.free_surface, self.water_depth,
308
+ float(self.omega), float(self.period), float(self.wavenumber), float(self.wavelength),
309
+ self.forward_speed, self.rho, self.g)
310
+
311
+ def __eq__(self, other):
312
+ if isinstance(other, LinearPotentialFlowProblem):
313
+ return self._astuple() == other._astuple()
314
+ else:
315
+ return NotImplemented
316
+
317
+ def __lt__(self, other):
318
+ # Arbitrary order. Used for ordering of problems: problems with same body are grouped together.
319
+ if isinstance(other, LinearPotentialFlowProblem):
320
+ return self._astuple()[:9] < other._astuple()[:9]
321
+ # Not the whole tuple, because when using inheriting classes,
322
+ # "radiating_dof" cannot be compared with "wave_direction"
323
+ else:
324
+ return NotImplemented
325
+
326
+ @property
327
+ def depth(self):
328
+ return self.water_depth
329
+
330
+ @property
331
+ def influenced_dofs(self):
332
+ # TODO: let the user choose the influenced dofs
333
+ return self.body.dofs if self.body is not None else set()
334
+
335
+ def make_results_container(self, *args, **kwargs):
336
+ return LinearPotentialFlowResult(self, *args, **kwargs)
337
+
338
+ def make_failed_results_container(self, *args, **kwargs):
339
+ return FailedLinearPotentialFlowResult(self, *args, **kwargs)
340
+
341
+
342
+ class DiffractionProblem(LinearPotentialFlowProblem):
343
+ """Particular LinearPotentialFlowProblem with boundary conditions
344
+ computed from an incoming Airy wave."""
345
+
346
+ def __init__(self, *,
347
+ body=None,
348
+ free_surface=_default_parameters['free_surface'],
349
+ water_depth=None, sea_bottom=None,
350
+ omega=None, freq=None, period=None, wavenumber=None, wavelength=None,
351
+ forward_speed=_default_parameters['forward_speed'],
352
+ rho=_default_parameters['rho'],
353
+ g=_default_parameters['g'],
354
+ wave_direction=_default_parameters['wave_direction']):
355
+
356
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
357
+ omega=omega, freq=freq, period=period, wavenumber=wavenumber, wavelength=wavelength, wave_direction=wave_direction,
358
+ forward_speed=forward_speed, rho=rho, g=g)
359
+
360
+ if self.body is not None:
361
+
362
+ self.boundary_condition = np.zeros(
363
+ shape=(self.body.mesh_including_lid.nb_faces,),
364
+ dtype=np.complex128
365
+ )
366
+
367
+ self.boundary_condition[self.body.hull_mask] = -(
368
+ airy_waves_velocity(self.body.mesh.faces_centers, self)
369
+ * self.body.mesh.faces_normals
370
+ ).sum(axis=1)
371
+ # Note that even with forward speed, this is computed based on the
372
+ # frequency and not the encounter frequency.
373
+
374
+ if len(self.body.dofs) == 0:
375
+ LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
376
+
377
+ def _str_other_attributes(self):
378
+ return [f"wave_direction={self.wave_direction:.3f}"]
379
+
380
+ def _specific_rich_repr(self):
381
+ yield "wave_direction", self.wave_direction, _default_parameters["wave_direction"]
382
+
383
+ def make_results_container(self, *args, **kwargs):
384
+ return DiffractionResult(self, *args, **kwargs)
385
+
386
+ def make_failed_results_container(self, *args, **kwargs):
387
+ return FailedDiffractionResult(self, *args, **kwargs)
388
+
389
+
390
+ class RadiationProblem(LinearPotentialFlowProblem):
391
+ """Particular LinearPotentialFlowProblem whose boundary conditions have
392
+ been computed from the degree of freedom of the body."""
393
+
394
+ def __init__(self, *, body=None,
395
+ free_surface=_default_parameters['free_surface'],
396
+ water_depth=None, sea_bottom=None,
397
+ omega=None, freq=None, period=None, wavenumber=None, wavelength=None,
398
+ forward_speed=_default_parameters['forward_speed'],
399
+ wave_direction=_default_parameters['wave_direction'],
400
+ rho=_default_parameters['rho'],
401
+ g=_default_parameters['g'],
402
+ radiating_dof=None):
403
+
404
+ self.radiating_dof = radiating_dof
405
+
406
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
407
+ omega=omega, freq=freq, period=period, wavenumber=wavenumber, wavelength=wavelength,
408
+ wave_direction=wave_direction, forward_speed=forward_speed, rho=rho, g=g)
409
+
410
+ if self.body is not None:
411
+
412
+ if len(self.body.dofs) == 0:
413
+ raise ValueError(f"Body {self.body.name} does not have any degrees of freedom.")
414
+
415
+ if self.radiating_dof is None:
416
+ self.radiating_dof = next(iter(self.body.dofs))
417
+
418
+ if self.radiating_dof not in self.body.dofs:
419
+ raise ValueError(f"In {self}:\n"
420
+ f"the radiating dof {repr(self.radiating_dof)} is not one of the degrees of freedom of the body.\n"
421
+ f"The dofs of the body are {list(self.body.dofs.keys())}")
422
+
423
+ dof = self.body.dofs[self.radiating_dof]
424
+ if isinstance(dof, AbstractDof):
425
+ dof_motion = dof.evaluate_motion(self.body.mesh)
426
+ else:
427
+ dof_motion = dof
428
+
429
+ self.boundary_condition = self.encounter_omega * np.zeros(
430
+ shape=(self.body.mesh_including_lid.nb_faces,),
431
+ dtype=np.complex128
432
+ )
433
+ # The multiplication by encounter_omega is just a programming trick to ensure that boundary_condition
434
+ # is implemented with the correct type (for zero and infinite frequencies), it does not affect the value.
435
+ # Below the value is update on the hull. It remains zero on the lid.
436
+
437
+ displacement_on_face = np.sum(dof_motion * self.body.mesh.faces_normals, axis=1) # This is a dot product on each face
438
+ self.boundary_condition[self.body.hull_mask] = -1j * self.encounter_omega * displacement_on_face
439
+
440
+ if self.forward_speed != 0.0:
441
+ # Adding the "m-terms":
442
+ try:
443
+ ddofdx = dof.evaluate_gradient_of_motion(self.body.mesh)[:, :, 0]
444
+ ddofdx_dot_n = np.sum(ddofdx * self.body.mesh.faces_normals)
445
+ except AttributeError:
446
+ raise NotImplementedError(
447
+ "Radiation problem with forward speed is currently only implemented for rigid bodies.\n"
448
+ "Only radiating dofs instantiating a TranslationDof or a RotationDof are supported.\n"
449
+ f"Got instead `radiating_dof={self.radiating_dof}`"
450
+ )
451
+
452
+ self.boundary_condition[self.body.hull_mask] += self.forward_speed * ddofdx_dot_n
453
+
454
+
455
+
456
+ def _astuple(self):
457
+ return super()._astuple() + (self.radiating_dof,)
458
+
459
+ def _asdict(self):
460
+ d = super()._asdict()
461
+ d["radiating_dof"] = self.radiating_dof
462
+ return d
463
+
464
+ def _str_other_attributes(self):
465
+ if self.forward_speed != 0.0:
466
+ return [f"wave_direction={self.wave_direction:.3f}, radiating_dof=\'{self.radiating_dof}\'"]
467
+ else:
468
+ return [f"radiating_dof=\'{self.radiating_dof}\'"]
469
+
470
+ def _specific_rich_repr(self):
471
+ yield "radiating_dof", self.radiating_dof
472
+
473
+ def make_results_container(self, *args, **kwargs):
474
+ return RadiationResult(self, *args, **kwargs)
475
+
476
+ def make_failed_results_container(self, *args, **kwargs):
477
+ return FailedRadiationResult(self, *args, **kwargs)
478
+
479
+
480
+ class LinearPotentialFlowResult:
481
+
482
+ def __init__(self, problem, forces=None, sources=None, potential=None, pressure=None):
483
+ self.problem = problem
484
+
485
+ self.forces = forces if forces is not None else {}
486
+ self.sources = sources
487
+ self.potential = potential
488
+ self.pressure = pressure
489
+
490
+ self.fs_elevation = {} # Only used in legacy `get_free_surface_elevation`. To be removed?
491
+
492
+ # Copy data from problem
493
+ self.body = self.problem.body
494
+ self.free_surface = self.problem.free_surface
495
+ self.omega = self.problem.omega
496
+ self.freq = self.problem.freq
497
+ self.period = self.problem.period
498
+ self.wavenumber = self.problem.wavenumber
499
+ self.wavelength = self.problem.wavelength
500
+ self.forward_speed = self.problem.forward_speed
501
+ self.wave_direction = self.problem.wave_direction
502
+ self.encounter_omega = self.problem.encounter_omega
503
+ self.encounter_freq = self.problem.encounter_freq
504
+ self.encounter_period = self.problem.encounter_period
505
+ self.encounter_wavenumber = self.problem.encounter_wavenumber
506
+ self.encounter_wavelength = self.problem.encounter_wavelength
507
+ self.encounter_wave_direction = self.problem.encounter_wave_direction
508
+ self.rho = self.problem.rho
509
+ self.g = self.problem.g
510
+ self.boundary_condition = self.problem.boundary_condition
511
+ self.water_depth = self.problem.water_depth
512
+ self.depth = self.problem.water_depth
513
+ self.provided_freq_type = self.problem.provided_freq_type
514
+ self.body_name = self.problem.body_name
515
+ self.influenced_dofs = self.problem.influenced_dofs
516
+
517
+ @property
518
+ def force(self):
519
+ # Just an alias
520
+ return self.forces
521
+
522
+ __str__ = LinearPotentialFlowProblem.__str__
523
+ __repr__ = LinearPotentialFlowProblem.__repr__
524
+ _repr_pretty_ = LinearPotentialFlowProblem._repr_pretty_
525
+ __rich_repr__ = LinearPotentialFlowProblem.__rich_repr__
526
+
527
+
528
+ class FailedLinearPotentialFlowResult(LinearPotentialFlowResult):
529
+ def __init__(self, problem, exception):
530
+ LinearPotentialFlowResult.__init__(self, problem)
531
+ self.forces = {dof: np.nan + 1j*np.nan for dof in self.influenced_dofs}
532
+ self.exception = exception
533
+
534
+
535
+ class DiffractionResult(LinearPotentialFlowResult):
536
+
537
+ def __init__(self, problem, *args, **kwargs):
538
+ super().__init__(problem, *args, **kwargs)
539
+
540
+ _str_other_attributes = DiffractionProblem._str_other_attributes
541
+ _specific_rich_repr = DiffractionProblem._specific_rich_repr
542
+
543
+ @property
544
+ def records(self):
545
+ params = self.problem._asdict()
546
+ FK = froude_krylov_force(self.problem)
547
+ return [dict(**params,
548
+ influenced_dof=dof,
549
+ diffraction_force=self.forces[dof],
550
+ Froude_Krylov_force=FK[dof],
551
+ kind="DiffractionResult")
552
+ for dof in self.influenced_dofs]
553
+
554
+
555
+ class FailedDiffractionResult(DiffractionResult):
556
+ def __init__(self, problem, exception):
557
+ DiffractionResult.__init__(self, problem)
558
+ self.forces = {dof: np.nan for dof in self.influenced_dofs}
559
+ self.exception = exception
560
+
561
+
562
+ class RadiationResult(LinearPotentialFlowResult):
563
+
564
+ def __init__(self, problem, *args, **kwargs):
565
+ super().__init__(problem, *args, **kwargs)
566
+ self.radiating_dof = self.problem.radiating_dof
567
+
568
+ _str_other_attributes = RadiationProblem._str_other_attributes
569
+ _specific_rich_repr = RadiationProblem._specific_rich_repr
570
+
571
+ @property
572
+ def added_mass(self):
573
+ return {dof: float(np.real(force)/(self.encounter_omega*self.encounter_omega)) for (dof, force) in self.forces.items()}
574
+
575
+ @property
576
+ def radiation_damping(self):
577
+ if float(self.encounter_omega) in {0.0, np.inf} and self.forward_speed == 0.0:
578
+ return {dof: 0.0 for dof in self.forces.keys()}
579
+ else:
580
+ return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
581
+
582
+ # Aliases for backward compatibility
583
+ added_masses = added_mass
584
+ radiation_dampings = radiation_damping
585
+
586
+ @property
587
+ def records(self):
588
+ params = self.problem._asdict()
589
+ return [dict(params,
590
+ influenced_dof=dof,
591
+ added_mass=self.added_mass[dof],
592
+ radiation_damping=self.radiation_damping[dof],
593
+ kind="RadiationResult")
594
+ for dof in self.influenced_dofs]
595
+
596
+
597
+ class FailedRadiationResult(RadiationResult):
598
+ def __init__(self, problem, exception):
599
+ RadiationResult.__init__(self, problem)
600
+ self.forces = {dof: np.nan + 1j*np.nan for dof in self.influenced_dofs}
601
+ self.exception = exception