capytaine 2.1__cp312-cp312-win_amd64.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 (85) hide show
  1. capytaine/__about__.py +16 -0
  2. capytaine/__init__.py +48 -0
  3. capytaine/bem/__init__.py +0 -0
  4. capytaine/bem/airy_waves.py +106 -0
  5. capytaine/bem/engines.py +441 -0
  6. capytaine/bem/problems_and_results.py +540 -0
  7. capytaine/bem/solver.py +463 -0
  8. capytaine/bodies/__init__.py +4 -0
  9. capytaine/bodies/bodies.py +1084 -0
  10. capytaine/bodies/dofs.py +19 -0
  11. capytaine/bodies/predefined/__init__.py +6 -0
  12. capytaine/bodies/predefined/cylinders.py +151 -0
  13. capytaine/bodies/predefined/rectangles.py +109 -0
  14. capytaine/bodies/predefined/spheres.py +70 -0
  15. capytaine/green_functions/__init__.py +2 -0
  16. capytaine/green_functions/abstract_green_function.py +12 -0
  17. capytaine/green_functions/delhommeau.py +380 -0
  18. capytaine/green_functions/libs/Delhommeau_float32.cp312-win_amd64.dll.a +0 -0
  19. capytaine/green_functions/libs/Delhommeau_float32.cp312-win_amd64.pyd +0 -0
  20. capytaine/green_functions/libs/Delhommeau_float64.cp312-win_amd64.dll.a +0 -0
  21. capytaine/green_functions/libs/Delhommeau_float64.cp312-win_amd64.pyd +0 -0
  22. capytaine/green_functions/libs/XieDelhommeau_float32.cp312-win_amd64.dll.a +0 -0
  23. capytaine/green_functions/libs/XieDelhommeau_float32.cp312-win_amd64.pyd +0 -0
  24. capytaine/green_functions/libs/XieDelhommeau_float64.cp312-win_amd64.dll.a +0 -0
  25. capytaine/green_functions/libs/XieDelhommeau_float64.cp312-win_amd64.pyd +0 -0
  26. capytaine/green_functions/libs/__init__.py +0 -0
  27. capytaine/io/__init__.py +0 -0
  28. capytaine/io/bemio.py +141 -0
  29. capytaine/io/legacy.py +328 -0
  30. capytaine/io/mesh_loaders.py +1061 -0
  31. capytaine/io/mesh_writers.py +692 -0
  32. capytaine/io/meshio.py +35 -0
  33. capytaine/io/xarray.py +514 -0
  34. capytaine/matrices/__init__.py +16 -0
  35. capytaine/matrices/block.py +590 -0
  36. capytaine/matrices/block_toeplitz.py +325 -0
  37. capytaine/matrices/builders.py +89 -0
  38. capytaine/matrices/linear_solvers.py +233 -0
  39. capytaine/matrices/low_rank.py +393 -0
  40. capytaine/meshes/__init__.py +6 -0
  41. capytaine/meshes/clipper.py +464 -0
  42. capytaine/meshes/collections.py +313 -0
  43. capytaine/meshes/geometry.py +409 -0
  44. capytaine/meshes/meshes.py +746 -0
  45. capytaine/meshes/predefined/__init__.py +6 -0
  46. capytaine/meshes/predefined/cylinders.py +314 -0
  47. capytaine/meshes/predefined/rectangles.py +261 -0
  48. capytaine/meshes/predefined/spheres.py +62 -0
  49. capytaine/meshes/properties.py +199 -0
  50. capytaine/meshes/quadratures.py +80 -0
  51. capytaine/meshes/quality.py +448 -0
  52. capytaine/meshes/surface_integrals.py +63 -0
  53. capytaine/meshes/symmetric.py +383 -0
  54. capytaine/post_pro/__init__.py +6 -0
  55. capytaine/post_pro/free_surfaces.py +88 -0
  56. capytaine/post_pro/impedance.py +92 -0
  57. capytaine/post_pro/kochin.py +54 -0
  58. capytaine/post_pro/rao.py +60 -0
  59. capytaine/tools/__init__.py +0 -0
  60. capytaine/tools/cache_on_disk.py +24 -0
  61. capytaine/tools/deprecation_handling.py +18 -0
  62. capytaine/tools/lists_of_points.py +52 -0
  63. capytaine/tools/lru_cache.py +49 -0
  64. capytaine/tools/optional_imports.py +27 -0
  65. capytaine/tools/prony_decomposition.py +94 -0
  66. capytaine/tools/symbolic_multiplication.py +104 -0
  67. capytaine/ui/__init__.py +0 -0
  68. capytaine/ui/cli.py +28 -0
  69. capytaine/ui/rich.py +5 -0
  70. capytaine/ui/vtk/__init__.py +3 -0
  71. capytaine/ui/vtk/animation.py +329 -0
  72. capytaine/ui/vtk/body_viewer.py +26 -0
  73. capytaine/ui/vtk/helpers.py +82 -0
  74. capytaine/ui/vtk/mesh_viewer.py +461 -0
  75. capytaine-2.1.dist-info/DELVEWHEEL +2 -0
  76. capytaine-2.1.dist-info/LICENSE +674 -0
  77. capytaine-2.1.dist-info/METADATA +756 -0
  78. capytaine-2.1.dist-info/RECORD +85 -0
  79. capytaine-2.1.dist-info/WHEEL +4 -0
  80. capytaine-2.1.dist-info/entry_points.txt +3 -0
  81. capytaine.libs/libgcc_s_seh-1.dll +0 -0
  82. capytaine.libs/libgfortran-5.dll +0 -0
  83. capytaine.libs/libgomp-1.dll +0 -0
  84. capytaine.libs/libquadmath-0.dll +0 -0
  85. capytaine.libs/libwinpthread-1.dll +0 -0
@@ -0,0 +1,540 @@
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
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.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}={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(f"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 len(self.body.dofs) == 0:
360
+ LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
361
+
362
+ def _str_other_attributes(self):
363
+ return [f"wave_direction={self.wave_direction:.3f}"]
364
+
365
+ def _specific_rich_repr(self):
366
+ yield "wave_direction", self.wave_direction, _default_parameters["wave_direction"]
367
+
368
+ def make_results_container(self, *args, **kwargs):
369
+ return DiffractionResult(self, *args, **kwargs)
370
+
371
+
372
+ class RadiationProblem(LinearPotentialFlowProblem):
373
+ """Particular LinearPotentialFlowProblem whose boundary conditions have
374
+ been computed from the degree of freedom of the body."""
375
+
376
+ def __init__(self, *, body=None,
377
+ free_surface=_default_parameters['free_surface'],
378
+ water_depth=None, sea_bottom=None,
379
+ omega=None, period=None, wavenumber=None, wavelength=None,
380
+ forward_speed=_default_parameters['forward_speed'],
381
+ wave_direction=_default_parameters['wave_direction'],
382
+ rho=_default_parameters['rho'],
383
+ g=_default_parameters['g'],
384
+ radiating_dof=None):
385
+
386
+ self.radiating_dof = radiating_dof
387
+
388
+ super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
389
+ omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength,
390
+ wave_direction=wave_direction, forward_speed=forward_speed, rho=rho, g=g)
391
+
392
+ if self.body is not None:
393
+
394
+ if len(self.body.dofs) == 0:
395
+ raise ValueError(f"Body {self.body.name} does not have any degrees of freedom.")
396
+
397
+ if self.radiating_dof is None:
398
+ self.radiating_dof = next(iter(self.body.dofs))
399
+
400
+ if self.radiating_dof not in self.body.dofs:
401
+ LOG.error(f"In {self}: the radiating degree of freedom {self.radiating_dof} is not one of"
402
+ f"the degrees of freedom of the body.\n"
403
+ f"The dofs of the body are {list(self.body.dofs.keys())}")
404
+ raise ValueError("Unrecognized degree of freedom name.")
405
+
406
+ dof = self.body.dofs[self.radiating_dof]
407
+
408
+ self.boundary_condition = -1j * self.encounter_omega * np.sum(dof * self.body.mesh.faces_normals, axis=1)
409
+
410
+ if self.forward_speed != 0.0:
411
+ if self.radiating_dof.lower() == "pitch":
412
+ ddofdx_dot_n = np.array([nz for (nx, ny, nz) in self.body.mesh.faces_normals])
413
+ elif self.radiating_dof.lower() == "yaw":
414
+ ddofdx_dot_n = np.array([-ny for (nx, ny, nz) in self.body.mesh.faces_normals])
415
+ elif self.radiating_dof.lower() in {"surge", "sway", "heave", "roll"}:
416
+ ddofdx_dot_n = 0.0
417
+ else:
418
+ raise NotImplementedError(
419
+ "Radiation problem with forward speed is currently only implemented for a single rigid body.\n"
420
+ "Only radiating dofs with name in {'Surge', 'Sway', 'Heave', 'Roll', 'Pitch', 'Yaw'} are supported.\n"
421
+ f"Got instead `radiating_dof={self.radiating_dof}`"
422
+ )
423
+ self.boundary_condition += self.forward_speed * ddofdx_dot_n
424
+
425
+
426
+ def _astuple(self):
427
+ return super()._astuple() + (self.radiating_dof,)
428
+
429
+ def _asdict(self):
430
+ d = super()._asdict()
431
+ d["radiating_dof"] = self.radiating_dof
432
+ return d
433
+
434
+ def _str_other_attributes(self):
435
+ if self.forward_speed != 0.0:
436
+ return [f"wave_direction={self.wave_direction:.3f}, radiating_dof=\'{self.radiating_dof}\'"]
437
+ else:
438
+ return [f"radiating_dof=\'{self.radiating_dof}\'"]
439
+
440
+ def _specific_rich_repr(self):
441
+ yield "radiating_dof", self.radiating_dof
442
+
443
+ def make_results_container(self, *args, **kwargs):
444
+ return RadiationResult(self, *args, **kwargs)
445
+
446
+
447
+ class LinearPotentialFlowResult:
448
+
449
+ def __init__(self, problem, forces=None, sources=None, potential=None, pressure=None):
450
+ self.problem = problem
451
+
452
+ self.forces = forces if forces is not None else {}
453
+ self.sources = sources
454
+ self.potential = potential
455
+ self.pressure = pressure
456
+
457
+ self.fs_elevation = {} # Only used in legacy `get_free_surface_elevation`. To be removed?
458
+
459
+ # Copy data from problem
460
+ self.body = self.problem.body
461
+ self.free_surface = self.problem.free_surface
462
+ self.omega = self.problem.omega
463
+ self.period = self.problem.period
464
+ self.wavenumber = self.problem.wavenumber
465
+ self.wavelength = self.problem.wavelength
466
+ self.forward_speed = self.problem.forward_speed
467
+ self.wave_direction = self.problem.wave_direction
468
+ self.encounter_omega = self.problem.encounter_omega
469
+ self.encounter_period = self.problem.encounter_period
470
+ self.encounter_wavenumber = self.problem.encounter_wavenumber
471
+ self.encounter_wavelength = self.problem.encounter_wavelength
472
+ self.encounter_wave_direction = self.problem.encounter_wave_direction
473
+ self.rho = self.problem.rho
474
+ self.g = self.problem.g
475
+ self.boundary_condition = self.problem.boundary_condition
476
+ self.water_depth = self.problem.water_depth
477
+ self.depth = self.problem.water_depth
478
+ self.provided_freq_type = self.problem.provided_freq_type
479
+ self.body_name = self.problem.body_name
480
+ self.influenced_dofs = self.problem.influenced_dofs
481
+
482
+ @property
483
+ def force(self):
484
+ # Just an alias
485
+ return self.forces
486
+
487
+ __str__ = LinearPotentialFlowProblem.__str__
488
+ __repr__ = LinearPotentialFlowProblem.__repr__
489
+ _repr_pretty_ = LinearPotentialFlowProblem._repr_pretty_
490
+ __rich_repr__ = LinearPotentialFlowProblem.__rich_repr__
491
+
492
+
493
+ class DiffractionResult(LinearPotentialFlowResult):
494
+
495
+ def __init__(self, problem, *args, **kwargs):
496
+ super().__init__(problem, *args, **kwargs)
497
+
498
+ _str_other_attributes = DiffractionProblem._str_other_attributes
499
+ _specific_rich_repr = DiffractionProblem._specific_rich_repr
500
+
501
+ @property
502
+ def records(self):
503
+ params = self.problem._asdict()
504
+ FK = froude_krylov_force(self.problem)
505
+ return [dict(**params,
506
+ influenced_dof=dof,
507
+ diffraction_force=self.forces[dof],
508
+ Froude_Krylov_force=FK[dof])
509
+ for dof in self.influenced_dofs]
510
+
511
+
512
+ class RadiationResult(LinearPotentialFlowResult):
513
+
514
+ def __init__(self, problem, *args, **kwargs):
515
+ super().__init__(problem, *args, **kwargs)
516
+ self.radiating_dof = self.problem.radiating_dof
517
+
518
+ _str_other_attributes = RadiationProblem._str_other_attributes
519
+ _specific_rich_repr = RadiationProblem._specific_rich_repr
520
+
521
+ @property
522
+ def added_mass(self):
523
+ return {dof: float(np.real(force)/(self.encounter_omega*self.encounter_omega)) for (dof, force) in self.forces.items()}
524
+
525
+ @property
526
+ def radiation_damping(self):
527
+ return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
528
+
529
+ # Aliases for backward compatibility
530
+ added_masses = added_mass
531
+ radiation_dampings = radiation_damping
532
+
533
+ @property
534
+ def records(self):
535
+ params = self.problem._asdict()
536
+ return [dict(params,
537
+ influenced_dof=dof,
538
+ added_mass=self.added_mass[dof],
539
+ radiation_damping=self.radiation_damping[dof])
540
+ for dof in self.influenced_dofs]