capytaine 2.3.1__cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_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.
- capytaine/__about__.py +16 -0
- capytaine/__init__.py +36 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +441 -0
- capytaine/bem/problems_and_results.py +600 -0
- capytaine/bem/solver.py +594 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +1221 -0
- capytaine/bodies/dofs.py +19 -0
- capytaine/bodies/predefined/__init__.py +6 -0
- capytaine/bodies/predefined/cylinders.py +151 -0
- capytaine/bodies/predefined/rectangles.py +111 -0
- capytaine/bodies/predefined/spheres.py +70 -0
- capytaine/green_functions/FinGreen3D/.gitignore +1 -0
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
- capytaine/green_functions/FinGreen3D/LICENSE +165 -0
- capytaine/green_functions/FinGreen3D/Makefile +16 -0
- capytaine/green_functions/FinGreen3D/README.md +24 -0
- capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
- capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
- capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
- capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
- capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +507 -0
- capytaine/green_functions/hams.py +204 -0
- capytaine/green_functions/libs/Delhommeau_float32.cpython-38-x86_64-linux-gnu.so +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cpython-38-x86_64-linux-gnu.so +0 -0
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +328 -0
- capytaine/io/mesh_loaders.py +1086 -0
- capytaine/io/mesh_writers.py +692 -0
- capytaine/io/meshio.py +38 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +668 -0
- capytaine/matrices/__init__.py +16 -0
- capytaine/matrices/block.py +592 -0
- capytaine/matrices/block_toeplitz.py +325 -0
- capytaine/matrices/builders.py +89 -0
- capytaine/matrices/linear_solvers.py +232 -0
- capytaine/matrices/low_rank.py +395 -0
- capytaine/meshes/__init__.py +6 -0
- capytaine/meshes/clipper.py +465 -0
- capytaine/meshes/collections.py +342 -0
- capytaine/meshes/geometry.py +409 -0
- capytaine/meshes/mesh_like_protocol.py +37 -0
- capytaine/meshes/meshes.py +890 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +314 -0
- capytaine/meshes/predefined/rectangles.py +261 -0
- capytaine/meshes/predefined/spheres.py +62 -0
- capytaine/meshes/properties.py +276 -0
- capytaine/meshes/quadratures.py +80 -0
- capytaine/meshes/quality.py +448 -0
- capytaine/meshes/surface_integrals.py +63 -0
- capytaine/meshes/symmetric.py +462 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +88 -0
- capytaine/post_pro/impedance.py +92 -0
- capytaine/post_pro/kochin.py +54 -0
- capytaine/post_pro/rao.py +60 -0
- capytaine/tools/__init__.py +0 -0
- capytaine/tools/cache_on_disk.py +26 -0
- capytaine/tools/deprecation_handling.py +18 -0
- capytaine/tools/lists_of_points.py +52 -0
- capytaine/tools/lru_cache.py +49 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +149 -0
- capytaine/tools/timer.py +66 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine/ui/vtk/__init__.py +3 -0
- capytaine/ui/vtk/animation.py +329 -0
- capytaine/ui/vtk/body_viewer.py +28 -0
- capytaine/ui/vtk/helpers.py +82 -0
- capytaine/ui/vtk/mesh_viewer.py +461 -0
- capytaine-2.3.1.dist-info/LICENSE +674 -0
- capytaine-2.3.1.dist-info/METADATA +750 -0
- capytaine-2.3.1.dist-info/RECORD +93 -0
- capytaine-2.3.1.dist-info/WHEEL +6 -0
- capytaine-2.3.1.dist-info/entry_points.txt +3 -0
- capytaine.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
- capytaine.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
- capytaine.libs/libmvec-2-583a17db.28.so +0 -0
- capytaine.libs/libquadmath-2284e583.so.0.0.0 +0 -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
|