capytaine 2.3.1__cp314-cp314t-macosx_14_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 (92) 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 +36 -0
  6. capytaine/bem/__init__.py +0 -0
  7. capytaine/bem/airy_waves.py +111 -0
  8. capytaine/bem/engines.py +441 -0
  9. capytaine/bem/problems_and_results.py +600 -0
  10. capytaine/bem/solver.py +594 -0
  11. capytaine/bodies/__init__.py +4 -0
  12. capytaine/bodies/bodies.py +1221 -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 +111 -0
  17. capytaine/bodies/predefined/spheres.py +70 -0
  18. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  19. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  20. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  21. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  22. capytaine/green_functions/FinGreen3D/README.md +24 -0
  23. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  24. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  25. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  26. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  27. capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
  28. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  29. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  30. capytaine/green_functions/__init__.py +2 -0
  31. capytaine/green_functions/abstract_green_function.py +64 -0
  32. capytaine/green_functions/delhommeau.py +507 -0
  33. capytaine/green_functions/hams.py +204 -0
  34. capytaine/green_functions/libs/Delhommeau_float32.cpython-314t-darwin.so +0 -0
  35. capytaine/green_functions/libs/Delhommeau_float64.cpython-314t-darwin.so +0 -0
  36. capytaine/green_functions/libs/__init__.py +0 -0
  37. capytaine/io/__init__.py +0 -0
  38. capytaine/io/bemio.py +153 -0
  39. capytaine/io/legacy.py +328 -0
  40. capytaine/io/mesh_loaders.py +1086 -0
  41. capytaine/io/mesh_writers.py +692 -0
  42. capytaine/io/meshio.py +38 -0
  43. capytaine/io/wamit.py +479 -0
  44. capytaine/io/xarray.py +668 -0
  45. capytaine/matrices/__init__.py +16 -0
  46. capytaine/matrices/block.py +592 -0
  47. capytaine/matrices/block_toeplitz.py +325 -0
  48. capytaine/matrices/builders.py +89 -0
  49. capytaine/matrices/linear_solvers.py +232 -0
  50. capytaine/matrices/low_rank.py +395 -0
  51. capytaine/meshes/__init__.py +6 -0
  52. capytaine/meshes/clipper.py +465 -0
  53. capytaine/meshes/collections.py +342 -0
  54. capytaine/meshes/geometry.py +409 -0
  55. capytaine/meshes/mesh_like_protocol.py +37 -0
  56. capytaine/meshes/meshes.py +890 -0
  57. capytaine/meshes/predefined/__init__.py +6 -0
  58. capytaine/meshes/predefined/cylinders.py +314 -0
  59. capytaine/meshes/predefined/rectangles.py +261 -0
  60. capytaine/meshes/predefined/spheres.py +62 -0
  61. capytaine/meshes/properties.py +276 -0
  62. capytaine/meshes/quadratures.py +80 -0
  63. capytaine/meshes/quality.py +448 -0
  64. capytaine/meshes/surface_integrals.py +63 -0
  65. capytaine/meshes/symmetric.py +462 -0
  66. capytaine/post_pro/__init__.py +6 -0
  67. capytaine/post_pro/free_surfaces.py +88 -0
  68. capytaine/post_pro/impedance.py +92 -0
  69. capytaine/post_pro/kochin.py +54 -0
  70. capytaine/post_pro/rao.py +60 -0
  71. capytaine/tools/__init__.py +0 -0
  72. capytaine/tools/cache_on_disk.py +26 -0
  73. capytaine/tools/deprecation_handling.py +18 -0
  74. capytaine/tools/lists_of_points.py +52 -0
  75. capytaine/tools/lru_cache.py +49 -0
  76. capytaine/tools/optional_imports.py +27 -0
  77. capytaine/tools/prony_decomposition.py +150 -0
  78. capytaine/tools/symbolic_multiplication.py +149 -0
  79. capytaine/tools/timer.py +66 -0
  80. capytaine/ui/__init__.py +0 -0
  81. capytaine/ui/cli.py +28 -0
  82. capytaine/ui/rich.py +5 -0
  83. capytaine/ui/vtk/__init__.py +3 -0
  84. capytaine/ui/vtk/animation.py +329 -0
  85. capytaine/ui/vtk/body_viewer.py +28 -0
  86. capytaine/ui/vtk/helpers.py +82 -0
  87. capytaine/ui/vtk/mesh_viewer.py +461 -0
  88. capytaine-2.3.1.dist-info/LICENSE +674 -0
  89. capytaine-2.3.1.dist-info/METADATA +750 -0
  90. capytaine-2.3.1.dist-info/RECORD +92 -0
  91. capytaine-2.3.1.dist-info/WHEEL +6 -0
  92. capytaine-2.3.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,600 @@
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 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 ((isinstance(self.body.mesh, CollectionOfMeshes) and len(self.body.mesh) == 0)
199
+ or len(self.body.mesh.faces) == 0):
200
+ raise ValueError(f"The mesh of the body {self.body.__short_str__()} is empty.")
201
+
202
+ self.body._check_dofs_shape_consistency()
203
+
204
+ panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface + 1e-8
205
+ panels_below_sb = self.body.mesh.faces_centers[:, 2] <= -self.water_depth
206
+ if (any(panels_above_fs) or any(panels_below_sb)):
207
+
208
+ if not any(panels_below_sb):
209
+ issue = f"{np.count_nonzero(panels_above_fs)} panels above the free surface"
210
+ elif not any(panels_above_fs):
211
+ issue = f"{np.count_nonzero(panels_below_sb)} panels below the sea bottom"
212
+ else:
213
+ issue = (f"{np.count_nonzero(panels_above_fs)} panels above the free surface " +
214
+ f"and {np.count_nonzero(panels_below_sb)} panels below the sea bottom")
215
+
216
+ LOG.warning(
217
+ f"The mesh of the body {self.body.__short_str__()} has {issue}.\n" +
218
+ "It has been clipped to fit inside the domain.\n" +
219
+ "To remove this warning, clip the mesh manually with the `immersed_part()` method."
220
+ )
221
+
222
+ self.body = self.body.immersed_part(free_surface=self.free_surface,
223
+ water_depth=self.water_depth)
224
+
225
+ if self.boundary_condition is not None:
226
+ if len(self.boundary_condition.shape) != 1:
227
+ raise ValueError(f"Expected a 1-dimensional array as boundary_condition. Provided boundary condition's shape: {self.boundary_condition.shape}.")
228
+
229
+ if self.boundary_condition.shape[0] != self.body.mesh_including_lid.nb_faces:
230
+ raise ValueError(
231
+ f"The shape of the boundary condition ({self.boundary_condition.shape})"
232
+ f"does not match the number of faces of the mesh ({self.body.mesh.nb_faces})."
233
+ )
234
+
235
+ @property
236
+ def body_name(self):
237
+ return self.body.name if self.body is not None else 'None'
238
+
239
+ def _asdict(self):
240
+ return {"body_name": self.body_name,
241
+ "water_depth": self.water_depth,
242
+ "free_surface": self.free_surface,
243
+ "omega": float(self.omega),
244
+ "freq": float(self.freq),
245
+ "encounter_omega": float(self.encounter_omega),
246
+ "encounter_freq": float(self.encounter_freq),
247
+ "period": float(self.period),
248
+ "wavelength": float(self.wavelength),
249
+ "wavenumber": float(self.wavenumber),
250
+ "forward_speed": self.forward_speed,
251
+ "wave_direction": self.wave_direction,
252
+ "encounter_wave_direction": self.encounter_wave_direction,
253
+ "rho": self.rho,
254
+ "g": self.g}
255
+
256
+ @staticmethod
257
+ def _group_for_parallel_resolution(problems):
258
+ """Given a list of problems, returns a list of groups of problems, such
259
+ that each group should be executed in the same process to benefit from
260
+ caching.
261
+ """
262
+ problems_params = pd.DataFrame([pb._asdict() for pb in problems])
263
+ groups_of_indices = problems_params.groupby(["body_name", "water_depth", "omega", "rho", "g"]).groups.values()
264
+ groups_of_problems = [[problems[i] for i in grp] for grp in groups_of_indices]
265
+ return groups_of_problems
266
+
267
+ def __str__(self):
268
+ """Do not display default values in str(problem)."""
269
+ parameters = [f"body={self.body.__short_str__() if self.body is not None else None}",
270
+ f"{self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type)):.3f}",
271
+ f"water_depth={self.water_depth}"]
272
+
273
+ if not self.forward_speed == _default_parameters['forward_speed']:
274
+ parameters.append(f"forward_speed={self.forward_speed:.3f}")
275
+
276
+ try:
277
+ parameters.extend(self._str_other_attributes())
278
+ except AttributeError:
279
+ pass
280
+
281
+ if not self.free_surface == _default_parameters['free_surface']:
282
+ parameters.append(f"free_surface={self.free_surface}")
283
+ if not self.g == _default_parameters['g']:
284
+ parameters.append(f"g={self.g}")
285
+ if not self.rho == _default_parameters['rho']:
286
+ parameters.append(f"rho={self.rho}")
287
+
288
+ return self.__class__.__name__ + "(" + ', '.join(parameters) + ")"
289
+
290
+ def __repr__(self):
291
+ return self.__str__()
292
+
293
+ def _repr_pretty_(self, p, cycle):
294
+ p.text(self.__str__())
295
+
296
+ def __rich_repr__(self):
297
+ yield "body", self.body, None
298
+ yield self.provided_freq_type, self.__getattribute__(self.provided_freq_type)
299
+ yield "water_depth", self.water_depth, _default_parameters["water_depth"]
300
+ try:
301
+ yield from self._specific_rich_repr()
302
+ except:
303
+ pass
304
+ yield "g", self.g, _default_parameters["g"]
305
+ yield "rho", self.rho, _default_parameters["rho"]
306
+
307
+ def _astuple(self):
308
+ return (self.body, self.free_surface, self.water_depth,
309
+ float(self.omega), float(self.period), float(self.wavenumber), float(self.wavelength),
310
+ self.forward_speed, self.rho, self.g)
311
+
312
+ def __eq__(self, other):
313
+ if isinstance(other, LinearPotentialFlowProblem):
314
+ return self._astuple() == other._astuple()
315
+ else:
316
+ return NotImplemented
317
+
318
+ def __lt__(self, other):
319
+ # Arbitrary order. Used for ordering of problems: problems with same body are grouped together.
320
+ if isinstance(other, LinearPotentialFlowProblem):
321
+ return self._astuple()[:9] < other._astuple()[:9]
322
+ # Not the whole tuple, because when using inheriting classes,
323
+ # "radiating_dof" cannot be compared with "wave_direction"
324
+ else:
325
+ return NotImplemented
326
+
327
+ @property
328
+ def depth(self):
329
+ return self.water_depth
330
+
331
+ @property
332
+ def influenced_dofs(self):
333
+ # TODO: let the user choose the influenced dofs
334
+ return self.body.dofs if self.body is not None else set()
335
+
336
+ def make_results_container(self, *args, **kwargs):
337
+ return LinearPotentialFlowResult(self, *args, **kwargs)
338
+
339
+ def make_failed_results_container(self, *args, **kwargs):
340
+ return FailedLinearPotentialFlowResult(self, *args, **kwargs)
341
+
342
+
343
+ class DiffractionProblem(LinearPotentialFlowProblem):
344
+ """Particular LinearPotentialFlowProblem with boundary conditions
345
+ computed from an incoming Airy wave."""
346
+
347
+ def __init__(self, *,
348
+ body=None,
349
+ free_surface=_default_parameters['free_surface'],
350
+ water_depth=None, sea_bottom=None,
351
+ omega=None, freq=None, period=None, wavenumber=None, wavelength=None,
352
+ forward_speed=_default_parameters['forward_speed'],
353
+ rho=_default_parameters['rho'],
354
+ g=_default_parameters['g'],
355
+ wave_direction=_default_parameters['wave_direction']):
356
+
357
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
358
+ omega=omega, freq=freq, period=period, wavenumber=wavenumber, wavelength=wavelength, wave_direction=wave_direction,
359
+ forward_speed=forward_speed, rho=rho, g=g)
360
+
361
+ if self.body is not None:
362
+
363
+ self.boundary_condition = np.zeros(
364
+ shape=(self.body.mesh_including_lid.nb_faces,),
365
+ dtype=np.complex128
366
+ )
367
+
368
+ self.boundary_condition[self.body.hull_mask] = -(
369
+ airy_waves_velocity(self.body.mesh.faces_centers, self)
370
+ * self.body.mesh.faces_normals
371
+ ).sum(axis=1)
372
+ # Note that even with forward speed, this is computed based on the
373
+ # frequency and not the encounter frequency.
374
+
375
+ if len(self.body.dofs) == 0:
376
+ LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
377
+
378
+ def _str_other_attributes(self):
379
+ return [f"wave_direction={self.wave_direction:.3f}"]
380
+
381
+ def _specific_rich_repr(self):
382
+ yield "wave_direction", self.wave_direction, _default_parameters["wave_direction"]
383
+
384
+ def make_results_container(self, *args, **kwargs):
385
+ return DiffractionResult(self, *args, **kwargs)
386
+
387
+ def make_failed_results_container(self, *args, **kwargs):
388
+ return FailedDiffractionResult(self, *args, **kwargs)
389
+
390
+
391
+ class RadiationProblem(LinearPotentialFlowProblem):
392
+ """Particular LinearPotentialFlowProblem whose boundary conditions have
393
+ been computed from the degree of freedom of the body."""
394
+
395
+ def __init__(self, *, body=None,
396
+ free_surface=_default_parameters['free_surface'],
397
+ water_depth=None, sea_bottom=None,
398
+ omega=None, freq=None, period=None, wavenumber=None, wavelength=None,
399
+ forward_speed=_default_parameters['forward_speed'],
400
+ wave_direction=_default_parameters['wave_direction'],
401
+ rho=_default_parameters['rho'],
402
+ g=_default_parameters['g'],
403
+ radiating_dof=None):
404
+
405
+ self.radiating_dof = radiating_dof
406
+
407
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
408
+ omega=omega, freq=freq, period=period, wavenumber=wavenumber, wavelength=wavelength,
409
+ wave_direction=wave_direction, forward_speed=forward_speed, rho=rho, g=g)
410
+
411
+ if self.body is not None:
412
+
413
+ if len(self.body.dofs) == 0:
414
+ raise ValueError(f"Body {self.body.name} does not have any degrees of freedom.")
415
+
416
+ if self.radiating_dof is None:
417
+ self.radiating_dof = next(iter(self.body.dofs))
418
+
419
+ if self.radiating_dof not in self.body.dofs:
420
+ raise ValueError(f"In {self}:\n"
421
+ f"the radiating dof {repr(self.radiating_dof)} is not one of the degrees of freedom of the body.\n"
422
+ f"The dofs of the body are {list(self.body.dofs.keys())}")
423
+
424
+ dof = self.body.dofs[self.radiating_dof]
425
+
426
+ self.boundary_condition = self.encounter_omega * np.zeros(
427
+ shape=(self.body.mesh_including_lid.nb_faces,),
428
+ dtype=np.complex128
429
+ )
430
+ # The multiplication by encounter_omega is just a programming trick to ensure that boundary_condition
431
+ # is implemented with the correct type (for zero and infinite frequencies), it does not affect the value.
432
+ # Below the value is update on the hull. It remains zero on the lid.
433
+
434
+ displacement_on_face = np.sum(dof * self.body.mesh.faces_normals, axis=1) # This is a dot product on each face
435
+ self.boundary_condition[self.body.hull_mask] = -1j * self.encounter_omega * displacement_on_face
436
+
437
+ if self.forward_speed != 0.0:
438
+ if self.radiating_dof.lower() == "pitch":
439
+ ddofdx_dot_n = np.array([nz for (nx, ny, nz) in self.body.mesh.faces_normals])
440
+ elif self.radiating_dof.lower() == "yaw":
441
+ ddofdx_dot_n = np.array([-ny for (nx, ny, nz) in self.body.mesh.faces_normals])
442
+ elif self.radiating_dof.lower() in {"surge", "sway", "heave", "roll"}:
443
+ ddofdx_dot_n = 0.0
444
+ else:
445
+ raise NotImplementedError(
446
+ "Radiation problem with forward speed is currently only implemented for a single rigid body.\n"
447
+ "Only radiating dofs with name in {'Surge', 'Sway', 'Heave', 'Roll', 'Pitch', 'Yaw'} are supported.\n"
448
+ f"Got instead `radiating_dof={self.radiating_dof}`"
449
+ )
450
+
451
+ self.boundary_condition[self.body.hull_mask] += self.forward_speed * ddofdx_dot_n
452
+
453
+
454
+
455
+ def _astuple(self):
456
+ return super()._astuple() + (self.radiating_dof,)
457
+
458
+ def _asdict(self):
459
+ d = super()._asdict()
460
+ d["radiating_dof"] = self.radiating_dof
461
+ return d
462
+
463
+ def _str_other_attributes(self):
464
+ if self.forward_speed != 0.0:
465
+ return [f"wave_direction={self.wave_direction:.3f}, radiating_dof=\'{self.radiating_dof}\'"]
466
+ else:
467
+ return [f"radiating_dof=\'{self.radiating_dof}\'"]
468
+
469
+ def _specific_rich_repr(self):
470
+ yield "radiating_dof", self.radiating_dof
471
+
472
+ def make_results_container(self, *args, **kwargs):
473
+ return RadiationResult(self, *args, **kwargs)
474
+
475
+ def make_failed_results_container(self, *args, **kwargs):
476
+ return FailedRadiationResult(self, *args, **kwargs)
477
+
478
+
479
+ class LinearPotentialFlowResult:
480
+
481
+ def __init__(self, problem, forces=None, sources=None, potential=None, pressure=None):
482
+ self.problem = problem
483
+
484
+ self.forces = forces if forces is not None else {}
485
+ self.sources = sources
486
+ self.potential = potential
487
+ self.pressure = pressure
488
+
489
+ self.fs_elevation = {} # Only used in legacy `get_free_surface_elevation`. To be removed?
490
+
491
+ # Copy data from problem
492
+ self.body = self.problem.body
493
+ self.free_surface = self.problem.free_surface
494
+ self.omega = self.problem.omega
495
+ self.freq = self.problem.freq
496
+ self.period = self.problem.period
497
+ self.wavenumber = self.problem.wavenumber
498
+ self.wavelength = self.problem.wavelength
499
+ self.forward_speed = self.problem.forward_speed
500
+ self.wave_direction = self.problem.wave_direction
501
+ self.encounter_omega = self.problem.encounter_omega
502
+ self.encounter_freq = self.problem.encounter_freq
503
+ self.encounter_period = self.problem.encounter_period
504
+ self.encounter_wavenumber = self.problem.encounter_wavenumber
505
+ self.encounter_wavelength = self.problem.encounter_wavelength
506
+ self.encounter_wave_direction = self.problem.encounter_wave_direction
507
+ self.rho = self.problem.rho
508
+ self.g = self.problem.g
509
+ self.boundary_condition = self.problem.boundary_condition
510
+ self.water_depth = self.problem.water_depth
511
+ self.depth = self.problem.water_depth
512
+ self.provided_freq_type = self.problem.provided_freq_type
513
+ self.body_name = self.problem.body_name
514
+ self.influenced_dofs = self.problem.influenced_dofs
515
+
516
+ @property
517
+ def force(self):
518
+ # Just an alias
519
+ return self.forces
520
+
521
+ __str__ = LinearPotentialFlowProblem.__str__
522
+ __repr__ = LinearPotentialFlowProblem.__repr__
523
+ _repr_pretty_ = LinearPotentialFlowProblem._repr_pretty_
524
+ __rich_repr__ = LinearPotentialFlowProblem.__rich_repr__
525
+
526
+
527
+ class FailedLinearPotentialFlowResult(LinearPotentialFlowResult):
528
+ def __init__(self, problem, exception):
529
+ LinearPotentialFlowResult.__init__(self, problem)
530
+ self.forces = {dof: np.nan + 1j*np.nan for dof in self.influenced_dofs}
531
+ self.exception = exception
532
+
533
+
534
+ class DiffractionResult(LinearPotentialFlowResult):
535
+
536
+ def __init__(self, problem, *args, **kwargs):
537
+ super().__init__(problem, *args, **kwargs)
538
+
539
+ _str_other_attributes = DiffractionProblem._str_other_attributes
540
+ _specific_rich_repr = DiffractionProblem._specific_rich_repr
541
+
542
+ @property
543
+ def records(self):
544
+ params = self.problem._asdict()
545
+ FK = froude_krylov_force(self.problem)
546
+ return [dict(**params,
547
+ influenced_dof=dof,
548
+ diffraction_force=self.forces[dof],
549
+ Froude_Krylov_force=FK[dof],
550
+ kind="DiffractionResult")
551
+ for dof in self.influenced_dofs]
552
+
553
+
554
+ class FailedDiffractionResult(DiffractionResult):
555
+ def __init__(self, problem, exception):
556
+ DiffractionResult.__init__(self, problem)
557
+ self.forces = {dof: np.nan for dof in self.influenced_dofs}
558
+ self.exception = exception
559
+
560
+
561
+ class RadiationResult(LinearPotentialFlowResult):
562
+
563
+ def __init__(self, problem, *args, **kwargs):
564
+ super().__init__(problem, *args, **kwargs)
565
+ self.radiating_dof = self.problem.radiating_dof
566
+
567
+ _str_other_attributes = RadiationProblem._str_other_attributes
568
+ _specific_rich_repr = RadiationProblem._specific_rich_repr
569
+
570
+ @property
571
+ def added_mass(self):
572
+ return {dof: float(np.real(force)/(self.encounter_omega*self.encounter_omega)) for (dof, force) in self.forces.items()}
573
+
574
+ @property
575
+ def radiation_damping(self):
576
+ if float(self.encounter_omega) in {0.0, np.inf} and self.forward_speed == 0.0:
577
+ return {dof: 0.0 for dof in self.forces.keys()}
578
+ else:
579
+ return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
580
+
581
+ # Aliases for backward compatibility
582
+ added_masses = added_mass
583
+ radiation_dampings = radiation_damping
584
+
585
+ @property
586
+ def records(self):
587
+ params = self.problem._asdict()
588
+ return [dict(params,
589
+ influenced_dof=dof,
590
+ added_mass=self.added_mass[dof],
591
+ radiation_damping=self.radiation_damping[dof],
592
+ kind="RadiationResult")
593
+ for dof in self.influenced_dofs]
594
+
595
+
596
+ class FailedRadiationResult(RadiationResult):
597
+ def __init__(self, problem, exception):
598
+ RadiationResult.__init__(self, problem)
599
+ self.forces = {dof: np.nan + 1j*np.nan for dof in self.influenced_dofs}
600
+ self.exception = exception