capytaine 2.1__cp311-cp311-macosx_10_9_x86_64.whl → 2.2__cp311-cp311-macosx_10_9_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 +1 -1
- capytaine/bem/problems_and_results.py +13 -8
- capytaine/bem/solver.py +57 -23
- capytaine/bodies/bodies.py +125 -24
- capytaine/green_functions/delhommeau.py +103 -51
- capytaine/green_functions/libs/Delhommeau_float32.cpython-311-darwin.so +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cpython-311-darwin.so +0 -0
- capytaine/io/mesh_loaders.py +48 -24
- capytaine/io/meshio.py +4 -1
- capytaine/io/xarray.py +4 -2
- capytaine/matrices/linear_solvers.py +2 -3
- capytaine/meshes/collections.py +13 -2
- capytaine/meshes/meshes.py +124 -2
- capytaine/meshes/predefined/cylinders.py +2 -2
- capytaine/meshes/properties.py +43 -0
- capytaine/post_pro/rao.py +1 -1
- capytaine/tools/cache_on_disk.py +3 -1
- capytaine/tools/symbolic_multiplication.py +5 -2
- capytaine/ui/vtk/body_viewer.py +2 -0
- {capytaine-2.1.dist-info → capytaine-2.2.dist-info}/METADATA +3 -8
- {capytaine-2.1.dist-info → capytaine-2.2.dist-info}/RECORD +27 -29
- capytaine/green_functions/libs/XieDelhommeau_float32.cpython-311-darwin.so +0 -0
- capytaine/green_functions/libs/XieDelhommeau_float64.cpython-311-darwin.so +0 -0
- {capytaine-2.1.dist-info → capytaine-2.2.dist-info}/LICENSE +0 -0
- {capytaine-2.1.dist-info → capytaine-2.2.dist-info}/WHEEL +0 -0
- {capytaine-2.1.dist-info → capytaine-2.2.dist-info}/entry_points.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
capytaine/__about__.py
CHANGED
|
@@ -5,7 +5,7 @@ __all__ = ["__title__", "__description__", "__version__", "__author__", "__uri__
|
|
|
5
5
|
__title__ = "capytaine"
|
|
6
6
|
__description__ = """Python BEM solver for linear potential flow, based on Nemoh"""
|
|
7
7
|
|
|
8
|
-
__version__ = "2.
|
|
8
|
+
__version__ = "2.2"
|
|
9
9
|
|
|
10
10
|
__author__ = "Matthieu Ancellin"
|
|
11
11
|
__uri__ = "https://github.com/capytaine/capytaine"
|
|
@@ -193,7 +193,7 @@ class LinearPotentialFlowProblem:
|
|
|
193
193
|
or len(self.body.mesh.faces) == 0):
|
|
194
194
|
raise ValueError(f"The mesh of the body {self.body.__short_str__()} is empty.")
|
|
195
195
|
|
|
196
|
-
panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface
|
|
196
|
+
panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface + 1e-8
|
|
197
197
|
panels_below_sb = self.body.mesh.faces_centers[:, 2] <= -self.water_depth
|
|
198
198
|
if (any(panels_above_fs) or any(panels_below_sb)):
|
|
199
199
|
|
|
@@ -218,7 +218,7 @@ class LinearPotentialFlowProblem:
|
|
|
218
218
|
if len(self.boundary_condition.shape) != 1:
|
|
219
219
|
raise ValueError(f"Expected a 1-dimensional array as boundary_condition. Provided boundary condition's shape: {self.boundary_condition.shape}.")
|
|
220
220
|
|
|
221
|
-
if self.boundary_condition.shape[0] != self.body.
|
|
221
|
+
if self.boundary_condition.shape[0] != self.body.mesh_including_lid.nb_faces:
|
|
222
222
|
raise ValueError(
|
|
223
223
|
f"The shape of the boundary condition ({self.boundary_condition.shape})"
|
|
224
224
|
f"does not match the number of faces of the mesh ({self.body.mesh.nb_faces})."
|
|
@@ -256,7 +256,7 @@ class LinearPotentialFlowProblem:
|
|
|
256
256
|
def __str__(self):
|
|
257
257
|
"""Do not display default values in str(problem)."""
|
|
258
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}",
|
|
259
|
+
f"{self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type)):.3f}",
|
|
260
260
|
f"water_depth={self.water_depth}"]
|
|
261
261
|
|
|
262
262
|
if not self.forward_speed == _default_parameters['forward_speed']:
|
|
@@ -345,7 +345,7 @@ class DiffractionProblem(LinearPotentialFlowProblem):
|
|
|
345
345
|
forward_speed=forward_speed, rho=rho, g=g)
|
|
346
346
|
|
|
347
347
|
if float(self.omega) in {0.0, np.inf}:
|
|
348
|
-
raise NotImplementedError(
|
|
348
|
+
raise NotImplementedError("DiffractionProblem does not support zero or infinite frequency.")
|
|
349
349
|
|
|
350
350
|
if self.body is not None:
|
|
351
351
|
|
|
@@ -356,6 +356,9 @@ class DiffractionProblem(LinearPotentialFlowProblem):
|
|
|
356
356
|
# Note that even with forward speed, this is computed based on the
|
|
357
357
|
# frequency and not the encounter frequency.
|
|
358
358
|
|
|
359
|
+
if self.body.lid_mesh is not None:
|
|
360
|
+
self.boundary_condition = np.concatenate([self.boundary_condition, np.zeros(self.body.lid_mesh.nb_faces)])
|
|
361
|
+
|
|
359
362
|
if len(self.body.dofs) == 0:
|
|
360
363
|
LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
|
|
361
364
|
|
|
@@ -398,10 +401,9 @@ class RadiationProblem(LinearPotentialFlowProblem):
|
|
|
398
401
|
self.radiating_dof = next(iter(self.body.dofs))
|
|
399
402
|
|
|
400
403
|
if self.radiating_dof not in self.body.dofs:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
raise ValueError("Unrecognized degree of freedom name.")
|
|
404
|
+
raise ValueError(f"In {self}:\n"
|
|
405
|
+
f"the radiating dof {repr(self.radiating_dof)} is not one of the degrees of freedom of the body.\n"
|
|
406
|
+
f"The dofs of the body are {list(self.body.dofs.keys())}")
|
|
405
407
|
|
|
406
408
|
dof = self.body.dofs[self.radiating_dof]
|
|
407
409
|
|
|
@@ -422,6 +424,9 @@ class RadiationProblem(LinearPotentialFlowProblem):
|
|
|
422
424
|
)
|
|
423
425
|
self.boundary_condition += self.forward_speed * ddofdx_dot_n
|
|
424
426
|
|
|
427
|
+
if self.body.lid_mesh is not None:
|
|
428
|
+
self.boundary_condition = np.concatenate([self.boundary_condition, np.zeros(self.body.lid_mesh.nb_faces)])
|
|
429
|
+
|
|
425
430
|
|
|
426
431
|
def _astuple(self):
|
|
427
432
|
return super()._astuple() + (self.radiating_dof,)
|
capytaine/bem/solver.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Copyright (C) 2017-
|
|
2
|
-
# See LICENSE file at <https://github.com/
|
|
1
|
+
# Copyright (C) 2017-2024 Matthieu Ancellin
|
|
2
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
3
3
|
"""Solver for the BEM problem.
|
|
4
4
|
|
|
5
5
|
.. code-block:: python
|
|
@@ -93,7 +93,9 @@ class BEMSolver:
|
|
|
93
93
|
"""
|
|
94
94
|
LOG.info("Solve %s.", problem)
|
|
95
95
|
|
|
96
|
-
if _check_wavelength:
|
|
96
|
+
if _check_wavelength:
|
|
97
|
+
self._check_wavelength_and_mesh_resolution([problem])
|
|
98
|
+
self._check_wavelength_and_irregular_frequencies([problem])
|
|
97
99
|
|
|
98
100
|
if problem.forward_speed != 0.0:
|
|
99
101
|
omega, wavenumber = problem.encounter_omega, problem.encounter_wavenumber
|
|
@@ -106,7 +108,7 @@ class BEMSolver:
|
|
|
106
108
|
raise NotImplementedError("Direct solver is not able to solve problems with forward speed.")
|
|
107
109
|
|
|
108
110
|
S, D = self.engine.build_matrices(
|
|
109
|
-
problem.body.
|
|
111
|
+
problem.body.mesh_including_lid, problem.body.mesh_including_lid,
|
|
110
112
|
problem.free_surface, problem.water_depth, wavenumber,
|
|
111
113
|
self.green_function, adjoint_double_layer=False
|
|
112
114
|
)
|
|
@@ -116,7 +118,7 @@ class BEMSolver:
|
|
|
116
118
|
sources = None
|
|
117
119
|
else:
|
|
118
120
|
S, K = self.engine.build_matrices(
|
|
119
|
-
problem.body.
|
|
121
|
+
problem.body.mesh_including_lid, problem.body.mesh_including_lid,
|
|
120
122
|
problem.free_surface, problem.water_depth, wavenumber,
|
|
121
123
|
self.green_function, adjoint_double_layer=True
|
|
122
124
|
)
|
|
@@ -127,10 +129,11 @@ class BEMSolver:
|
|
|
127
129
|
if problem.forward_speed != 0.0:
|
|
128
130
|
result = problem.make_results_container(sources=sources)
|
|
129
131
|
# Temporary result object to compute the ∇Φ term
|
|
130
|
-
nabla_phi = self._compute_potential_gradient(problem.body.
|
|
132
|
+
nabla_phi = self._compute_potential_gradient(problem.body.mesh_including_lid, result)
|
|
131
133
|
pressure += problem.rho * problem.forward_speed * nabla_phi[:, 0]
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
pressure_on_hull = pressure[:problem.body.mesh.nb_faces] # Discards pressure on lid if any
|
|
136
|
+
forces = problem.body.integrate_pressure(pressure_on_hull)
|
|
134
137
|
|
|
135
138
|
if not keep_details:
|
|
136
139
|
result = problem.make_results_container(forces)
|
|
@@ -162,7 +165,9 @@ class BEMSolver:
|
|
|
162
165
|
list of LinearPotentialFlowResult
|
|
163
166
|
the solved problems
|
|
164
167
|
"""
|
|
165
|
-
if _check_wavelength:
|
|
168
|
+
if _check_wavelength:
|
|
169
|
+
self._check_wavelength_and_mesh_resolution(problems)
|
|
170
|
+
self._check_wavelength_and_irregular_frequencies(problems)
|
|
166
171
|
|
|
167
172
|
if n_jobs == 1: # force sequential resolution
|
|
168
173
|
problems = sorted(problems)
|
|
@@ -184,31 +189,60 @@ class BEMSolver:
|
|
|
184
189
|
return results
|
|
185
190
|
|
|
186
191
|
@staticmethod
|
|
187
|
-
def
|
|
192
|
+
def _check_wavelength_and_mesh_resolution(problems):
|
|
188
193
|
"""Display a warning if some of the problems have a mesh resolution
|
|
189
194
|
that might not be sufficient for the given wavelength."""
|
|
190
195
|
risky_problems = [pb for pb in problems
|
|
191
|
-
if pb.wavelength < pb.body.minimal_computable_wavelength]
|
|
196
|
+
if 0.0 < pb.wavelength < pb.body.minimal_computable_wavelength]
|
|
192
197
|
nb_risky_problems = len(risky_problems)
|
|
193
198
|
if nb_risky_problems == 1:
|
|
194
199
|
pb = risky_problems[0]
|
|
195
200
|
freq_type = risky_problems[0].provided_freq_type
|
|
196
201
|
freq = pb.__getattribute__(freq_type)
|
|
197
202
|
LOG.warning(f"Mesh resolution for {pb}:\n"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
f"The resolution of the mesh of the body {pb.body.__short_str__()} might "
|
|
204
|
+
f"be insufficient for {freq_type}={freq}.\n"
|
|
205
|
+
"This warning appears because the largest panel of this mesh "
|
|
206
|
+
f"has radius {pb.body.mesh.faces_radiuses.max():.3f} > wavelength/8."
|
|
207
|
+
)
|
|
203
208
|
elif nb_risky_problems > 1:
|
|
204
209
|
freq_type = risky_problems[0].provided_freq_type
|
|
205
210
|
freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
|
|
206
211
|
LOG.warning(f"Mesh resolution for {nb_risky_problems} problems:\n"
|
|
207
|
-
|
|
212
|
+
"The resolution of the mesh might be insufficient "
|
|
208
213
|
f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
"This warning appears when the largest panel of this mesh "
|
|
215
|
+
"has radius > wavelength/8."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _check_wavelength_and_irregular_frequencies(problems):
|
|
220
|
+
"""Display a warning if some of the problems might encounter irregular frequencies."""
|
|
221
|
+
risky_problems = [pb for pb in problems
|
|
222
|
+
if pb.body.first_irregular_frequency_estimate() < pb.omega < np.inf]
|
|
223
|
+
nb_risky_problems = len(risky_problems)
|
|
224
|
+
if nb_risky_problems >= 1:
|
|
225
|
+
if any(pb.body.lid_mesh is None for pb in problems):
|
|
226
|
+
recommendation = "Setting a lid for the floating body is recommended."
|
|
227
|
+
else:
|
|
228
|
+
recommendation = "The lid might need to be closer to the free surface."
|
|
229
|
+
if nb_risky_problems == 1:
|
|
230
|
+
pb = risky_problems[0]
|
|
231
|
+
freq_type = risky_problems[0].provided_freq_type
|
|
232
|
+
freq = pb.__getattribute__(freq_type)
|
|
233
|
+
LOG.warning(f"Irregular frequencies for {pb}:\n"
|
|
234
|
+
f"The body {pb.body.__short_str__()} might display irregular frequencies "
|
|
235
|
+
f"for {freq_type}={freq}.\n"
|
|
236
|
+
+ recommendation
|
|
237
|
+
)
|
|
238
|
+
elif nb_risky_problems > 1:
|
|
239
|
+
freq_type = risky_problems[0].provided_freq_type
|
|
240
|
+
freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
|
|
241
|
+
LOG.warning(f"Irregular frequencies for {nb_risky_problems} problems:\n"
|
|
242
|
+
"Irregular frequencies might be encountered "
|
|
243
|
+
f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
|
|
244
|
+
+ recommendation
|
|
245
|
+
)
|
|
212
246
|
|
|
213
247
|
def fill_dataset(self, dataset, bodies, *, method='indirect', n_jobs=1, **kwargs):
|
|
214
248
|
"""Solve a set of problems defined by the coordinates of an xarray dataset.
|
|
@@ -271,7 +305,7 @@ class BEMSolver:
|
|
|
271
305
|
They probably have not been stored by the solver because the option keep_details=True have not been set or the direct method has been used.
|
|
272
306
|
Please re-run the resolution with the indirect method and keep_details=True.""")
|
|
273
307
|
|
|
274
|
-
S, _ = self.green_function.evaluate(points, result.body.
|
|
308
|
+
S, _ = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber)
|
|
275
309
|
potential = S @ result.sources # Sum the contributions of all panels in the mesh
|
|
276
310
|
return potential.reshape(output_shape)
|
|
277
311
|
|
|
@@ -283,7 +317,7 @@ class BEMSolver:
|
|
|
283
317
|
They probably have not been stored by the solver because the option keep_details=True have not been set.
|
|
284
318
|
Please re-run the resolution with this option.""")
|
|
285
319
|
|
|
286
|
-
_, gradG = self.green_function.evaluate(points, result.body.
|
|
320
|
+
_, gradG = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber,
|
|
287
321
|
early_dot_product=False)
|
|
288
322
|
velocities = np.einsum('ijk,j->ik', gradG, result.sources) # Sum the contributions of all panels in the mesh
|
|
289
323
|
return velocities.reshape((*output_shape, 3))
|
|
@@ -409,7 +443,7 @@ class BEMSolver:
|
|
|
409
443
|
if chunk_size > mesh.nb_faces:
|
|
410
444
|
S = self.engine.build_S_matrix(
|
|
411
445
|
mesh,
|
|
412
|
-
result.body.
|
|
446
|
+
result.body.mesh_including_lid,
|
|
413
447
|
result.free_surface, result.water_depth, result.wavenumber,
|
|
414
448
|
self.green_function
|
|
415
449
|
)
|
|
@@ -421,7 +455,7 @@ class BEMSolver:
|
|
|
421
455
|
faces_to_extract = list(range(i, min(i+chunk_size, mesh.nb_faces)))
|
|
422
456
|
S = self.engine.build_S_matrix(
|
|
423
457
|
mesh.extract_faces(faces_to_extract),
|
|
424
|
-
result.body.
|
|
458
|
+
result.body.mesh_including_lid,
|
|
425
459
|
result.free_surface, result.water_depth, result.wavenumber,
|
|
426
460
|
self.green_function
|
|
427
461
|
)
|
capytaine/bodies/bodies.py
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""Floating bodies to be used in radiation-diffraction problems."""
|
|
2
|
-
# Copyright (C) 2017-
|
|
3
|
-
# See LICENSE file at <https://github.com/
|
|
2
|
+
# Copyright (C) 2017-2024 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import copy
|
|
7
7
|
from itertools import chain, accumulate, zip_longest
|
|
8
|
+
from functools import cached_property
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
10
11
|
import xarray as xr
|
|
11
12
|
|
|
12
|
-
from capytaine.
|
|
13
|
-
meshio = silently_import_optional_dependency("meshio")
|
|
14
|
-
|
|
13
|
+
from capytaine.meshes.collections import CollectionOfMeshes
|
|
15
14
|
from capytaine.meshes.geometry import Abstract3DObject, ClippableMixin, Plane, inplace_transformation
|
|
15
|
+
from capytaine.meshes.properties import connected_components, connected_components_of_waterline
|
|
16
16
|
from capytaine.meshes.meshes import Mesh
|
|
17
17
|
from capytaine.meshes.symmetric import build_regular_array_of_meshes
|
|
18
|
-
from capytaine.meshes.collections import CollectionOfMeshes
|
|
19
18
|
from capytaine.bodies.dofs import RigidBodyDofsPlaceholder
|
|
20
19
|
|
|
20
|
+
from capytaine.tools.optional_imports import silently_import_optional_dependency
|
|
21
|
+
meshio = silently_import_optional_dependency("meshio")
|
|
22
|
+
|
|
21
23
|
LOG = logging.getLogger(__name__)
|
|
22
24
|
|
|
23
25
|
TRANSLATION_DOFS_DIRECTIONS = {"surge": (1, 0, 0), "sway": (0, 1, 0), "heave": (0, 0, 1)}
|
|
@@ -38,8 +40,12 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
38
40
|
Parameters
|
|
39
41
|
----------
|
|
40
42
|
mesh : Mesh or CollectionOfMeshes, optional
|
|
41
|
-
the mesh describing the geometry of the floating body.
|
|
43
|
+
the mesh describing the geometry of the hull of the floating body.
|
|
42
44
|
If none is given, a empty one is created.
|
|
45
|
+
lid_mesh : Mesh or CollectionOfMeshes or None, optional
|
|
46
|
+
a mesh of an internal lid for irregular frequencies removal.
|
|
47
|
+
Unlike the mesh of the hull, no dof is defined on the lid_mesh.
|
|
48
|
+
If none is given, none is used when solving the Boundary Integral Equation.
|
|
43
49
|
dofs : dict, optional
|
|
44
50
|
the degrees of freedom of the body.
|
|
45
51
|
If none is given, a empty dictionary is initialized.
|
|
@@ -55,7 +61,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
55
61
|
If none is given, the one of the mesh is used.
|
|
56
62
|
"""
|
|
57
63
|
|
|
58
|
-
def __init__(self, mesh=None, dofs=None, mass=None, center_of_mass=None, name=None):
|
|
64
|
+
def __init__(self, mesh=None, dofs=None, mass=None, center_of_mass=None, name=None, *, lid_mesh=None):
|
|
59
65
|
if mesh is None:
|
|
60
66
|
self.mesh = Mesh(name="dummy_mesh")
|
|
61
67
|
|
|
@@ -69,6 +75,11 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
69
75
|
else:
|
|
70
76
|
raise TypeError("Unrecognized `mesh` object passed to the FloatingBody constructor.")
|
|
71
77
|
|
|
78
|
+
if lid_mesh is not None:
|
|
79
|
+
self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
|
|
80
|
+
else:
|
|
81
|
+
self.lid_mesh = None
|
|
82
|
+
|
|
72
83
|
if name is None and mesh is None:
|
|
73
84
|
self.name = "dummy_body"
|
|
74
85
|
elif name is None:
|
|
@@ -99,13 +110,15 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
99
110
|
|
|
100
111
|
@staticmethod
|
|
101
112
|
def from_meshio(mesh, name=None) -> 'FloatingBody':
|
|
102
|
-
"""Create a FloatingBody from a meshio mesh object.
|
|
113
|
+
"""Create a FloatingBody from a meshio mesh object.
|
|
114
|
+
Kinda deprecated, use cpt.load_mesh instead."""
|
|
103
115
|
from capytaine.io.meshio import load_from_meshio
|
|
104
116
|
return FloatingBody(mesh=load_from_meshio(mesh, name), name=name)
|
|
105
117
|
|
|
106
118
|
@staticmethod
|
|
107
119
|
def from_file(filename: str, file_format=None, name=None) -> 'FloatingBody':
|
|
108
|
-
"""Create a FloatingBody from a mesh file using meshmagick.
|
|
120
|
+
"""Create a FloatingBody from a mesh file using meshmagick.
|
|
121
|
+
Kinda deprecated, use cpt.load_mesh instead."""
|
|
109
122
|
from capytaine.io.mesh_loaders import load_mesh
|
|
110
123
|
if name is None: name = filename
|
|
111
124
|
mesh = load_mesh(filename, file_format, name=f"{name}_mesh")
|
|
@@ -115,6 +128,13 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
115
128
|
"""Arbitrary order. The point is to sort together the problems involving the same body."""
|
|
116
129
|
return self.name < other.name
|
|
117
130
|
|
|
131
|
+
@cached_property
|
|
132
|
+
def mesh_including_lid(self):
|
|
133
|
+
if self.lid_mesh is not None:
|
|
134
|
+
return CollectionOfMeshes([self.mesh, self.lid_mesh])
|
|
135
|
+
else:
|
|
136
|
+
return self.mesh
|
|
137
|
+
|
|
118
138
|
##########
|
|
119
139
|
# Dofs #
|
|
120
140
|
##########
|
|
@@ -173,7 +193,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
173
193
|
if hasattr(self, point_attr) and getattr(self, point_attr) is not None:
|
|
174
194
|
axis_point = getattr(self, point_attr)
|
|
175
195
|
LOG.info(f"The rotation dof {name} has been initialized around the point: "
|
|
176
|
-
f"
|
|
196
|
+
f"{self.__short_str__()}.{point_attr} = {getattr(self, point_attr)}")
|
|
177
197
|
break
|
|
178
198
|
else:
|
|
179
199
|
axis_point = np.array([0, 0, 0])
|
|
@@ -267,7 +287,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
267
287
|
"""Returns volume of the FloatingBody."""
|
|
268
288
|
return self.mesh.volume
|
|
269
289
|
|
|
270
|
-
def disp_mass(self, *, rho=1000):
|
|
290
|
+
def disp_mass(self, *, rho=1000.0):
|
|
271
291
|
return self.mesh.disp_mass(rho=rho)
|
|
272
292
|
|
|
273
293
|
@property
|
|
@@ -512,7 +532,9 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
512
532
|
raise AttributeError("Cannot compute hydrostatics stiffness on {} since no dof has been defined.".format(self.name))
|
|
513
533
|
|
|
514
534
|
def divergence_dof(influenced_dof):
|
|
515
|
-
if
|
|
535
|
+
if influenced_dof.lower() in [*TRANSLATION_DOFS_DIRECTIONS, *ROTATION_DOFS_AXIS]:
|
|
536
|
+
return 0.0 # Dummy value that is not actually used afterwards.
|
|
537
|
+
elif divergence is None:
|
|
516
538
|
return 0.0
|
|
517
539
|
elif isinstance(divergence, dict) and influenced_dof in divergence.keys():
|
|
518
540
|
return divergence[influenced_dof]
|
|
@@ -536,7 +558,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
536
558
|
K = hs_set.hydrostatic_stiffness.sel(influenced_dof=list(self.dofs.keys()), radiating_dof=list(self.dofs.keys()))
|
|
537
559
|
return K
|
|
538
560
|
|
|
539
|
-
def compute_rigid_body_inertia(self, *, rho=1000, output_type="body_dofs"):
|
|
561
|
+
def compute_rigid_body_inertia(self, *, rho=1000.0, output_type="body_dofs"):
|
|
540
562
|
"""
|
|
541
563
|
Inertia Mass matrix of the body for 6 rigid DOFs.
|
|
542
564
|
|
|
@@ -626,8 +648,8 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
626
648
|
|
|
627
649
|
if output_type == "body_dofs":
|
|
628
650
|
if len(non_rigid_dofs) > 0:
|
|
629
|
-
LOG.warning(f"Non-rigid dofs
|
|
630
|
-
|
|
651
|
+
LOG.warning(f"Non-rigid dofs {non_rigid_dofs} detected: their \
|
|
652
|
+
inertia coefficients are assigned as NaN.")
|
|
631
653
|
|
|
632
654
|
inertia_matrix_xr = total_mass_xr.sel(influenced_dof=body_dof_names,
|
|
633
655
|
radiating_dof=body_dof_names)
|
|
@@ -729,7 +751,17 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
729
751
|
def join_bodies(*bodies, name=None) -> 'FloatingBody':
|
|
730
752
|
if name is None:
|
|
731
753
|
name = "+".join(body.name for body in bodies)
|
|
732
|
-
meshes = CollectionOfMeshes(
|
|
754
|
+
meshes = CollectionOfMeshes(
|
|
755
|
+
[body.mesh for body in bodies],
|
|
756
|
+
name=f"{name}_mesh"
|
|
757
|
+
)
|
|
758
|
+
if all(body.lid_mesh is None for body in bodies):
|
|
759
|
+
lid_meshes = None
|
|
760
|
+
else:
|
|
761
|
+
lid_meshes = CollectionOfMeshes(
|
|
762
|
+
[body.lid_mesh for body in bodies if body.lid_mesh is not None],
|
|
763
|
+
name=f"{name}_lid_mesh"
|
|
764
|
+
)
|
|
733
765
|
dofs = FloatingBody.combine_dofs(bodies)
|
|
734
766
|
|
|
735
767
|
if all(body.mass is not None for body in bodies):
|
|
@@ -744,7 +776,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
744
776
|
new_cog = None
|
|
745
777
|
|
|
746
778
|
joined_bodies = FloatingBody(
|
|
747
|
-
mesh=meshes,
|
|
779
|
+
mesh=meshes, lid_mesh=lid_meshes, dofs=dofs,
|
|
780
|
+
mass=new_mass, center_of_mass=new_cog, name=name
|
|
748
781
|
)
|
|
749
782
|
|
|
750
783
|
for matrix_name in ["inertia_matrix", "hydrostatic_stiffness"]:
|
|
@@ -820,7 +853,6 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
820
853
|
if not isinstance(locations, np.ndarray):
|
|
821
854
|
raise TypeError('locations must be of type np.ndarray')
|
|
822
855
|
assert locations.shape[1] == 2, 'locations must be of shape nx2, received {:}'.format(locations.shape)
|
|
823
|
-
n = locations.shape[0]
|
|
824
856
|
|
|
825
857
|
fb_list = []
|
|
826
858
|
for idx, li in enumerate(locations):
|
|
@@ -836,6 +868,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
836
868
|
def extract_faces(self, id_faces_to_extract, return_index=False):
|
|
837
869
|
"""Create a new FloatingBody by extracting some faces from the mesh.
|
|
838
870
|
The dofs evolve accordingly.
|
|
871
|
+
The lid_mesh, center_of_mass, mass and hydrostatics data are discarded.
|
|
839
872
|
"""
|
|
840
873
|
if isinstance(self.mesh, CollectionOfMeshes):
|
|
841
874
|
raise NotImplementedError # TODO
|
|
@@ -857,7 +890,12 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
857
890
|
return new_body
|
|
858
891
|
|
|
859
892
|
def sliced_by_plane(self, plane):
|
|
860
|
-
|
|
893
|
+
"""Return the same body, but replace the mesh by a set of two meshes
|
|
894
|
+
corresponding to each sides of the plane."""
|
|
895
|
+
return FloatingBody(mesh=self.mesh.sliced_by_plane(plane),
|
|
896
|
+
lid_mesh=self.lid_mesh.sliced_by_plane(plane)
|
|
897
|
+
if self.lid_mesh is not None else None,
|
|
898
|
+
dofs=self.dofs, name=self.name)
|
|
861
899
|
|
|
862
900
|
def minced(self, nb_slices=(8, 8, 4)):
|
|
863
901
|
"""Experimental method decomposing the mesh as a hierarchical structure.
|
|
@@ -918,6 +956,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
918
956
|
@inplace_transformation
|
|
919
957
|
def mirror(self, plane):
|
|
920
958
|
self.mesh.mirror(plane)
|
|
959
|
+
if self.lid_mesh is not None:
|
|
960
|
+
self.lid_mesh.mirror(plane)
|
|
921
961
|
for dof in self.dofs:
|
|
922
962
|
self.dofs[dof] -= 2 * np.outer(np.dot(self.dofs[dof], plane.normal), plane.normal)
|
|
923
963
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
@@ -930,6 +970,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
930
970
|
@inplace_transformation
|
|
931
971
|
def translate(self, vector, *args, **kwargs):
|
|
932
972
|
self.mesh.translate(vector, *args, **kwargs)
|
|
973
|
+
if self.lid_mesh is not None:
|
|
974
|
+
self.lid_mesh.translate(vector, *args, **kwargs)
|
|
933
975
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
934
976
|
if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
|
|
935
977
|
self.__dict__[point_attr] = np.array(self.__dict__[point_attr]) + vector
|
|
@@ -938,6 +980,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
938
980
|
@inplace_transformation
|
|
939
981
|
def rotate(self, axis, angle):
|
|
940
982
|
self.mesh.rotate(axis, angle)
|
|
983
|
+
if self.lid_mesh is not None:
|
|
984
|
+
self.lid_mesh.rotate(axis, angle)
|
|
941
985
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
942
986
|
if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
|
|
943
987
|
self.__dict__[point_attr] = axis.rotate_points([self.__dict__[point_attr]], angle)[0, :]
|
|
@@ -950,6 +994,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
950
994
|
# Clip mesh
|
|
951
995
|
LOG.info(f"Clipping {self.name} with respect to {plane}")
|
|
952
996
|
self.mesh.clip(plane)
|
|
997
|
+
if self.lid_mesh is not None:
|
|
998
|
+
self.lid_mesh.clip(plane)
|
|
953
999
|
|
|
954
1000
|
# Clip dofs
|
|
955
1001
|
ids = self.mesh._clipping_data['faces_ids']
|
|
@@ -976,11 +1022,25 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
976
1022
|
|
|
977
1023
|
def __str__(self):
|
|
978
1024
|
short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
|
|
979
|
-
|
|
1025
|
+
|
|
1026
|
+
if self.lid_mesh is not None:
|
|
1027
|
+
lid_mesh_str = self.lid_mesh.__short_str__()
|
|
1028
|
+
else:
|
|
1029
|
+
lid_mesh_str = str(None)
|
|
1030
|
+
|
|
1031
|
+
return (f"{self.__class__.__name__}(mesh={self.mesh.__short_str__()}, lid_mesh={lid_mesh_str}, "
|
|
1032
|
+
f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
|
|
980
1033
|
|
|
981
1034
|
def __repr__(self):
|
|
982
1035
|
short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
|
|
983
|
-
|
|
1036
|
+
|
|
1037
|
+
if self.lid_mesh is not None:
|
|
1038
|
+
lid_mesh_str = str(self.lid_mesh)
|
|
1039
|
+
else:
|
|
1040
|
+
lid_mesh_str = str(None)
|
|
1041
|
+
|
|
1042
|
+
return (f"{self.__class__.__name__}(mesh={str(self.mesh)}, lid_mesh={lid_mesh_str}, "
|
|
1043
|
+
f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
|
|
984
1044
|
|
|
985
1045
|
def _repr_pretty_(self, p, cycle):
|
|
986
1046
|
p.text(self.__str__())
|
|
@@ -990,6 +1050,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
990
1050
|
def __repr__(self):
|
|
991
1051
|
return '...'
|
|
992
1052
|
yield "mesh", self.mesh
|
|
1053
|
+
yield "lid_mesh", self.lid_mesh
|
|
993
1054
|
yield "dofs", {d: DofWithShortRepr() for d in self.dofs}
|
|
994
1055
|
if self.mass is not None:
|
|
995
1056
|
yield "mass", self.mass, None
|
|
@@ -1034,7 +1095,47 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
1034
1095
|
@property
|
|
1035
1096
|
def minimal_computable_wavelength(self):
|
|
1036
1097
|
"""For accuracy of the resolution, wavelength should not be smaller than this value."""
|
|
1037
|
-
|
|
1098
|
+
if self.lid_mesh is not None:
|
|
1099
|
+
return max(8*self.mesh.faces_radiuses.max(), 8*self.lid_mesh.faces_radiuses.max())
|
|
1100
|
+
else:
|
|
1101
|
+
return 8*self.mesh.faces_radiuses.max()
|
|
1102
|
+
|
|
1103
|
+
def first_irregular_frequency_estimate(self, *, g=9.81):
|
|
1104
|
+
r"""Estimates the angular frequency of the lowest irregular
|
|
1105
|
+
frequency.
|
|
1106
|
+
This is based on the formula for the lowest irregular frequency of a
|
|
1107
|
+
parallelepiped of size :math:`L \times B` and draft :math:`H`:
|
|
1108
|
+
|
|
1109
|
+
.. math::
|
|
1110
|
+
\omega = \sqrt{
|
|
1111
|
+
\frac{\pi g \sqrt{\frac{1}{B^2} + \frac{1}{L^2}}}
|
|
1112
|
+
{\tanh\left(\pi H \sqrt{\frac{1}{B^2} + \frac{1}{L^2}} \right)}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
The formula is applied to all shapes to get an estimate that is usually
|
|
1116
|
+
conservative.
|
|
1117
|
+
The definition of a lid (supposed to be fully covering and horizontal)
|
|
1118
|
+
is taken into account.
|
|
1119
|
+
"""
|
|
1120
|
+
if self.lid_mesh is None:
|
|
1121
|
+
draft = abs(self.mesh.vertices[:, 2].min())
|
|
1122
|
+
else:
|
|
1123
|
+
draft = abs(self.lid_mesh.vertices[:, 2].min())
|
|
1124
|
+
if draft < 1e-6:
|
|
1125
|
+
return np.inf
|
|
1126
|
+
|
|
1127
|
+
# Look for the x and y span of each components (e.g. for multibody) and
|
|
1128
|
+
# keep the one causing the lowest irregular frequency.
|
|
1129
|
+
# The draft is supposed to be same for all components.
|
|
1130
|
+
omega = np.inf
|
|
1131
|
+
for comp in connected_components(self.mesh):
|
|
1132
|
+
for ccomp in connected_components_of_waterline(comp):
|
|
1133
|
+
x_span = ccomp.vertices[:, 0].max() - ccomp.vertices[:, 0].min()
|
|
1134
|
+
y_span = ccomp.vertices[:, 1].max() - ccomp.vertices[:, 1].min()
|
|
1135
|
+
p = np.hypot(1/x_span, 1/y_span)
|
|
1136
|
+
omega_comp = np.sqrt(np.pi*g*p/(np.tanh(np.pi*draft*p)))
|
|
1137
|
+
omega = min(omega, omega_comp)
|
|
1138
|
+
return omega
|
|
1038
1139
|
|
|
1039
1140
|
def cluster_bodies(*bodies, name=None):
|
|
1040
1141
|
"""
|
|
@@ -1052,7 +1153,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
1052
1153
|
FloatingBody
|
|
1053
1154
|
Array built from the provided bodies
|
|
1054
1155
|
"""
|
|
1055
|
-
from scipy.cluster.hierarchy import linkage
|
|
1156
|
+
from scipy.cluster.hierarchy import linkage
|
|
1056
1157
|
nb_buoys = len(bodies)
|
|
1057
1158
|
|
|
1058
1159
|
if any(body.center_of_buoyancy is None for body in bodies):
|