capytaine 3.0.0a1__cp38-cp38-macosx_15_0_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
- capytaine/.dylibs/libgfortran.5.dylib +0 -0
- capytaine/.dylibs/libquadmath.0.dylib +0 -0
- capytaine/__about__.py +21 -0
- capytaine/__init__.py +32 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +321 -0
- capytaine/bem/problems_and_results.py +601 -0
- capytaine/bem/solver.py +718 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +630 -0
- capytaine/bodies/dofs.py +146 -0
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/Delhommeau_float32.cpython-38-darwin.so +0 -0
- capytaine/green_functions/Delhommeau_float64.cpython-38-darwin.so +0 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +522 -0
- capytaine/green_functions/hams.py +210 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +228 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +673 -0
- capytaine/meshes/__init__.py +2 -0
- capytaine/meshes/abstract_meshes.py +375 -0
- capytaine/meshes/clean.py +302 -0
- capytaine/meshes/clip.py +347 -0
- capytaine/meshes/export.py +89 -0
- capytaine/meshes/geometry.py +259 -0
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +826 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +280 -0
- capytaine/meshes/predefined/rectangles.py +202 -0
- capytaine/meshes/predefined/spheres.py +55 -0
- capytaine/meshes/quality.py +159 -0
- capytaine/meshes/surface_integrals.py +82 -0
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +85 -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/block_circulant_matrices.py +275 -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/memory_monitor.py +45 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +161 -0
- capytaine/tools/timer.py +90 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine-3.0.0a1.dist-info/LICENSE +674 -0
- capytaine-3.0.0a1.dist-info/METADATA +755 -0
- capytaine-3.0.0a1.dist-info/RECORD +65 -0
- capytaine-3.0.0a1.dist-info/WHEEL +4 -0
- capytaine-3.0.0a1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""Floating bodies to be used in radiation-diffraction problems."""
|
|
2
|
+
# Copyright (C) 2017-2024 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import copy
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import xarray as xr
|
|
13
|
+
|
|
14
|
+
from capytaine.meshes.abstract_meshes import AbstractMesh
|
|
15
|
+
from capytaine.meshes.meshes import Mesh
|
|
16
|
+
from capytaine.meshes.geometry import connected_components, connected_components_of_waterline
|
|
17
|
+
from capytaine.bodies.dofs import (
|
|
18
|
+
AbstractDof,
|
|
19
|
+
TranslationDof,
|
|
20
|
+
RotationDof,
|
|
21
|
+
DofOnSubmesh,
|
|
22
|
+
normalize_name,
|
|
23
|
+
rigid_body_dofs,
|
|
24
|
+
add_dofs_labels_to_vector,
|
|
25
|
+
add_dofs_labels_to_matrix
|
|
26
|
+
)
|
|
27
|
+
from capytaine.bodies.hydrostatics import _HydrostaticsMixin
|
|
28
|
+
|
|
29
|
+
LOG = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FloatingBody(_HydrostaticsMixin):
|
|
33
|
+
"""A floating body described as a mesh and some degrees of freedom.
|
|
34
|
+
|
|
35
|
+
The mesh structure is stored as a Mesh from capytaine.mesh.mesh or a
|
|
36
|
+
CollectionOfMeshes from capytaine.mesh.meshes_collection.
|
|
37
|
+
|
|
38
|
+
The degrees of freedom (dofs) are stored as a dict associating a name to
|
|
39
|
+
a complex-valued array of shape (nb_faces, 3). To each face of the body
|
|
40
|
+
(as indexed in the mesh) corresponds a complex-valued 3d vector, which
|
|
41
|
+
defines the displacement of the center of the face in frequency domain.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
mesh : AbstractMesh, optional
|
|
46
|
+
the mesh describing the geometry of the hull of the floating body.
|
|
47
|
+
If none is given, a empty one is created.
|
|
48
|
+
dofs : dict, optional
|
|
49
|
+
the degrees of freedom of the body.
|
|
50
|
+
If none is given, a empty dictionary is initialized.
|
|
51
|
+
lid_mesh : AbstractMesh or None, optional
|
|
52
|
+
a mesh of an internal lid for irregular frequencies removal.
|
|
53
|
+
Unlike the mesh of the hull, no dof is defined on the lid_mesh.
|
|
54
|
+
If none is given, none is used when solving the Boundary Integral Equation.
|
|
55
|
+
center_of_mass: 3-element array, optional
|
|
56
|
+
the position of the center of mass.
|
|
57
|
+
Required only for some hydrostatics computation.
|
|
58
|
+
mass : float or None, optional
|
|
59
|
+
the mass of the body in kilograms.
|
|
60
|
+
Required only for some hydrostatics computation.
|
|
61
|
+
If None, the mass is implicitly assumed to be the mass of displaced water.
|
|
62
|
+
name : str, optional
|
|
63
|
+
a name for the body.
|
|
64
|
+
If none is given, the one of the mesh is used.
|
|
65
|
+
|
|
66
|
+
Attributes
|
|
67
|
+
----------
|
|
68
|
+
mesh_including_lid: AbstractMesh
|
|
69
|
+
The hull mesh joined with the lid mesh
|
|
70
|
+
hull_mask: np.array
|
|
71
|
+
The indices of the faces in mesh_including_lid that are part of the hull
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, mesh=None, dofs=None, *, lid_mesh=None, center_of_mass=None, mass=None, name=None):
|
|
75
|
+
if mesh is None:
|
|
76
|
+
self.mesh = Mesh(name="dummy_mesh")
|
|
77
|
+
elif isinstance(mesh, AbstractMesh):
|
|
78
|
+
self.mesh = mesh
|
|
79
|
+
else:
|
|
80
|
+
raise TypeError("Unrecognized `mesh` object passed to the FloatingBody constructor.")
|
|
81
|
+
|
|
82
|
+
if lid_mesh is None:
|
|
83
|
+
self.lid_mesh = None
|
|
84
|
+
elif isinstance(mesh, AbstractMesh):
|
|
85
|
+
if lid_mesh.nb_faces == 0:
|
|
86
|
+
LOG.warning("Lid mesh %s provided for body initialization is empty. The lid mesh is ignored.", lid_mesh)
|
|
87
|
+
self.lid_mesh = None
|
|
88
|
+
else:
|
|
89
|
+
self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
|
|
90
|
+
else:
|
|
91
|
+
raise TypeError("Unrecognized `lid_mesh` object passed to the FloatingBody constructor.")
|
|
92
|
+
|
|
93
|
+
if self.lid_mesh is None:
|
|
94
|
+
self.mesh_including_lid = self.mesh
|
|
95
|
+
self.hull_mask = np.full((self.mesh.nb_faces,), True)
|
|
96
|
+
else:
|
|
97
|
+
self.mesh_including_lid, masks = self.mesh.join_meshes(self.lid_mesh, return_masks=True)
|
|
98
|
+
self.hull_mask = masks[0]
|
|
99
|
+
|
|
100
|
+
if name is None and mesh is None:
|
|
101
|
+
self.name = "dummy_body"
|
|
102
|
+
elif name is None:
|
|
103
|
+
if hasattr(self.mesh, "name") and self.mesh.name is not None:
|
|
104
|
+
self.name = self.mesh.name
|
|
105
|
+
else:
|
|
106
|
+
self.name = "anonymous_body"
|
|
107
|
+
else:
|
|
108
|
+
self.name = name
|
|
109
|
+
|
|
110
|
+
self.mass = mass
|
|
111
|
+
if center_of_mass is not None:
|
|
112
|
+
self.center_of_mass = np.asarray(center_of_mass, dtype=float)
|
|
113
|
+
else:
|
|
114
|
+
self.center_of_mass = None
|
|
115
|
+
|
|
116
|
+
if dofs is None:
|
|
117
|
+
self.dofs = {}
|
|
118
|
+
else:
|
|
119
|
+
self.dofs = {
|
|
120
|
+
k: v if isinstance(v, AbstractDof) else np.asarray(v)
|
|
121
|
+
for k, v in dofs.items()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
self._check_dofs_shape_consistency()
|
|
125
|
+
|
|
126
|
+
LOG.debug(f"New floating body: {self.__str__()}.")
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def from_meshio(mesh, name=None) -> 'FloatingBody':
|
|
130
|
+
"""Create a FloatingBody from a meshio mesh object.
|
|
131
|
+
Kinda deprecated, use cpt.load_mesh instead."""
|
|
132
|
+
LOG.warning("Deprecation warning: The method FloatingBody.from_meshio(...) is deprecated. "
|
|
133
|
+
"Please prefer FloatingBody(mesh=cpt.load_mesh(...), ...)")
|
|
134
|
+
from capytaine.io.meshio import load_from_meshio
|
|
135
|
+
return FloatingBody(mesh=load_from_meshio(mesh, name), name=name)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def from_file(filename: str, file_format=None, name=None) -> 'FloatingBody':
|
|
139
|
+
"""Create a FloatingBody from a mesh file using meshmagick.
|
|
140
|
+
Kinda deprecated, use cpt.load_mesh instead."""
|
|
141
|
+
LOG.warning("Deprecation warning: The method FloatingBody.from_file(...) is deprecated. "
|
|
142
|
+
"Please prefer FloatingBody(mesh=cpt.load_mesh(...), ...)")
|
|
143
|
+
from capytaine.io.mesh_loaders import load_mesh
|
|
144
|
+
if name is None:
|
|
145
|
+
name = filename
|
|
146
|
+
mesh = load_mesh(filename, file_format, name=f"{name}_mesh")
|
|
147
|
+
return FloatingBody(mesh, name=name)
|
|
148
|
+
|
|
149
|
+
def __lt__(self, other: 'FloatingBody') -> bool:
|
|
150
|
+
"""Arbitrary order. The point is to sort together the problems involving the same body."""
|
|
151
|
+
return self.name < other.name
|
|
152
|
+
|
|
153
|
+
##########
|
|
154
|
+
# Dofs #
|
|
155
|
+
##########
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def nb_dofs(self) -> int:
|
|
159
|
+
"""Number of degrees of freedom."""
|
|
160
|
+
return len(self.dofs)
|
|
161
|
+
|
|
162
|
+
def add_translation_dof(self, direction=None, name=None) -> None:
|
|
163
|
+
"""Add a new translation dof (in place).
|
|
164
|
+
If no direction is given, the code tries to infer it from the name.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
direction : array of shape (3,), optional
|
|
169
|
+
the direction of the translation
|
|
170
|
+
name : str, optional
|
|
171
|
+
a name for the degree of freedom
|
|
172
|
+
"""
|
|
173
|
+
if name is None:
|
|
174
|
+
name = f"dof_{self.nb_dofs}_translation"
|
|
175
|
+
if direction is None and normalize_name(name) in {"Surge", "Sway", "Heave"}:
|
|
176
|
+
self.dofs[name] = rigid_body_dofs()[normalize_name(name)]
|
|
177
|
+
else:
|
|
178
|
+
self.dofs[name] = TranslationDof(
|
|
179
|
+
direction=direction,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def add_rotation_dof(self, rotation_center=None, direction=None, name=None) -> None:
|
|
183
|
+
"""Add a new rotation dof (in place).
|
|
184
|
+
If no axis is given, the code tries to infer it from the name.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
rotation_center: array of shape (3,), optional
|
|
189
|
+
One point on the rotation axis
|
|
190
|
+
direction: array of shape (3,), optional
|
|
191
|
+
The direction of the rotation axis
|
|
192
|
+
name : str, optional
|
|
193
|
+
a name for the degree of freedom
|
|
194
|
+
"""
|
|
195
|
+
if name is None:
|
|
196
|
+
name = f"dof_{self.nb_dofs}_rotation"
|
|
197
|
+
if rotation_center is None:
|
|
198
|
+
for point_attr in ('rotation_center', 'center_of_mass'):
|
|
199
|
+
if hasattr(self, point_attr) and getattr(self, point_attr) is not None:
|
|
200
|
+
rotation_center = getattr(self, point_attr)
|
|
201
|
+
LOG.info(f"The rotation dof {name} has been initialized around the point: "
|
|
202
|
+
f"{self.__short_str__()}.{point_attr} = {getattr(self, point_attr)}")
|
|
203
|
+
break
|
|
204
|
+
if direction is None and normalize_name(name) in {"Roll", "Pitch", "Yaw"}:
|
|
205
|
+
self.dofs[name] = rigid_body_dofs(rotation_center=rotation_center)[normalize_name(name)]
|
|
206
|
+
else:
|
|
207
|
+
self.dofs[name] = RotationDof(
|
|
208
|
+
rotation_center=rotation_center,
|
|
209
|
+
direction=direction,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def add_all_rigid_body_dofs(self, rotation_center=None) -> None:
|
|
213
|
+
"""Add the six degrees of freedom of rigid bodies (in place)."""
|
|
214
|
+
self.add_translation_dof(name="Surge")
|
|
215
|
+
self.add_translation_dof(name="Sway")
|
|
216
|
+
self.add_translation_dof(name="Heave")
|
|
217
|
+
self.add_rotation_dof(rotation_center=rotation_center, name="Roll")
|
|
218
|
+
self.add_rotation_dof(rotation_center=rotation_center, name="Pitch")
|
|
219
|
+
self.add_rotation_dof(rotation_center=rotation_center, name="Yaw")
|
|
220
|
+
|
|
221
|
+
def integrate_pressure(self, pressure):
|
|
222
|
+
forces = {}
|
|
223
|
+
for dof_name in self.dofs:
|
|
224
|
+
if isinstance(self.dofs[dof_name], AbstractDof):
|
|
225
|
+
dof = self.dofs[dof_name].evaluate_motion(self.mesh)
|
|
226
|
+
else:
|
|
227
|
+
dof = self.dofs[dof_name]
|
|
228
|
+
# Scalar product on each face:
|
|
229
|
+
normal_dof_amplitude_on_face = - np.sum(dof * self.mesh.faces_normals, axis=1)
|
|
230
|
+
# The minus sign in the above line is because we want the force of the fluid on the body and not the force of the body on the fluid.
|
|
231
|
+
# Sum over all faces:
|
|
232
|
+
forces[dof_name] = np.sum(pressure * normal_dof_amplitude_on_face * self.mesh.faces_areas)
|
|
233
|
+
return forces
|
|
234
|
+
|
|
235
|
+
def keep_only_dofs(self, *args, **kwargs):
|
|
236
|
+
raise NotImplementedError("`keep_only_dofs` has been removed. Consider using `body = body.with_only_dofs(['dof_name'])` instead.")
|
|
237
|
+
|
|
238
|
+
def with_only_dofs(self, dofs):
|
|
239
|
+
body = FloatingBody(mesh=self.mesh,
|
|
240
|
+
lid_mesh=self.lid_mesh,
|
|
241
|
+
dofs={k: v for k, v in self.dofs.items() if k in dofs},
|
|
242
|
+
mass=self.mass,
|
|
243
|
+
center_of_mass=self.center_of_mass,
|
|
244
|
+
name=self.name)
|
|
245
|
+
|
|
246
|
+
if hasattr(self, 'inertia_matrix'):
|
|
247
|
+
body.inertia_matrix = self.inertia_matrix.sel(radiating_dof=dofs, influenced_dof=dofs)
|
|
248
|
+
if hasattr(self, 'hydrostatic_stiffness'):
|
|
249
|
+
body.hydrostatic_stiffness = self.hydrostatic_stiffness.sel(radiating_dof=dofs, influenced_dof=dofs)
|
|
250
|
+
|
|
251
|
+
return body
|
|
252
|
+
|
|
253
|
+
def add_dofs_labels_to_vector(self, vector):
|
|
254
|
+
"""Helper function turning a bare vector into a vector labelled by the name of the dofs of the body,
|
|
255
|
+
to be used for instance for the computation of RAO."""
|
|
256
|
+
return add_dofs_labels_to_vector(self.dofs.keys(), vector)
|
|
257
|
+
|
|
258
|
+
def add_dofs_labels_to_matrix(self, matrix):
|
|
259
|
+
"""Helper function turning a bare matrix into a matrix labelled by the name of the dofs of the body,
|
|
260
|
+
to be used for instance for the computation of RAO."""
|
|
261
|
+
return add_dofs_labels_to_matrix(self.dofs.keys(), matrix)
|
|
262
|
+
|
|
263
|
+
def _check_dofs_shape_consistency(self):
|
|
264
|
+
for dof_name, dof in self.dofs.items():
|
|
265
|
+
if (not isinstance(dof, AbstractDof) and
|
|
266
|
+
(np.array(dof).shape != (self.mesh.nb_faces, 3))):
|
|
267
|
+
raise ValueError(f"The array defining the dof {dof_name} of {self.__short_str__()} does not have the expected shape.\n"
|
|
268
|
+
f"Expected shape: ({self.mesh.nb_faces}, 3)\n"
|
|
269
|
+
f" Actual shape: {dof.shape}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
###################
|
|
273
|
+
# Transformations #
|
|
274
|
+
###################
|
|
275
|
+
|
|
276
|
+
def __add__(self, body_to_add: 'FloatingBody'):
|
|
277
|
+
return self.join_bodies(body_to_add)
|
|
278
|
+
|
|
279
|
+
def join_bodies(*bodies, name=None):
|
|
280
|
+
from capytaine.bodies.multibodies import Multibody
|
|
281
|
+
return Multibody(bodies, name=name)
|
|
282
|
+
|
|
283
|
+
def copy(self, name=None) -> 'FloatingBody':
|
|
284
|
+
"""Return a deep copy of the body.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
name : str, optional
|
|
289
|
+
a name for the new copy
|
|
290
|
+
"""
|
|
291
|
+
self._check_dofs_shape_consistency()
|
|
292
|
+
|
|
293
|
+
new_body = copy.deepcopy(self)
|
|
294
|
+
if name is None:
|
|
295
|
+
new_body.name = f"copy_of_{self.name}"
|
|
296
|
+
LOG.debug(f"Copy {self.name}.")
|
|
297
|
+
else:
|
|
298
|
+
new_body.name = name
|
|
299
|
+
LOG.debug(f"Copy {self.name} under the name {name}.")
|
|
300
|
+
return new_body
|
|
301
|
+
|
|
302
|
+
def assemble_regular_array(self, distance, nb_bodies):
|
|
303
|
+
"""Create an regular array of identical bodies.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
distance : float
|
|
308
|
+
Center-to-center distance between objects in the array
|
|
309
|
+
nb_bodies : couple of ints
|
|
310
|
+
Number of objects in the x and y directions.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
FloatingBody
|
|
315
|
+
"""
|
|
316
|
+
bodies = (self.translated((i*distance, j*distance, 0), name=f"{i}_{j}") for j in range(nb_bodies[1]) for i in range(nb_bodies[0]))
|
|
317
|
+
array = FloatingBody.join_bodies(*bodies)
|
|
318
|
+
array.name = f"array_of_{self.name}"
|
|
319
|
+
return array
|
|
320
|
+
|
|
321
|
+
def assemble_arbitrary_array(self, locations:np.ndarray):
|
|
322
|
+
if not isinstance(locations, np.ndarray):
|
|
323
|
+
raise TypeError('locations must be of type np.ndarray')
|
|
324
|
+
assert locations.shape[1] == 2, 'locations must be of shape nx2, received {:}'.format(locations.shape)
|
|
325
|
+
|
|
326
|
+
fb_list = []
|
|
327
|
+
for idx, li in enumerate(locations):
|
|
328
|
+
fb_list.append(self.translated(np.append(li, 0), name='arbitrary_array_body{:02d}'.format(idx)))
|
|
329
|
+
arbitrary_array = FloatingBody.join_bodies(*fb_list)
|
|
330
|
+
|
|
331
|
+
return arbitrary_array
|
|
332
|
+
|
|
333
|
+
def mirrored(self, plane: Literal['xOz', 'yOz']) -> "FloatingBody":
|
|
334
|
+
if plane == "xOz":
|
|
335
|
+
mirrored_coord = 1
|
|
336
|
+
elif plane == "yOz":
|
|
337
|
+
mirrored_coord = 0
|
|
338
|
+
else:
|
|
339
|
+
raise ValueError(f"Unsupported value for plane: {plane}")
|
|
340
|
+
def mirror(p):
|
|
341
|
+
if isinstance(p, TranslationDof):
|
|
342
|
+
return TranslationDof(
|
|
343
|
+
direction=mirror(p.direction),
|
|
344
|
+
)
|
|
345
|
+
if isinstance(p, RotationDof):
|
|
346
|
+
return RotationDof(
|
|
347
|
+
rotation_center=mirror(p.rotation_center),
|
|
348
|
+
direction=mirror(p.direction),
|
|
349
|
+
)
|
|
350
|
+
elif isinstance(p, np.ndarray):
|
|
351
|
+
mirrored_p = p.copy()
|
|
352
|
+
mirrored_p[..., mirrored_coord] *= -1
|
|
353
|
+
return p
|
|
354
|
+
mirrored_dofs = {k: mirror(v) for k,v in self.dofs.items()}
|
|
355
|
+
mirrored_self = FloatingBody(
|
|
356
|
+
mesh=self.mesh.mirrored(plane),
|
|
357
|
+
lid_mesh=self.lid_mesh.mirrored(plane) if self.lid_mesh is not None else None,
|
|
358
|
+
dofs=mirrored_dofs,
|
|
359
|
+
center_of_mass=mirror(self.center_of_mass) if self.center_of_mass is not None else None,
|
|
360
|
+
mass=self.mass,
|
|
361
|
+
)
|
|
362
|
+
if hasattr(self, 'rotation_center'):
|
|
363
|
+
mirrored_self.rotation_center = mirror(self.rotation_center)
|
|
364
|
+
return mirrored_self
|
|
365
|
+
|
|
366
|
+
def translated(self, shift, *, name=None) -> "FloatingBody":
|
|
367
|
+
shift = np.asarray(shift)
|
|
368
|
+
def translate_dof(d):
|
|
369
|
+
if isinstance(d, RotationDof):
|
|
370
|
+
return RotationDof(
|
|
371
|
+
rotation_center=d.rotation_center + shift,
|
|
372
|
+
direction=d.direction,
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
return d
|
|
376
|
+
|
|
377
|
+
translated_self = FloatingBody(
|
|
378
|
+
mesh=self.mesh.translated(shift),
|
|
379
|
+
lid_mesh=self.lid_mesh.translated(shift) if self.lid_mesh is not None else None,
|
|
380
|
+
dofs={k: translate_dof(v) for k, v in self.dofs.items()},
|
|
381
|
+
center_of_mass=self.center_of_mass + shift if self.center_of_mass is not None else None,
|
|
382
|
+
mass=self.mass,
|
|
383
|
+
name=name
|
|
384
|
+
)
|
|
385
|
+
if hasattr(self, 'rotation_center'):
|
|
386
|
+
translated_self.rotation_center = self.rotation_center + shift
|
|
387
|
+
return translated_self
|
|
388
|
+
|
|
389
|
+
def translated_x(self, dx: float, *, name=None) -> "FloatingBody":
|
|
390
|
+
return self.translated([dx, 0.0, 0.0], name=name)
|
|
391
|
+
|
|
392
|
+
def translated_y(self, dy: float, *, name=None) -> "FloatingBody":
|
|
393
|
+
return self.translated([0.0, dy, 0.0], name=name)
|
|
394
|
+
|
|
395
|
+
def translated_z(self, dz: float, *, name=None) -> "FloatingBody":
|
|
396
|
+
return self.translated([0.0, 0.0, dz], name=name)
|
|
397
|
+
|
|
398
|
+
def rotated_with_matrix(self, R, *, name=None) -> "FloatingBody":
|
|
399
|
+
def rotate_dof(d):
|
|
400
|
+
if isinstance(d, TranslationDof):
|
|
401
|
+
return TranslationDof(
|
|
402
|
+
direction=d.direction @ R.T,
|
|
403
|
+
)
|
|
404
|
+
elif isinstance(d, RotationDof):
|
|
405
|
+
return RotationDof(
|
|
406
|
+
rotation_center=d.rotation_center @ R.T,
|
|
407
|
+
direction=d.direction @ R.T,
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
return d
|
|
411
|
+
rotated_self = FloatingBody(
|
|
412
|
+
mesh=self.mesh.rotated_with_matrix(R),
|
|
413
|
+
lid_mesh=self.lid_mesh.rotated_with_matrix(R) if self.lid_mesh is not None else None,
|
|
414
|
+
dofs={k: rotate_dof(v) for k, v in self.dofs.items()},
|
|
415
|
+
center_of_mass=self.center_of_mass @ R.T if self.center_of_mass is not None else None,
|
|
416
|
+
mass=self.mass,
|
|
417
|
+
name=name
|
|
418
|
+
)
|
|
419
|
+
if hasattr(self, 'rotation_center'):
|
|
420
|
+
rotated_self.rotation_center = self.rotation_center @ R.T
|
|
421
|
+
return rotated_self
|
|
422
|
+
|
|
423
|
+
def rotated_x(self, angle: float, *, name=None) -> "FloatingBody":
|
|
424
|
+
c, s = np.cos(angle), np.sin(angle)
|
|
425
|
+
R = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
|
426
|
+
return self.rotated_with_matrix(R, name=name)
|
|
427
|
+
|
|
428
|
+
def rotated_y(self, angle: float, *, name=None) -> "FloatingBody":
|
|
429
|
+
c, s = np.cos(angle), np.sin(angle)
|
|
430
|
+
R = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
|
431
|
+
return self.rotated_with_matrix(R, name=name)
|
|
432
|
+
|
|
433
|
+
def rotated_z(self, angle: float, *, name=None) -> "FloatingBody":
|
|
434
|
+
c, s = np.cos(angle), np.sin(angle)
|
|
435
|
+
R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
|
436
|
+
return self.rotated_with_matrix(R, name=name)
|
|
437
|
+
|
|
438
|
+
def _apply_on_mesh(self, func, args, kwargs):
|
|
439
|
+
mesh_with_ids = self.mesh.with_metadata(origin_panel=np.arange(self.mesh.nb_faces))
|
|
440
|
+
transformed_mesh = func(mesh_with_ids, *args, **kwargs)
|
|
441
|
+
transformed_mesh, faces_ids = transformed_mesh.pop_metadata("origin_panel")
|
|
442
|
+
|
|
443
|
+
if self.lid_mesh is not None:
|
|
444
|
+
transformed_lid_mesh = func(self.lid_mesh, *args, **kwargs)
|
|
445
|
+
if transformed_lid_mesh.nb_faces == 0:
|
|
446
|
+
LOG.warning("Empty lid mesh %s has been removed.", self.lid_mesh)
|
|
447
|
+
transformed_lid_mesh = None
|
|
448
|
+
else:
|
|
449
|
+
transformed_lid_mesh = None
|
|
450
|
+
|
|
451
|
+
new_dofs = {}
|
|
452
|
+
for name, dof in self.dofs.items():
|
|
453
|
+
if isinstance(dof, DofOnSubmesh):
|
|
454
|
+
former_mask = np.zeros(self.mesh.nb_faces, dtype=bool)
|
|
455
|
+
former_mask[dof.faces] = True
|
|
456
|
+
new_mask = former_mask[faces_ids]
|
|
457
|
+
new_dofs[name] = DofOnSubmesh(dof.dof, np.where(new_mask)[0])
|
|
458
|
+
elif isinstance(dof, AbstractDof):
|
|
459
|
+
new_dofs[name] = dof
|
|
460
|
+
else:
|
|
461
|
+
# dof is an array or array-like
|
|
462
|
+
new_dofs[name] = np.asarray(dof)[faces_ids]
|
|
463
|
+
|
|
464
|
+
return transformed_mesh, transformed_lid_mesh, new_dofs
|
|
465
|
+
|
|
466
|
+
def clipped(self, *, origin, normal, name=None) -> "FloatingBody":
|
|
467
|
+
clipped_mesh, clipped_lid_mesh, updated_dofs = self._apply_on_mesh(
|
|
468
|
+
self.mesh.__class__.clipped,
|
|
469
|
+
(),
|
|
470
|
+
{'origin': origin, 'normal': normal}
|
|
471
|
+
)
|
|
472
|
+
if name is None:
|
|
473
|
+
name = self.name
|
|
474
|
+
return FloatingBody(
|
|
475
|
+
mesh=clipped_mesh,
|
|
476
|
+
lid_mesh=clipped_lid_mesh,
|
|
477
|
+
dofs=updated_dofs,
|
|
478
|
+
name=name
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def immersed_part(self, free_surface=0.0, *, sea_bottom=None, water_depth=None, name=None) -> "FloatingBody":
|
|
482
|
+
clipped_mesh, clipped_lid_mesh, updated_dofs = self._apply_on_mesh(
|
|
483
|
+
self.mesh.__class__.immersed_part,
|
|
484
|
+
(free_surface,),
|
|
485
|
+
{'sea_bottom': sea_bottom, 'water_depth': water_depth}
|
|
486
|
+
)
|
|
487
|
+
if name is None:
|
|
488
|
+
name = self.name
|
|
489
|
+
return FloatingBody(
|
|
490
|
+
mesh=clipped_mesh,
|
|
491
|
+
lid_mesh=clipped_lid_mesh,
|
|
492
|
+
dofs=updated_dofs,
|
|
493
|
+
center_of_mass=self.center_of_mass,
|
|
494
|
+
mass=self.mass,
|
|
495
|
+
name=name
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
#############
|
|
499
|
+
# Display #
|
|
500
|
+
#############
|
|
501
|
+
|
|
502
|
+
def __short_str__(self):
|
|
503
|
+
return (f"{self.__class__.__name__}(..., name=\"{self.name}\")")
|
|
504
|
+
|
|
505
|
+
def _optional_params_str(self):
|
|
506
|
+
items = []
|
|
507
|
+
if self.mass is not None: items.append(f"mass={self.mass}, ")
|
|
508
|
+
if self.center_of_mass is not None: items.append(f"center_of_mass={self.center_of_mass}, ")
|
|
509
|
+
return ''.join(items)
|
|
510
|
+
|
|
511
|
+
def __str__(self):
|
|
512
|
+
short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
|
|
513
|
+
|
|
514
|
+
if self.lid_mesh is not None:
|
|
515
|
+
lid_mesh_str = self.lid_mesh.__short_str__()
|
|
516
|
+
else:
|
|
517
|
+
lid_mesh_str = str(None)
|
|
518
|
+
|
|
519
|
+
return (f"{self.__class__.__name__}(mesh={self.mesh.__short_str__()}, lid_mesh={lid_mesh_str}, "
|
|
520
|
+
f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
|
|
521
|
+
|
|
522
|
+
def __repr__(self):
|
|
523
|
+
short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
|
|
524
|
+
|
|
525
|
+
if self.lid_mesh is not None:
|
|
526
|
+
lid_mesh_str = str(self.lid_mesh)
|
|
527
|
+
else:
|
|
528
|
+
lid_mesh_str = str(None)
|
|
529
|
+
|
|
530
|
+
return (f"{self.__class__.__name__}(mesh={str(self.mesh)}, lid_mesh={lid_mesh_str}, "
|
|
531
|
+
f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
|
|
532
|
+
|
|
533
|
+
def _repr_pretty_(self, p, cycle):
|
|
534
|
+
p.text(self.__str__())
|
|
535
|
+
|
|
536
|
+
def __rich_repr__(self):
|
|
537
|
+
class DofWithShortRepr:
|
|
538
|
+
def __repr__(self):
|
|
539
|
+
return '...'
|
|
540
|
+
yield "mesh", self.mesh
|
|
541
|
+
yield "lid_mesh", self.lid_mesh
|
|
542
|
+
yield "dofs", {d: DofWithShortRepr() for d in self.dofs}
|
|
543
|
+
if self.mass is not None:
|
|
544
|
+
yield "mass", self.mass, None
|
|
545
|
+
if self.center_of_mass is not None:
|
|
546
|
+
yield "center_of_mass", tuple(self.center_of_mass)
|
|
547
|
+
yield "name", self.name
|
|
548
|
+
|
|
549
|
+
def show(self, *args, **kwargs):
|
|
550
|
+
return self.mesh.show(*args, **kwargs)
|
|
551
|
+
|
|
552
|
+
def show_pyvista(self, *args, **kwargs):
|
|
553
|
+
return self.mesh.show_pyvista(*args, **kwargs)
|
|
554
|
+
|
|
555
|
+
def show_matplotlib(self, *args, **kwargs):
|
|
556
|
+
return self.mesh.show_matplotlib(*args, **kwargs)
|
|
557
|
+
|
|
558
|
+
def animate(self, motion, *args, **kwargs):
|
|
559
|
+
"""Display a motion as a 3D animation.
|
|
560
|
+
|
|
561
|
+
Parameters
|
|
562
|
+
==========
|
|
563
|
+
motion: dict or pd.Series or str
|
|
564
|
+
A dict or series mapping the name of the dofs to its amplitude.
|
|
565
|
+
If a single string is passed, it is assumed to be the name of a dof
|
|
566
|
+
and this dof with a unit amplitude will be displayed.
|
|
567
|
+
"""
|
|
568
|
+
from capytaine.ui.vtk.animation import Animation
|
|
569
|
+
if isinstance(motion, str):
|
|
570
|
+
motion = {motion: 1.0}
|
|
571
|
+
elif isinstance(motion, xr.DataArray):
|
|
572
|
+
motion = {k: motion.sel(radiating_dof=k).data for k in motion.coords["radiating_dof"].data}
|
|
573
|
+
|
|
574
|
+
if any(dof not in self.dofs for dof in motion):
|
|
575
|
+
missing_dofs = set(motion.keys()) - set(self.dofs.keys())
|
|
576
|
+
raise ValueError(f"Trying to animate the body {self.name} using dof(s) {missing_dofs}, but no dof of this name is defined for {self.name}.")
|
|
577
|
+
|
|
578
|
+
animation = Animation(*args, **kwargs)
|
|
579
|
+
animation._add_actor(self.mesh.merged(), faces_motion=sum(motion[dof_name] * dof for dof_name, dof in self.dofs.items() if dof_name in motion))
|
|
580
|
+
return animation
|
|
581
|
+
|
|
582
|
+
#################################
|
|
583
|
+
# Irregular frequencies removal #
|
|
584
|
+
#################################
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def minimal_computable_wavelength(self):
|
|
588
|
+
"""For accuracy of the resolution, wavelength should not be smaller than this value."""
|
|
589
|
+
if self.lid_mesh is not None:
|
|
590
|
+
return max(8*self.mesh.faces_radiuses.max(), 8*self.lid_mesh.faces_radiuses.max())
|
|
591
|
+
else:
|
|
592
|
+
return 8*self.mesh.faces_radiuses.max()
|
|
593
|
+
|
|
594
|
+
@lru_cache
|
|
595
|
+
def first_irregular_frequency_estimate(self, *, g=9.81):
|
|
596
|
+
r"""Estimates the angular frequency of the lowest irregular
|
|
597
|
+
frequency.
|
|
598
|
+
This is based on the formula for the lowest irregular frequency of a
|
|
599
|
+
parallelepiped of size :math:`L \times B` and draft :math:`H`:
|
|
600
|
+
|
|
601
|
+
.. math::
|
|
602
|
+
\omega = \sqrt{
|
|
603
|
+
\frac{\pi g \sqrt{\frac{1}{B^2} + \frac{1}{L^2}}}
|
|
604
|
+
{\tanh\left(\pi H \sqrt{\frac{1}{B^2} + \frac{1}{L^2}} \right)}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
The formula is applied to all shapes to get an estimate that is usually
|
|
608
|
+
conservative.
|
|
609
|
+
The definition of a lid (supposed to be fully covering and horizontal)
|
|
610
|
+
is taken into account.
|
|
611
|
+
"""
|
|
612
|
+
if self.lid_mesh is None:
|
|
613
|
+
draft = abs(self.mesh.vertices[:, 2].min())
|
|
614
|
+
else:
|
|
615
|
+
draft = abs(self.lid_mesh.vertices[:, 2].min())
|
|
616
|
+
if draft < 1e-6:
|
|
617
|
+
return np.inf
|
|
618
|
+
|
|
619
|
+
# Look for the x and y span of each components (e.g. for multibody) and
|
|
620
|
+
# keep the one causing the lowest irregular frequency.
|
|
621
|
+
# The draft is supposed to be same for all components.
|
|
622
|
+
omega = np.inf
|
|
623
|
+
for comp in connected_components(self.mesh):
|
|
624
|
+
for ccomp in connected_components_of_waterline(comp):
|
|
625
|
+
x_span = ccomp.vertices[:, 0].max() - ccomp.vertices[:, 0].min()
|
|
626
|
+
y_span = ccomp.vertices[:, 1].max() - ccomp.vertices[:, 1].min()
|
|
627
|
+
p = np.hypot(1/x_span, 1/y_span)
|
|
628
|
+
omega_comp = np.sqrt(np.pi*g*p/(np.tanh(np.pi*draft*p)))
|
|
629
|
+
omega = min(omega, omega_comp)
|
|
630
|
+
return omega
|