capytaine 2.2__cp312-cp312-macosx_11_0_arm64.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 (76) 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 +16 -0
  5. capytaine/__init__.py +35 -0
  6. capytaine/bem/__init__.py +0 -0
  7. capytaine/bem/airy_waves.py +106 -0
  8. capytaine/bem/engines.py +441 -0
  9. capytaine/bem/problems_and_results.py +545 -0
  10. capytaine/bem/solver.py +497 -0
  11. capytaine/bodies/__init__.py +4 -0
  12. capytaine/bodies/bodies.py +1185 -0
  13. capytaine/bodies/dofs.py +19 -0
  14. capytaine/bodies/predefined/__init__.py +6 -0
  15. capytaine/bodies/predefined/cylinders.py +151 -0
  16. capytaine/bodies/predefined/rectangles.py +109 -0
  17. capytaine/bodies/predefined/spheres.py +70 -0
  18. capytaine/green_functions/__init__.py +2 -0
  19. capytaine/green_functions/abstract_green_function.py +12 -0
  20. capytaine/green_functions/delhommeau.py +432 -0
  21. capytaine/green_functions/libs/Delhommeau_float32.cpython-312-darwin.so +0 -0
  22. capytaine/green_functions/libs/Delhommeau_float64.cpython-312-darwin.so +0 -0
  23. capytaine/green_functions/libs/__init__.py +0 -0
  24. capytaine/io/__init__.py +0 -0
  25. capytaine/io/bemio.py +141 -0
  26. capytaine/io/legacy.py +328 -0
  27. capytaine/io/mesh_loaders.py +1085 -0
  28. capytaine/io/mesh_writers.py +692 -0
  29. capytaine/io/meshio.py +38 -0
  30. capytaine/io/xarray.py +516 -0
  31. capytaine/matrices/__init__.py +16 -0
  32. capytaine/matrices/block.py +590 -0
  33. capytaine/matrices/block_toeplitz.py +325 -0
  34. capytaine/matrices/builders.py +89 -0
  35. capytaine/matrices/linear_solvers.py +232 -0
  36. capytaine/matrices/low_rank.py +393 -0
  37. capytaine/meshes/__init__.py +6 -0
  38. capytaine/meshes/clipper.py +464 -0
  39. capytaine/meshes/collections.py +324 -0
  40. capytaine/meshes/geometry.py +409 -0
  41. capytaine/meshes/meshes.py +868 -0
  42. capytaine/meshes/predefined/__init__.py +6 -0
  43. capytaine/meshes/predefined/cylinders.py +314 -0
  44. capytaine/meshes/predefined/rectangles.py +261 -0
  45. capytaine/meshes/predefined/spheres.py +62 -0
  46. capytaine/meshes/properties.py +242 -0
  47. capytaine/meshes/quadratures.py +80 -0
  48. capytaine/meshes/quality.py +448 -0
  49. capytaine/meshes/surface_integrals.py +63 -0
  50. capytaine/meshes/symmetric.py +383 -0
  51. capytaine/post_pro/__init__.py +6 -0
  52. capytaine/post_pro/free_surfaces.py +88 -0
  53. capytaine/post_pro/impedance.py +92 -0
  54. capytaine/post_pro/kochin.py +54 -0
  55. capytaine/post_pro/rao.py +60 -0
  56. capytaine/tools/__init__.py +0 -0
  57. capytaine/tools/cache_on_disk.py +26 -0
  58. capytaine/tools/deprecation_handling.py +18 -0
  59. capytaine/tools/lists_of_points.py +52 -0
  60. capytaine/tools/lru_cache.py +49 -0
  61. capytaine/tools/optional_imports.py +27 -0
  62. capytaine/tools/prony_decomposition.py +94 -0
  63. capytaine/tools/symbolic_multiplication.py +107 -0
  64. capytaine/ui/__init__.py +0 -0
  65. capytaine/ui/cli.py +28 -0
  66. capytaine/ui/rich.py +5 -0
  67. capytaine/ui/vtk/__init__.py +3 -0
  68. capytaine/ui/vtk/animation.py +329 -0
  69. capytaine/ui/vtk/body_viewer.py +28 -0
  70. capytaine/ui/vtk/helpers.py +82 -0
  71. capytaine/ui/vtk/mesh_viewer.py +461 -0
  72. capytaine-2.2.dist-info/LICENSE +674 -0
  73. capytaine-2.2.dist-info/METADATA +751 -0
  74. capytaine-2.2.dist-info/RECORD +76 -0
  75. capytaine-2.2.dist-info/WHEEL +4 -0
  76. capytaine-2.2.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,545 @@
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.meshes.collections import CollectionOfMeshes
13
+ from capytaine.bem.airy_waves import airy_waves_velocity, froude_krylov_force
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 parameter must be provided: omega, period, wavenumber or wavelength.
28
+ Internally only omega is stored, hence setting another parameter can lead to small rounding errors.
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
+ period: float, optional
43
+ The period of the waves in s
44
+ wavenumber: float, optional
45
+ The angular wave number of the waves in rad/m
46
+ wavelength: float, optional
47
+ The wave length of the waves in m
48
+ forward_speed: float, optional
49
+ The speed of the body (in m/s, in the x direction, default: 0.0)
50
+ rho: float, optional
51
+ The density of water in kg/m3 (default: 1000.0)
52
+ g: float, optional
53
+ The acceleration of gravity in m/s2 (default: 9.81)
54
+ boundary_condition: np.ndarray of shape (body.mesh.nb_faces,), optional
55
+ The Neumann boundary condition on the floating body
56
+ """
57
+
58
+ def __init__(self, *,
59
+ body=None,
60
+ free_surface=_default_parameters['free_surface'],
61
+ water_depth=None, sea_bottom=None,
62
+ omega=None, period=None, wavenumber=None, wavelength=None,
63
+ forward_speed=_default_parameters['forward_speed'],
64
+ rho=_default_parameters['rho'],
65
+ g=_default_parameters['g'],
66
+ wave_direction=_default_parameters['wave_direction'],
67
+ boundary_condition=None):
68
+
69
+ self.body = body
70
+ self.free_surface = float(free_surface)
71
+ self.rho = float(rho)
72
+ self.g = float(g)
73
+ self.forward_speed = float(forward_speed)
74
+ self.wave_direction = float(wave_direction) # Required for (diffraction problem) and (radiation problems with forward speed).
75
+
76
+ self.boundary_condition = boundary_condition
77
+
78
+ self.water_depth = _get_water_depth(free_surface, water_depth, sea_bottom, default_water_depth=_default_parameters["water_depth"])
79
+ self.omega, self.period, self.wavenumber, self.wavelength, self.provided_freq_type = \
80
+ self._get_frequencies(omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength)
81
+
82
+ self._check_data()
83
+
84
+ if forward_speed != 0.0:
85
+ dopplered_omega = self.omega - self.wavenumber*self.forward_speed*np.cos(self.wave_direction)
86
+ self.encounter_omega, self.encounter_period, self.encounter_wavenumber, self.encounter_wavelength, _ = \
87
+ self._get_frequencies(omega=abs(dopplered_omega))
88
+
89
+ if dopplered_omega >= 0.0:
90
+ self.encounter_wave_direction = self.wave_direction
91
+ else:
92
+ self.encounter_wave_direction = self.wave_direction + np.pi
93
+ else:
94
+ self.encounter_omega = self.omega
95
+ self.encounter_period = self.period
96
+ self.encounter_wavenumber = self.wavenumber
97
+ self.encounter_wavelength = self.wavelength
98
+ self.encounter_wave_direction = self.wave_direction
99
+
100
+
101
+ def _get_frequencies(self, *, omega=None, period=None, wavenumber=None, wavelength=None):
102
+ frequency_data = dict(omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength)
103
+ nb_provided_frequency_data = 4 - list(frequency_data.values()).count(None)
104
+
105
+ if nb_provided_frequency_data > 1:
106
+ raise ValueError("Settings a problem requires at most one of the following: omega (angular frequency) OR period OR wavenumber OR wavelength.\n"
107
+ "Received {} of them: {}".format(nb_provided_frequency_data, {k: v for k, v in frequency_data.items() if v is not None}))
108
+
109
+ if nb_provided_frequency_data == 0:
110
+ provided_freq_type = 'omega'
111
+ frequency_data = {'omega': _default_parameters['omega']}
112
+ else:
113
+ provided_freq_type = [k for (k, v) in frequency_data.items() if v is not None][0]
114
+
115
+ if ((float(frequency_data[provided_freq_type]) == 0.0 and provided_freq_type in {'omega', 'wavenumber'})
116
+ or (float(frequency_data[provided_freq_type]) == np.inf and provided_freq_type in {'period', 'wavelength'})):
117
+ omega = SymbolicMultiplication("0")
118
+ wavenumber = SymbolicMultiplication("0")
119
+ period = SymbolicMultiplication("∞")
120
+ wavelength = SymbolicMultiplication("∞")
121
+ elif ((float(frequency_data[provided_freq_type]) == 0.0 and provided_freq_type in {'period', 'wavelength'})
122
+ or (float(frequency_data[provided_freq_type]) == np.inf and provided_freq_type in {'omega', 'wavenumber'})):
123
+ omega = SymbolicMultiplication("∞")
124
+ wavenumber = SymbolicMultiplication("∞")
125
+ period = SymbolicMultiplication("0")
126
+ wavelength = SymbolicMultiplication("0")
127
+ else:
128
+
129
+ if provided_freq_type in {'omega', 'period'}:
130
+ if provided_freq_type == 'omega':
131
+ omega = frequency_data['omega']
132
+ period = 2*np.pi/omega
133
+ else: # provided_freq_type is 'period'
134
+ period = frequency_data['period']
135
+ omega = 2*np.pi/period
136
+
137
+ if self.water_depth == np.inf:
138
+ wavenumber = omega**2/self.g
139
+ else:
140
+ wavenumber = newton(lambda k: k*np.tanh(k*self.water_depth) - omega**2/self.g, x0=1.0)
141
+ wavelength = 2*np.pi/wavenumber
142
+
143
+ else: # provided_freq_type is 'wavelength' or 'wavenumber'
144
+ if provided_freq_type == 'wavelength':
145
+ wavelength = frequency_data['wavelength']
146
+ wavenumber = 2*np.pi/wavelength
147
+ else: # provided_freq_type is 'wavenumber'
148
+ wavenumber = frequency_data['wavenumber']
149
+ wavelength = 2*np.pi/wavenumber
150
+
151
+ omega = np.sqrt(self.g*wavenumber*np.tanh(wavenumber*self.water_depth))
152
+ period = 2*np.pi/omega
153
+
154
+ return omega, period, wavenumber, wavelength, provided_freq_type
155
+
156
+ def _check_data(self):
157
+ """Sanity checks on the data."""
158
+
159
+ if self.free_surface not in {0.0, np.inf}:
160
+ raise NotImplementedError(
161
+ f"Free surface is {self.free_surface}. "
162
+ "Only z=0 and z=∞ are accepted values for the free surface position."
163
+ )
164
+
165
+ if not (-2*np.pi-1e-3 <= self.wave_direction <= 2*np.pi+1e-3):
166
+ 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. "
167
+ "The wave direction in Capytaine is defined in radians and not in degrees, so the result might not be what you expect. "
168
+ "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.")
169
+
170
+ if self.free_surface == np.inf and self.water_depth != np.inf:
171
+
172
+ raise NotImplementedError(
173
+ "Problems with a sea bottom but no free surface have not been implemented."
174
+ )
175
+
176
+ if self.water_depth < 0.0:
177
+ raise ValueError("`water_depth` should be strictly positive (provided water depth: {self.water_depth}).")
178
+
179
+ if float(self.omega) in {0, np.inf}:
180
+ if self.water_depth != np.inf:
181
+ LOG.warning(
182
+ f"Default Green function allows for {self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type))} only for infinite depth (provided water depth: {self.water_depth})."
183
+ )
184
+
185
+ if self.forward_speed != 0.0:
186
+ raise NotImplementedError(
187
+ f"omega={float(self.omega)} is only implemented without forward speed (provided forward speed: {self.forward_speed})."
188
+ )
189
+
190
+
191
+ if self.body is not None:
192
+ if ((isinstance(self.body.mesh, CollectionOfMeshes) and len(self.body.mesh) == 0)
193
+ or len(self.body.mesh.faces) == 0):
194
+ raise ValueError(f"The mesh of the body {self.body.__short_str__()} is empty.")
195
+
196
+ panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface + 1e-8
197
+ panels_below_sb = self.body.mesh.faces_centers[:, 2] <= -self.water_depth
198
+ if (any(panels_above_fs) or any(panels_below_sb)):
199
+
200
+ if not any(panels_below_sb):
201
+ issue = f"{np.count_nonzero(panels_above_fs)} panels above the free surface"
202
+ elif not any(panels_above_fs):
203
+ issue = f"{np.count_nonzero(panels_below_sb)} panels below the sea bottom"
204
+ else:
205
+ issue = (f"{np.count_nonzero(panels_above_fs)} panels above the free surface " +
206
+ f"and {np.count_nonzero(panels_below_sb)} panels below the sea bottom")
207
+
208
+ LOG.warning(
209
+ f"The mesh of the body {self.body.__short_str__()} has {issue}.\n" +
210
+ "It has been clipped to fit inside the domain.\n" +
211
+ "To remove this warning, clip the mesh manually with the `immersed_part()` method."
212
+ )
213
+
214
+ self.body = self.body.immersed_part(free_surface=self.free_surface,
215
+ water_depth=self.water_depth)
216
+
217
+ if self.boundary_condition is not None:
218
+ if len(self.boundary_condition.shape) != 1:
219
+ raise ValueError(f"Expected a 1-dimensional array as boundary_condition. Provided boundary condition's shape: {self.boundary_condition.shape}.")
220
+
221
+ if self.boundary_condition.shape[0] != self.body.mesh_including_lid.nb_faces:
222
+ raise ValueError(
223
+ f"The shape of the boundary condition ({self.boundary_condition.shape})"
224
+ f"does not match the number of faces of the mesh ({self.body.mesh.nb_faces})."
225
+ )
226
+
227
+ @property
228
+ def body_name(self):
229
+ return self.body.name if self.body is not None else 'None'
230
+
231
+ def _asdict(self):
232
+ return {"body_name": self.body_name,
233
+ "water_depth": self.water_depth,
234
+ "omega": float(self.omega),
235
+ "encounter_omega": float(self.encounter_omega),
236
+ "period": float(self.period),
237
+ "wavelength": float(self.wavelength),
238
+ "wavenumber": float(self.wavenumber),
239
+ "forward_speed": self.forward_speed,
240
+ "wave_direction": self.wave_direction,
241
+ "encounter_wave_direction": self.encounter_wave_direction,
242
+ "rho": self.rho,
243
+ "g": self.g}
244
+
245
+ @staticmethod
246
+ def _group_for_parallel_resolution(problems):
247
+ """Given a list of problems, returns a list of groups of problems, such
248
+ that each group should be executed in the same process to benefit from
249
+ caching.
250
+ """
251
+ problems_params = pd.DataFrame([pb._asdict() for pb in problems])
252
+ groups_of_indices = problems_params.groupby(["body_name", "water_depth", "omega", "rho", "g"]).groups.values()
253
+ groups_of_problems = [[problems[i] for i in grp] for grp in groups_of_indices]
254
+ return groups_of_problems
255
+
256
+ def __str__(self):
257
+ """Do not display default values in str(problem)."""
258
+ parameters = [f"body={self.body.__short_str__() if self.body is not None else None}",
259
+ f"{self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type)):.3f}",
260
+ f"water_depth={self.water_depth}"]
261
+
262
+ if not self.forward_speed == _default_parameters['forward_speed']:
263
+ parameters.append(f"forward_speed={self.forward_speed:.3f}")
264
+
265
+ try:
266
+ parameters.extend(self._str_other_attributes())
267
+ except AttributeError:
268
+ pass
269
+
270
+ if not self.free_surface == _default_parameters['free_surface']:
271
+ parameters.append(f"free_surface={self.free_surface}")
272
+ if not self.g == _default_parameters['g']:
273
+ parameters.append(f"g={self.g}")
274
+ if not self.rho == _default_parameters['rho']:
275
+ parameters.append(f"rho={self.rho}")
276
+
277
+ return self.__class__.__name__ + "(" + ', '.join(parameters) + ")"
278
+
279
+ def __repr__(self):
280
+ return self.__str__()
281
+
282
+ def _repr_pretty_(self, p, cycle):
283
+ p.text(self.__str__())
284
+
285
+ def __rich_repr__(self):
286
+ yield "body", self.body, None
287
+ yield self.provided_freq_type, self.__getattribute__(self.provided_freq_type)
288
+ yield "water_depth", self.water_depth, _default_parameters["water_depth"]
289
+ try:
290
+ yield from self._specific_rich_repr()
291
+ except:
292
+ pass
293
+ yield "g", self.g, _default_parameters["g"]
294
+ yield "rho", self.rho, _default_parameters["rho"]
295
+
296
+ def _astuple(self):
297
+ return (self.body, self.free_surface, self.water_depth,
298
+ float(self.omega), float(self.period), float(self.wavenumber), float(self.wavelength),
299
+ self.forward_speed, self.rho, self.g)
300
+
301
+ def __eq__(self, other):
302
+ if isinstance(other, LinearPotentialFlowProblem):
303
+ return self._astuple() == other._astuple()
304
+ else:
305
+ return NotImplemented
306
+
307
+ def __lt__(self, other):
308
+ # Arbitrary order. Used for ordering of problems: problems with same body are grouped together.
309
+ if isinstance(other, LinearPotentialFlowProblem):
310
+ return self._astuple()[:9] < other._astuple()[:9]
311
+ # Not the whole tuple, because when using inheriting classes,
312
+ # "radiating_dof" cannot be compared with "wave_direction"
313
+ else:
314
+ return NotImplemented
315
+
316
+ @property
317
+ def depth(self):
318
+ return self.water_depth
319
+
320
+ @property
321
+ def influenced_dofs(self):
322
+ # TODO: let the user choose the influenced dofs
323
+ return self.body.dofs if self.body is not None else set()
324
+
325
+ def make_results_container(self):
326
+ return LinearPotentialFlowResult(self)
327
+
328
+
329
+ class DiffractionProblem(LinearPotentialFlowProblem):
330
+ """Particular LinearPotentialFlowProblem with boundary conditions
331
+ computed from an incoming Airy wave."""
332
+
333
+ def __init__(self, *,
334
+ body=None,
335
+ free_surface=_default_parameters['free_surface'],
336
+ water_depth=None, sea_bottom=None,
337
+ omega=None, period=None, wavenumber=None, wavelength=None,
338
+ forward_speed=_default_parameters['forward_speed'],
339
+ rho=_default_parameters['rho'],
340
+ g=_default_parameters['g'],
341
+ wave_direction=_default_parameters['wave_direction']):
342
+
343
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
344
+ omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength, wave_direction=wave_direction,
345
+ forward_speed=forward_speed, rho=rho, g=g)
346
+
347
+ if float(self.omega) in {0.0, np.inf}:
348
+ raise NotImplementedError("DiffractionProblem does not support zero or infinite frequency.")
349
+
350
+ if self.body is not None:
351
+
352
+ self.boundary_condition = -(
353
+ airy_waves_velocity(self.body.mesh.faces_centers, self)
354
+ * self.body.mesh.faces_normals
355
+ ).sum(axis=1)
356
+ # Note that even with forward speed, this is computed based on the
357
+ # frequency and not the encounter frequency.
358
+
359
+ if self.body.lid_mesh is not None:
360
+ self.boundary_condition = np.concatenate([self.boundary_condition, np.zeros(self.body.lid_mesh.nb_faces)])
361
+
362
+ if len(self.body.dofs) == 0:
363
+ LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
364
+
365
+ def _str_other_attributes(self):
366
+ return [f"wave_direction={self.wave_direction:.3f}"]
367
+
368
+ def _specific_rich_repr(self):
369
+ yield "wave_direction", self.wave_direction, _default_parameters["wave_direction"]
370
+
371
+ def make_results_container(self, *args, **kwargs):
372
+ return DiffractionResult(self, *args, **kwargs)
373
+
374
+
375
+ class RadiationProblem(LinearPotentialFlowProblem):
376
+ """Particular LinearPotentialFlowProblem whose boundary conditions have
377
+ been computed from the degree of freedom of the body."""
378
+
379
+ def __init__(self, *, body=None,
380
+ free_surface=_default_parameters['free_surface'],
381
+ water_depth=None, sea_bottom=None,
382
+ omega=None, period=None, wavenumber=None, wavelength=None,
383
+ forward_speed=_default_parameters['forward_speed'],
384
+ wave_direction=_default_parameters['wave_direction'],
385
+ rho=_default_parameters['rho'],
386
+ g=_default_parameters['g'],
387
+ radiating_dof=None):
388
+
389
+ self.radiating_dof = radiating_dof
390
+
391
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
392
+ omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength,
393
+ wave_direction=wave_direction, forward_speed=forward_speed, rho=rho, g=g)
394
+
395
+ if self.body is not None:
396
+
397
+ if len(self.body.dofs) == 0:
398
+ raise ValueError(f"Body {self.body.name} does not have any degrees of freedom.")
399
+
400
+ if self.radiating_dof is None:
401
+ self.radiating_dof = next(iter(self.body.dofs))
402
+
403
+ if self.radiating_dof not in self.body.dofs:
404
+ raise ValueError(f"In {self}:\n"
405
+ f"the radiating dof {repr(self.radiating_dof)} is not one of the degrees of freedom of the body.\n"
406
+ f"The dofs of the body are {list(self.body.dofs.keys())}")
407
+
408
+ dof = self.body.dofs[self.radiating_dof]
409
+
410
+ self.boundary_condition = -1j * self.encounter_omega * np.sum(dof * self.body.mesh.faces_normals, axis=1)
411
+
412
+ if self.forward_speed != 0.0:
413
+ if self.radiating_dof.lower() == "pitch":
414
+ ddofdx_dot_n = np.array([nz for (nx, ny, nz) in self.body.mesh.faces_normals])
415
+ elif self.radiating_dof.lower() == "yaw":
416
+ ddofdx_dot_n = np.array([-ny for (nx, ny, nz) in self.body.mesh.faces_normals])
417
+ elif self.radiating_dof.lower() in {"surge", "sway", "heave", "roll"}:
418
+ ddofdx_dot_n = 0.0
419
+ else:
420
+ raise NotImplementedError(
421
+ "Radiation problem with forward speed is currently only implemented for a single rigid body.\n"
422
+ "Only radiating dofs with name in {'Surge', 'Sway', 'Heave', 'Roll', 'Pitch', 'Yaw'} are supported.\n"
423
+ f"Got instead `radiating_dof={self.radiating_dof}`"
424
+ )
425
+ self.boundary_condition += self.forward_speed * ddofdx_dot_n
426
+
427
+ if self.body.lid_mesh is not None:
428
+ self.boundary_condition = np.concatenate([self.boundary_condition, np.zeros(self.body.lid_mesh.nb_faces)])
429
+
430
+
431
+ def _astuple(self):
432
+ return super()._astuple() + (self.radiating_dof,)
433
+
434
+ def _asdict(self):
435
+ d = super()._asdict()
436
+ d["radiating_dof"] = self.radiating_dof
437
+ return d
438
+
439
+ def _str_other_attributes(self):
440
+ if self.forward_speed != 0.0:
441
+ return [f"wave_direction={self.wave_direction:.3f}, radiating_dof=\'{self.radiating_dof}\'"]
442
+ else:
443
+ return [f"radiating_dof=\'{self.radiating_dof}\'"]
444
+
445
+ def _specific_rich_repr(self):
446
+ yield "radiating_dof", self.radiating_dof
447
+
448
+ def make_results_container(self, *args, **kwargs):
449
+ return RadiationResult(self, *args, **kwargs)
450
+
451
+
452
+ class LinearPotentialFlowResult:
453
+
454
+ def __init__(self, problem, forces=None, sources=None, potential=None, pressure=None):
455
+ self.problem = problem
456
+
457
+ self.forces = forces if forces is not None else {}
458
+ self.sources = sources
459
+ self.potential = potential
460
+ self.pressure = pressure
461
+
462
+ self.fs_elevation = {} # Only used in legacy `get_free_surface_elevation`. To be removed?
463
+
464
+ # Copy data from problem
465
+ self.body = self.problem.body
466
+ self.free_surface = self.problem.free_surface
467
+ self.omega = self.problem.omega
468
+ self.period = self.problem.period
469
+ self.wavenumber = self.problem.wavenumber
470
+ self.wavelength = self.problem.wavelength
471
+ self.forward_speed = self.problem.forward_speed
472
+ self.wave_direction = self.problem.wave_direction
473
+ self.encounter_omega = self.problem.encounter_omega
474
+ self.encounter_period = self.problem.encounter_period
475
+ self.encounter_wavenumber = self.problem.encounter_wavenumber
476
+ self.encounter_wavelength = self.problem.encounter_wavelength
477
+ self.encounter_wave_direction = self.problem.encounter_wave_direction
478
+ self.rho = self.problem.rho
479
+ self.g = self.problem.g
480
+ self.boundary_condition = self.problem.boundary_condition
481
+ self.water_depth = self.problem.water_depth
482
+ self.depth = self.problem.water_depth
483
+ self.provided_freq_type = self.problem.provided_freq_type
484
+ self.body_name = self.problem.body_name
485
+ self.influenced_dofs = self.problem.influenced_dofs
486
+
487
+ @property
488
+ def force(self):
489
+ # Just an alias
490
+ return self.forces
491
+
492
+ __str__ = LinearPotentialFlowProblem.__str__
493
+ __repr__ = LinearPotentialFlowProblem.__repr__
494
+ _repr_pretty_ = LinearPotentialFlowProblem._repr_pretty_
495
+ __rich_repr__ = LinearPotentialFlowProblem.__rich_repr__
496
+
497
+
498
+ class DiffractionResult(LinearPotentialFlowResult):
499
+
500
+ def __init__(self, problem, *args, **kwargs):
501
+ super().__init__(problem, *args, **kwargs)
502
+
503
+ _str_other_attributes = DiffractionProblem._str_other_attributes
504
+ _specific_rich_repr = DiffractionProblem._specific_rich_repr
505
+
506
+ @property
507
+ def records(self):
508
+ params = self.problem._asdict()
509
+ FK = froude_krylov_force(self.problem)
510
+ return [dict(**params,
511
+ influenced_dof=dof,
512
+ diffraction_force=self.forces[dof],
513
+ Froude_Krylov_force=FK[dof])
514
+ for dof in self.influenced_dofs]
515
+
516
+
517
+ class RadiationResult(LinearPotentialFlowResult):
518
+
519
+ def __init__(self, problem, *args, **kwargs):
520
+ super().__init__(problem, *args, **kwargs)
521
+ self.radiating_dof = self.problem.radiating_dof
522
+
523
+ _str_other_attributes = RadiationProblem._str_other_attributes
524
+ _specific_rich_repr = RadiationProblem._specific_rich_repr
525
+
526
+ @property
527
+ def added_mass(self):
528
+ return {dof: float(np.real(force)/(self.encounter_omega*self.encounter_omega)) for (dof, force) in self.forces.items()}
529
+
530
+ @property
531
+ def radiation_damping(self):
532
+ return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
533
+
534
+ # Aliases for backward compatibility
535
+ added_masses = added_mass
536
+ radiation_dampings = radiation_damping
537
+
538
+ @property
539
+ def records(self):
540
+ params = self.problem._asdict()
541
+ return [dict(params,
542
+ influenced_dof=dof,
543
+ added_mass=self.added_mass[dof],
544
+ radiation_damping=self.radiation_damping[dof])
545
+ for dof in self.influenced_dofs]