capytaine 2.3__cp312-cp312-macosx_14_0_arm64.whl → 3.0.0a1__cp312-cp312-macosx_14_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. capytaine/.dylibs/libgfortran.5.dylib +0 -0
  3. capytaine/.dylibs/libquadmath.0.dylib +0 -0
  4. capytaine/__about__.py +7 -2
  5. capytaine/__init__.py +8 -12
  6. capytaine/bem/engines.py +234 -354
  7. capytaine/bem/problems_and_results.py +30 -21
  8. capytaine/bem/solver.py +205 -81
  9. capytaine/bodies/bodies.py +279 -862
  10. capytaine/bodies/dofs.py +136 -9
  11. capytaine/bodies/hydrostatics.py +540 -0
  12. capytaine/bodies/multibodies.py +216 -0
  13. capytaine/green_functions/{libs/Delhommeau_float32.cpython-312-darwin.so → Delhommeau_float32.cpython-312-darwin.so} +0 -0
  14. capytaine/green_functions/{libs/Delhommeau_float64.cpython-312-darwin.so → Delhommeau_float64.cpython-312-darwin.so} +0 -0
  15. capytaine/green_functions/abstract_green_function.py +2 -2
  16. capytaine/green_functions/delhommeau.py +50 -31
  17. capytaine/green_functions/hams.py +19 -13
  18. capytaine/io/legacy.py +3 -103
  19. capytaine/io/xarray.py +15 -10
  20. capytaine/meshes/__init__.py +2 -6
  21. capytaine/meshes/abstract_meshes.py +375 -0
  22. capytaine/meshes/clean.py +302 -0
  23. capytaine/meshes/clip.py +347 -0
  24. capytaine/meshes/export.py +89 -0
  25. capytaine/meshes/geometry.py +244 -394
  26. capytaine/meshes/io.py +433 -0
  27. capytaine/meshes/meshes.py +621 -676
  28. capytaine/meshes/predefined/cylinders.py +22 -56
  29. capytaine/meshes/predefined/rectangles.py +26 -85
  30. capytaine/meshes/predefined/spheres.py +4 -11
  31. capytaine/meshes/quality.py +118 -407
  32. capytaine/meshes/surface_integrals.py +48 -29
  33. capytaine/meshes/symmetric_meshes.py +641 -0
  34. capytaine/meshes/visualization.py +353 -0
  35. capytaine/post_pro/free_surfaces.py +1 -4
  36. capytaine/post_pro/kochin.py +10 -10
  37. capytaine/tools/block_circulant_matrices.py +275 -0
  38. capytaine/tools/lists_of_points.py +2 -2
  39. capytaine/tools/memory_monitor.py +45 -0
  40. capytaine/tools/symbolic_multiplication.py +31 -5
  41. capytaine/tools/timer.py +68 -42
  42. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +8 -14
  43. capytaine-3.0.0a1.dist-info/RECORD +65 -0
  44. capytaine-3.0.0a1.dist-info/WHEEL +6 -0
  45. capytaine/bodies/predefined/__init__.py +0 -6
  46. capytaine/bodies/predefined/cylinders.py +0 -151
  47. capytaine/bodies/predefined/rectangles.py +0 -111
  48. capytaine/bodies/predefined/spheres.py +0 -70
  49. capytaine/green_functions/FinGreen3D/.gitignore +0 -1
  50. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
  51. capytaine/green_functions/FinGreen3D/LICENSE +0 -165
  52. capytaine/green_functions/FinGreen3D/Makefile +0 -16
  53. capytaine/green_functions/FinGreen3D/README.md +0 -24
  54. capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
  55. capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
  56. capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
  57. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
  58. capytaine/green_functions/LiangWuNoblesse/Makefile +0 -18
  59. capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
  60. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
  61. capytaine/green_functions/libs/__init__.py +0 -0
  62. capytaine/io/mesh_loaders.py +0 -1086
  63. capytaine/io/mesh_writers.py +0 -692
  64. capytaine/io/meshio.py +0 -38
  65. capytaine/matrices/__init__.py +0 -16
  66. capytaine/matrices/block.py +0 -592
  67. capytaine/matrices/block_toeplitz.py +0 -325
  68. capytaine/matrices/builders.py +0 -89
  69. capytaine/matrices/linear_solvers.py +0 -232
  70. capytaine/matrices/low_rank.py +0 -395
  71. capytaine/meshes/clipper.py +0 -465
  72. capytaine/meshes/collections.py +0 -334
  73. capytaine/meshes/mesh_like_protocol.py +0 -37
  74. capytaine/meshes/properties.py +0 -276
  75. capytaine/meshes/quadratures.py +0 -80
  76. capytaine/meshes/symmetric.py +0 -392
  77. capytaine/tools/lru_cache.py +0 -49
  78. capytaine/ui/vtk/__init__.py +0 -3
  79. capytaine/ui/vtk/animation.py +0 -329
  80. capytaine/ui/vtk/body_viewer.py +0 -28
  81. capytaine/ui/vtk/helpers.py +0 -82
  82. capytaine/ui/vtk/mesh_viewer.py +0 -461
  83. capytaine-2.3.dist-info/RECORD +0 -92
  84. capytaine-2.3.dist-info/WHEEL +0 -4
  85. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
  86. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
@@ -1,33 +1,35 @@
1
1
  """Floating bodies to be used in radiation-diffraction problems."""
2
2
  # Copyright (C) 2017-2024 Matthieu Ancellin
3
3
  # See LICENSE file at <https://github.com/capytaine/capytaine>
4
+ from __future__ import annotations
4
5
 
5
6
  import logging
6
7
  import copy
7
- from itertools import chain, accumulate, zip_longest
8
- from functools import cached_property, lru_cache
8
+ from functools import lru_cache
9
+ from typing import Literal
9
10
 
10
11
  import numpy as np
11
12
  import xarray as xr
12
13
 
13
- from capytaine.meshes.mesh_like_protocol import MeshLike
14
- from capytaine.meshes.collections import CollectionOfMeshes
15
- from capytaine.meshes.geometry import Abstract3DObject, ClippableMixin, Plane, inplace_transformation
16
- from capytaine.meshes.properties import connected_components, connected_components_of_waterline
14
+ from capytaine.meshes.abstract_meshes import AbstractMesh
17
15
  from capytaine.meshes.meshes import Mesh
18
- from capytaine.meshes.symmetric import build_regular_array_of_meshes
19
- from capytaine.bodies.dofs import RigidBodyDofsPlaceholder
20
-
21
- from capytaine.tools.optional_imports import silently_import_optional_dependency
22
- meshio = silently_import_optional_dependency("meshio")
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
23
28
 
24
29
  LOG = logging.getLogger(__name__)
25
30
 
26
- TRANSLATION_DOFS_DIRECTIONS = {"surge": (1, 0, 0), "sway": (0, 1, 0), "heave": (0, 0, 1)}
27
- ROTATION_DOFS_AXIS = {"roll": (1, 0, 0), "pitch": (0, 1, 0), "yaw": (0, 0, 1)}
28
-
29
31
 
30
- class FloatingBody(ClippableMixin, Abstract3DObject):
32
+ class FloatingBody(_HydrostaticsMixin):
31
33
  """A floating body described as a mesh and some degrees of freedom.
32
34
 
33
35
  The mesh structure is stored as a Mesh from capytaine.mesh.mesh or a
@@ -40,55 +42,65 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
40
42
 
41
43
  Parameters
42
44
  ----------
43
- mesh : MeshLike, optional
45
+ mesh : AbstractMesh, optional
44
46
  the mesh describing the geometry of the hull of the floating body.
45
47
  If none is given, a empty one is created.
46
- lid_mesh : MeshLike or None, optional
47
- a mesh of an internal lid for irregular frequencies removal.
48
- Unlike the mesh of the hull, no dof is defined on the lid_mesh.
49
- If none is given, none is used when solving the Boundary Integral Equation.
50
48
  dofs : dict, optional
51
49
  the degrees of freedom of the body.
52
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.
53
58
  mass : float or None, optional
54
59
  the mass of the body in kilograms.
55
60
  Required only for some hydrostatics computation.
56
61
  If None, the mass is implicitly assumed to be the mass of displaced water.
57
- center_of_mass: 3-element array, optional
58
- the position of the center of mass.
59
- Required only for some hydrostatics computation.
60
62
  name : str, optional
61
63
  a name for the body.
62
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
63
72
  """
64
73
 
65
- def __init__(self, mesh=None, dofs=None, mass=None, center_of_mass=None, name=None, *, lid_mesh=None):
74
+ def __init__(self, mesh=None, dofs=None, *, lid_mesh=None, center_of_mass=None, mass=None, name=None):
66
75
  if mesh is None:
67
76
  self.mesh = Mesh(name="dummy_mesh")
68
-
69
- elif meshio is not None and isinstance(mesh, meshio._mesh.Mesh):
70
- from capytaine.io.meshio import load_from_meshio
71
- self.mesh = load_from_meshio(mesh)
72
-
73
- elif isinstance(mesh, MeshLike):
77
+ elif isinstance(mesh, AbstractMesh):
74
78
  self.mesh = mesh
75
-
76
79
  else:
77
80
  raise TypeError("Unrecognized `mesh` object passed to the FloatingBody constructor.")
78
81
 
79
- if lid_mesh is not None:
82
+ if lid_mesh is None:
83
+ self.lid_mesh = None
84
+ elif isinstance(mesh, AbstractMesh):
80
85
  if lid_mesh.nb_faces == 0:
81
86
  LOG.warning("Lid mesh %s provided for body initialization is empty. The lid mesh is ignored.", lid_mesh)
82
87
  self.lid_mesh = None
83
88
  else:
84
89
  self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
85
90
  else:
86
- self.lid_mesh = None
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]
87
99
 
88
100
  if name is None and mesh is None:
89
101
  self.name = "dummy_body"
90
102
  elif name is None:
91
- if hasattr(self.mesh, "name"):
103
+ if hasattr(self.mesh, "name") and self.mesh.name is not None:
92
104
  self.name = self.mesh.name
93
105
  else:
94
106
  self.name = "anonymous_body"
@@ -101,27 +113,24 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
101
113
  else:
102
114
  self.center_of_mass = None
103
115
 
104
- if hasattr(self.mesh, "heal_mesh") and self.mesh.nb_vertices > 0 and self.mesh.nb_faces > 0:
105
- self.mesh.heal_mesh()
106
-
107
116
  if dofs is None:
108
117
  self.dofs = {}
109
- elif isinstance(dofs, RigidBodyDofsPlaceholder):
110
- if dofs.rotation_center is not None:
111
- self.rotation_center = np.asarray(dofs.rotation_center, dtype=float)
112
- self.dofs = {}
113
- self.add_all_rigid_body_dofs()
114
118
  else:
115
- self.dofs = dofs
116
-
117
- LOG.info(f"New floating body: {self.__str__()}.")
119
+ self.dofs = {
120
+ k: v if isinstance(v, AbstractDof) else np.asarray(v)
121
+ for k, v in dofs.items()
122
+ }
118
123
 
119
124
  self._check_dofs_shape_consistency()
120
125
 
126
+ LOG.debug(f"New floating body: {self.__str__()}.")
127
+
121
128
  @staticmethod
122
129
  def from_meshio(mesh, name=None) -> 'FloatingBody':
123
130
  """Create a FloatingBody from a meshio mesh object.
124
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(...), ...)")
125
134
  from capytaine.io.meshio import load_from_meshio
126
135
  return FloatingBody(mesh=load_from_meshio(mesh, name), name=name)
127
136
 
@@ -129,8 +138,11 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
129
138
  def from_file(filename: str, file_format=None, name=None) -> 'FloatingBody':
130
139
  """Create a FloatingBody from a mesh file using meshmagick.
131
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(...), ...)")
132
143
  from capytaine.io.mesh_loaders import load_mesh
133
- if name is None: name = filename
144
+ if name is None:
145
+ name = filename
134
146
  mesh = load_mesh(filename, file_format, name=f"{name}_mesh")
135
147
  return FloatingBody(mesh, name=name)
136
148
 
@@ -138,13 +150,6 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
138
150
  """Arbitrary order. The point is to sort together the problems involving the same body."""
139
151
  return self.name < other.name
140
152
 
141
- @cached_property
142
- def mesh_including_lid(self):
143
- if self.lid_mesh is not None:
144
- return self.mesh.join_meshes(self.lid_mesh)
145
- else:
146
- return self.mesh
147
-
148
153
  ##########
149
154
  # Dofs #
150
155
  ##########
@@ -154,7 +159,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
154
159
  """Number of degrees of freedom."""
155
160
  return len(self.dofs)
156
161
 
157
- def add_translation_dof(self, direction=None, name=None, amplitude=1.0) -> None:
162
+ def add_translation_dof(self, direction=None, name=None) -> None:
158
163
  """Add a new translation dof (in place).
159
164
  If no direction is given, the code tries to infer it from the name.
160
165
 
@@ -164,672 +169,116 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
164
169
  the direction of the translation
165
170
  name : str, optional
166
171
  a name for the degree of freedom
167
- amplitude : float, optional
168
- amplitude of the dof (default: 1.0 m/s)
169
172
  """
170
- if direction is None:
171
- if name is not None and name.lower() in TRANSLATION_DOFS_DIRECTIONS:
172
- direction = TRANSLATION_DOFS_DIRECTIONS[name.lower()]
173
- else:
174
- raise ValueError("A direction needs to be specified for the dof.")
175
-
176
173
  if name is None:
177
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
+ )
178
181
 
179
- direction = np.asarray(direction)
180
- assert direction.shape == (3,)
181
-
182
- motion = np.empty((self.mesh.nb_faces, 3))
183
- motion[:, :] = direction
184
- self.dofs[name] = amplitude * motion
185
-
186
- def add_rotation_dof(self, axis=None, name=None, amplitude=1.0) -> None:
182
+ def add_rotation_dof(self, rotation_center=None, direction=None, name=None) -> None:
187
183
  """Add a new rotation dof (in place).
188
184
  If no axis is given, the code tries to infer it from the name.
189
185
 
190
186
  Parameters
191
187
  ----------
192
- axis: Axis, optional
193
- the axis of the rotation
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
194
192
  name : str, optional
195
193
  a name for the degree of freedom
196
- amplitude : float, optional
197
- amplitude of the dof (default: 1.0)
198
194
  """
199
- if axis is None:
200
- if name is not None and name.lower() in ROTATION_DOFS_AXIS:
201
- axis_direction = ROTATION_DOFS_AXIS[name.lower()]
202
- for point_attr in ('rotation_center', 'center_of_mass', 'geometric_center'):
203
- if hasattr(self, point_attr) and getattr(self, point_attr) is not None:
204
- axis_point = getattr(self, point_attr)
205
- LOG.info(f"The rotation dof {name} has been initialized around the point: "
206
- f"{self.__short_str__()}.{point_attr} = {getattr(self, point_attr)}")
207
- break
208
- else:
209
- axis_point = np.array([0, 0, 0])
210
- LOG.warning(f"The rotation dof {name} has been initialized "
211
- f"around the origin of the domain (0, 0, 0).")
212
- else:
213
- raise ValueError("A direction needs to be specified for the dof.")
214
- else:
215
- axis_point = axis.point
216
- axis_direction = axis.vector
217
-
218
195
  if name is None:
219
196
  name = f"dof_{self.nb_dofs}_rotation"
220
-
221
- if self.mesh.nb_faces == 0:
222
- self.dofs[name] = np.empty((self.mesh.nb_faces, 3))
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)]
223
206
  else:
224
- motion = np.cross(axis_point - self.mesh.faces_centers, axis_direction)
225
- self.dofs[name] = amplitude * motion
207
+ self.dofs[name] = RotationDof(
208
+ rotation_center=rotation_center,
209
+ direction=direction,
210
+ )
226
211
 
227
- def add_all_rigid_body_dofs(self) -> None:
212
+ def add_all_rigid_body_dofs(self, rotation_center=None) -> None:
228
213
  """Add the six degrees of freedom of rigid bodies (in place)."""
229
214
  self.add_translation_dof(name="Surge")
230
215
  self.add_translation_dof(name="Sway")
231
216
  self.add_translation_dof(name="Heave")
232
- self.add_rotation_dof(name="Roll")
233
- self.add_rotation_dof(name="Pitch")
234
- self.add_rotation_dof(name="Yaw")
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")
235
220
 
236
221
  def integrate_pressure(self, pressure):
237
222
  forces = {}
238
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]
239
228
  # Scalar product on each face:
240
- normal_dof_amplitude_on_face = - np.sum(self.dofs[dof_name] * self.mesh.faces_normals, axis=1)
229
+ normal_dof_amplitude_on_face = - np.sum(dof * self.mesh.faces_normals, axis=1)
241
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.
242
231
  # Sum over all faces:
243
232
  forces[dof_name] = np.sum(pressure * normal_dof_amplitude_on_face * self.mesh.faces_areas)
244
233
  return forces
245
234
 
246
- @inplace_transformation
247
- def keep_only_dofs(self, dofs):
248
- for dof in list(self.dofs.keys()):
249
- if dof not in dofs:
250
- del self.dofs[dof]
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)
251
245
 
252
246
  if hasattr(self, 'inertia_matrix'):
253
- self.inertia_matrix = self.inertia_matrix.sel(radiating_dof=dofs, influenced_dof=dofs)
247
+ body.inertia_matrix = self.inertia_matrix.sel(radiating_dof=dofs, influenced_dof=dofs)
254
248
  if hasattr(self, 'hydrostatic_stiffness'):
255
- self.hydrostatic_stiffness = self.hydrostatic_stiffness.sel(radiating_dof=dofs, influenced_dof=dofs)
249
+ body.hydrostatic_stiffness = self.hydrostatic_stiffness.sel(radiating_dof=dofs, influenced_dof=dofs)
256
250
 
257
- return self
251
+ return body
258
252
 
259
253
  def add_dofs_labels_to_vector(self, vector):
260
254
  """Helper function turning a bare vector into a vector labelled by the name of the dofs of the body,
261
255
  to be used for instance for the computation of RAO."""
262
- return xr.DataArray(data=np.asarray(vector), dims=['influenced_dof'],
263
- coords={'influenced_dof': list(self.dofs)},
264
- )
256
+ return add_dofs_labels_to_vector(self.dofs.keys(), vector)
265
257
 
266
258
  def add_dofs_labels_to_matrix(self, matrix):
267
259
  """Helper function turning a bare matrix into a matrix labelled by the name of the dofs of the body,
268
260
  to be used for instance for the computation of RAO."""
269
- return xr.DataArray(data=np.asarray(matrix), dims=['influenced_dof', 'radiating_dof'],
270
- coords={'influenced_dof': list(self.dofs), 'radiating_dof': list(self.dofs)},
271
- )
261
+ return add_dofs_labels_to_matrix(self.dofs.keys(), matrix)
272
262
 
273
263
  def _check_dofs_shape_consistency(self):
274
264
  for dof_name, dof in self.dofs.items():
275
- if np.array(dof).shape != (self.mesh.nb_faces, 3):
276
- raise ValueError(f"The array defining the dof {dof_name} of body {self.name} does not have the expected shape.\n"
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"
277
268
  f"Expected shape: ({self.mesh.nb_faces}, 3)\n"
278
269
  f" Actual shape: {dof.shape}")
279
270
 
280
271
 
281
- ###################
282
- # Hydrostatics #
283
- ###################
284
-
285
- def surface_integral(self, data, **kwargs):
286
- """Returns integral of given data along wet surface area."""
287
- return self.mesh.surface_integral(data, **kwargs)
288
-
289
- def waterplane_integral(self, data, **kwargs):
290
- """Returns integral of given data along water plane area."""
291
- return self.mesh.waterplane_integral(data, **kwargs)
292
-
293
- @property
294
- def wet_surface_area(self):
295
- """Returns wet surface area."""
296
- return self.mesh.wet_surface_area
297
-
298
- @property
299
- def volumes(self):
300
- """Returns volumes using x, y, z components of the FloatingBody."""
301
- return self.mesh.volumes
302
-
303
- @property
304
- def volume(self):
305
- """Returns volume of the FloatingBody."""
306
- return self.mesh.volume
307
-
308
- def disp_mass(self, *, rho=1000.0):
309
- return self.mesh.disp_mass(rho=rho)
310
-
311
- @property
312
- def center_of_buoyancy(self):
313
- """Returns center of buoyancy of the FloatingBody."""
314
- return self.mesh.center_of_buoyancy
315
-
316
- @property
317
- def waterplane_area(self):
318
- """Returns water plane area of the FloatingBody."""
319
- return self.mesh.waterplane_area
320
-
321
- @property
322
- def waterplane_center(self):
323
- """Returns water plane center of the FloatingBody.
324
-
325
- Note: Returns None if the FloatingBody is full submerged.
326
- """
327
- return self.mesh.waterplane_center
328
-
329
- @property
330
- def transversal_metacentric_radius(self):
331
- """Returns transversal metacentric radius of the mesh."""
332
- inertia_moment = -self.waterplane_integral(self.mesh.faces_centers[:,1]**2)
333
- return inertia_moment / self.volume
334
-
335
- @property
336
- def longitudinal_metacentric_radius(self):
337
- """Returns longitudinal metacentric radius of the mesh."""
338
- inertia_moment = -self.waterplane_integral(self.mesh.faces_centers[:,0]**2)
339
- return inertia_moment / self.volume
340
-
341
- @property
342
- def transversal_metacentric_height(self):
343
- """Returns transversal metacentric height of the mesh."""
344
- gb = self.center_of_mass - self.center_of_buoyancy
345
- return self.transversal_metacentric_radius - gb[2]
346
-
347
- @property
348
- def longitudinal_metacentric_height(self):
349
- """Returns longitudinal metacentric height of the mesh."""
350
- gb = self.center_of_mass - self.center_of_buoyancy
351
- return self.longitudinal_metacentric_radius - gb[2]
352
-
353
- def dof_normals(self, dof):
354
- """Returns dot product of the surface face normals and DOF"""
355
- return np.sum(self.mesh.faces_normals * dof, axis=1)
356
-
357
- def _infer_rotation_center(self):
358
- """Hacky way to infer the point around which the rotation dofs are defined.
359
- (Assuming all three rotation dofs are defined around the same point).
360
- In the future, should be replaced by something more robust.
361
- """
362
- if hasattr(self, "rotation_center"):
363
- return np.asarray(self.rotation_center)
364
-
365
- else:
366
- try:
367
- xc1 = self.dofs["Pitch"][:, 2] + self.mesh.faces_centers[:, 0]
368
- xc2 = -self.dofs["Yaw"][:, 1] + self.mesh.faces_centers[:, 0]
369
- yc1 = self.dofs["Yaw"][:, 0] + self.mesh.faces_centers[:, 1]
370
- yc2 = -self.dofs["Roll"][:, 2] + self.mesh.faces_centers[:, 1]
371
- zc1 = -self.dofs["Pitch"][:, 0] + self.mesh.faces_centers[:, 2]
372
- zc2 = self.dofs["Roll"][:, 1] + self.mesh.faces_centers[:, 2]
373
-
374
- # All items should be identical in a given vector
375
- assert np.isclose(xc1, xc1[0]).all()
376
- assert np.isclose(yc1, yc1[0]).all()
377
- assert np.isclose(zc1, zc1[0]).all()
378
-
379
- # Both vector should be identical
380
- assert np.allclose(xc1, xc2)
381
- assert np.allclose(yc1, yc2)
382
- assert np.allclose(zc1, zc2)
383
-
384
- return np.array([xc1[0], yc1[0], zc1[0]])
385
-
386
- except Exception as e:
387
- raise ValueError(
388
- f"Failed to infer the rotation center of {self.name} to compute rigid body hydrostatics.\n"
389
- f"Possible fix: add a `rotation_center` attribute to {self.name}.\n"
390
- "Note that rigid body hydrostatic methods currently assume that the three rotation dofs have the same rotation center."
391
- ) from e
392
-
393
- def each_hydrostatic_stiffness(self, influenced_dof_name, radiating_dof_name, *,
394
- influenced_dof_div=0.0, rho=1000.0, g=9.81):
395
- r"""
396
- Return the hydrostatic stiffness for a pair of DOFs.
397
-
398
- :math:`C_{ij} = \rho g\iint_S (\hat{n} \cdot V_j) (w_i + z D_i) dS`
399
-
400
- where :math:`\hat{n}` is surface normal,
401
-
402
- :math:`V_i = u_i \hat{n}_x + v_i \hat{n}_y + w_i \hat{n}_z` is DOF vector and
403
-
404
- :math:`D_i = \nabla \cdot V_i` is the divergence of the DOF.
405
-
406
- Parameters
407
- ----------
408
- influenced_dof_name : str
409
- Name of influenced DOF vector of the FloatingBody
410
- radiating_dof_name: str
411
- Name of radiating DOF vector of the FloatingBody
412
- influenced_dof_div: np.ndarray (Face_count), optional
413
- Influenced DOF divergence of the FloatingBody, by default 0.0.
414
- rho: float, optional
415
- water density, by default 1000.0
416
- g: float, optional
417
- Gravity acceleration, by default 9.81
418
-
419
- Returns
420
- -------
421
- hs_ij: xarray.variable
422
- hydrostatic_stiffness of ith DOF and jth DOF.
423
-
424
- Note
425
- ----
426
- This function computes the hydrostatic stiffness assuming :math:`D_{i} = 0`.
427
- If :math:`D_i \neq 0`, input the divergence interpolated to face centers.
428
-
429
- General integral equations are used for the rigid body modes and
430
- Neumann (1994) method is used for flexible modes.
431
-
432
- References
433
- ----------
434
- Newman, John Nicholas. "Wave effects on deformable bodies."Applied ocean
435
- research" 16.1 (1994): 47-59.
436
- http://resolver.tudelft.nl/uuid:0adff84c-43c7-43aa-8cd8-d4c44240bed8
437
-
438
- """
439
- # Newman (1994) formula is not 'complete' as recovering the rigid body
440
- # terms is not possible. https://doi.org/10.1115/1.3058702.
441
-
442
- # Alternative is to use the general equation of hydrostatic and
443
- # restoring coefficient for rigid modes and use Newman equation for elastic
444
- # modes.
445
-
446
- rigid_dof_names = ("Surge", "Sway", "Heave", "Roll", "Pitch", "Yaw")
447
- dof_pair = (influenced_dof_name, radiating_dof_name)
448
-
449
- if set(dof_pair).issubset(set(rigid_dof_names)):
450
- if self.center_of_mass is None:
451
- raise ValueError(f"Trying to compute rigid-body hydrostatic stiffness for {self.name}, but no center of mass has been defined.\n"
452
- f"Suggested solution: define a `center_of_mass` attribute for the FloatingBody {self.name}.")
453
- mass = self.disp_mass(rho=rho) if self.mass is None else self.mass
454
- xc, yc, zc = self._infer_rotation_center()
455
-
456
- if dof_pair == ("Heave", "Heave"):
457
- norm_hs_stiff = self.waterplane_area
458
- elif dof_pair in [("Heave", "Roll"), ("Roll", "Heave")]:
459
- norm_hs_stiff = -self.waterplane_integral(self.mesh.faces_centers[:,1] - yc)
460
- elif dof_pair in [("Heave", "Pitch"), ("Pitch", "Heave")]:
461
- norm_hs_stiff = self.waterplane_integral(self.mesh.faces_centers[:,0] - xc)
462
- elif dof_pair == ("Roll", "Roll"):
463
- norm_hs_stiff = (
464
- -self.waterplane_integral((self.mesh.faces_centers[:,1] - yc)**2)
465
- + self.volume*(self.center_of_buoyancy[2] - zc) - mass/rho*(self.center_of_mass[2] - zc)
466
- )
467
- elif dof_pair in [("Roll", "Pitch"), ("Pitch", "Roll")]:
468
- norm_hs_stiff = self.waterplane_integral((self.mesh.faces_centers[:,0] - xc)
469
- * (self.mesh.faces_centers[:,1] - yc))
470
- elif dof_pair == ("Roll", "Yaw"):
471
- norm_hs_stiff = - self.volume*(self.center_of_buoyancy[0] - xc) + mass/rho*(self.center_of_mass[0] - xc)
472
- elif dof_pair == ("Pitch", "Pitch"):
473
- norm_hs_stiff = (
474
- -self.waterplane_integral((self.mesh.faces_centers[:,0] - xc)**2)
475
- + self.volume*(self.center_of_buoyancy[2] - zc) - mass/rho*(self.center_of_mass[2] - zc)
476
- )
477
- elif dof_pair == ("Pitch", "Yaw"):
478
- norm_hs_stiff = - self.volume*(self.center_of_buoyancy[1] - yc) + mass/rho*(self.center_of_mass[1] - yc)
479
- else:
480
- norm_hs_stiff = 0.0
481
- else:
482
- if self.mass is not None and not np.isclose(self.mass, self.disp_mass(rho=rho), rtol=1e-4):
483
- raise NotImplementedError(
484
- f"Trying to compute the hydrostatic stiffness for dofs {radiating_dof_name} and {influenced_dof_name}"
485
- f"of body {self.name}, which is not neutrally buoyant (mass={self.mass}, disp_mass={self.disp_mass(rho=rho)}).\n"
486
- f"This case has not been implemented in Capytaine. You need either a single rigid body or a neutrally buoyant body."
487
- )
488
-
489
- # Newman (1994) formula for flexible DOFs
490
- influenced_dof = np.array(self.dofs[influenced_dof_name])
491
- radiating_dof = np.array(self.dofs[radiating_dof_name])
492
- influenced_dof_div_array = np.array(influenced_dof_div)
493
-
494
- radiating_dof_normal = self.dof_normals(radiating_dof)
495
- z_influenced_dof_div = influenced_dof[:,2] + self.mesh.faces_centers[:,2] * influenced_dof_div_array
496
- norm_hs_stiff = self.surface_integral( -radiating_dof_normal * z_influenced_dof_div)
497
-
498
- hs_stiff = rho * g * norm_hs_stiff
499
-
500
- return xr.DataArray([[hs_stiff]],
501
- dims=['influenced_dof', 'radiating_dof'],
502
- coords={'influenced_dof': [influenced_dof_name],
503
- 'radiating_dof': [radiating_dof_name]},
504
- name="hydrostatic_stiffness"
505
- )
506
-
507
- def compute_hydrostatic_stiffness(self, *, divergence=None, rho=1000.0, g=9.81):
508
- r"""
509
- Compute hydrostatic stiffness matrix for all DOFs of the body.
510
-
511
- :math:`C_{ij} = \rho g\iint_S (\hat{n} \cdot V_j) (w_i + z D_i) dS`
512
-
513
- where :math:`\hat{n}` is surface normal,
514
-
515
- :math:`V_i = u_i \hat{n}_x + v_i \hat{n}_y + w_i \hat{n}_z` is DOF vector and
516
-
517
- :math:`D_i = \nabla \cdot V_i` is the divergence of the DOF.
518
-
519
- Parameters
520
- ----------
521
- divergence : dict mapping a dof name to an array of shape (nb_faces) or
522
- xarray.DataArray of shape (nb_dofs × nb_faces), optional
523
- Divergence of the DOFs, by default None
524
- rho : float, optional
525
- Water density, by default 1000.0
526
- g: float, optional
527
- Gravity acceleration, by default 9.81
528
-
529
- Returns
530
- -------
531
- xr.DataArray
532
- Matrix of hydrostatic stiffness
533
-
534
- Note
535
- ----
536
- This function computes the hydrostatic stiffness assuming :math:`D_{i} = 0`.
537
- If :math:`D_i \neq 0`, input the divergence interpolated to face centers.
538
-
539
- General integral equations are used for the rigid body modes and
540
- Neumann (1994) method is used for flexible modes.
541
-
542
- References
543
- ----------
544
- Newman, John Nicholas. "Wave effects on deformable bodies."Applied ocean
545
- research" 16.1 (1994): 47-59.
546
- http://resolver.tudelft.nl/uuid:0adff84c-43c7-43aa-8cd8-d4c44240bed8
547
-
548
- """
549
- if len(self.dofs) == 0:
550
- raise AttributeError("Cannot compute hydrostatics stiffness on {} since no dof has been defined.".format(self.name))
551
-
552
- def divergence_dof(influenced_dof):
553
- if influenced_dof.lower() in [*TRANSLATION_DOFS_DIRECTIONS, *ROTATION_DOFS_AXIS]:
554
- return 0.0 # Dummy value that is not actually used afterwards.
555
- elif divergence is None:
556
- return 0.0
557
- elif isinstance(divergence, dict) and influenced_dof in divergence.keys():
558
- return divergence[influenced_dof]
559
- elif isinstance(divergence, xr.DataArray) and influenced_dof in divergence.coords["influenced_dof"]:
560
- return divergence.sel(influenced_dof=influenced_dof).values
561
- else:
562
- LOG.warning("Computing hydrostatic stiffness without the divergence of {}".format(influenced_dof))
563
- return 0.0
564
-
565
- hs_set = xr.merge([
566
- self.each_hydrostatic_stiffness(
567
- influenced_dof_name, radiating_dof_name,
568
- influenced_dof_div = divergence_dof(influenced_dof_name),
569
- rho=rho, g=g
570
- )
571
- for radiating_dof_name in self.dofs
572
- for influenced_dof_name in self.dofs
573
- ])
574
-
575
- # Reorder dofs
576
- K = hs_set.hydrostatic_stiffness.sel(influenced_dof=list(self.dofs.keys()), radiating_dof=list(self.dofs.keys()))
577
- return K
578
-
579
- def compute_rigid_body_inertia(self, *, rho=1000.0, output_type="body_dofs"):
580
- """
581
- Inertia Mass matrix of the body for 6 rigid DOFs.
582
-
583
- Parameters
584
- ----------
585
- rho : float, optional
586
- Density of water, by default 1000.0
587
- output_type : {"body_dofs", "rigid_dofs", "all_dofs"}
588
- Type of DOFs for mass mat output, by default "body_dofs".
589
-
590
- Returns
591
- -------
592
- xarray.DataArray
593
- Inertia matrix
594
-
595
- Raises
596
- ------
597
- ValueError
598
- If output_type is not in {"body_dofs", "rigid_dofs", "all_dofs"}.
599
- """
600
- if self.center_of_mass is None:
601
- raise ValueError(f"Trying to compute rigid-body inertia matrix for {self.name}, but no center of mass has been defined.\n"
602
- f"Suggested solution: define a `center_of_mass` attribute for the FloatingBody {self.name}.")
603
-
604
- rc = self._infer_rotation_center()
605
- fcs = (self.mesh.faces_centers - rc).T
606
- combinations = np.array([fcs[0]**2, fcs[1]**2, fcs[2]**2, fcs[0]*fcs[1],
607
- fcs[1]*fcs[2], fcs[2]*fcs[0]])
608
- integrals = np.array([
609
- [np.sum(normal_i * fcs[axis] * combination * self.mesh.faces_areas)
610
- for combination in combinations]
611
- for axis, normal_i in enumerate(self.mesh.faces_normals.T)])
612
-
613
-
614
- inertias = np.array([
615
- (integrals[0,1] + integrals[0,2] + integrals[1,1]/3
616
- + integrals[1,2] + integrals[2,1] + integrals[2,2]/3)/3,
617
- (integrals[0,0]/3 + integrals[0,2] + integrals[1,0]
618
- + integrals[1,2] + integrals[2,0] + integrals[2,2]/3)/3,
619
- (integrals[0,0]/3 + integrals[0,1] + integrals[1,0]
620
- + integrals[1,1]/3 + integrals[2,0] + integrals[2,1] )/3,
621
- integrals[2,3],
622
- integrals[0,4],
623
- integrals[1,5]
624
- ])
625
-
626
- cog = self.center_of_mass - rc
627
- volume = self.volume
628
- volumic_inertia_matrix = np.array([
629
- [ volume , 0 , 0 ,
630
- 0 , volume*cog[2] , -volume*cog[1] ],
631
- [ 0 , volume , 0 ,
632
- -volume*cog[2] , 0 , volume*cog[0] ],
633
- [ 0 , 0 , volume ,
634
- volume*cog[1] , -volume*cog[0] , 0 ] ,
635
- [ 0 , -volume*cog[2] , volume*cog[1] ,
636
- inertias[0] , -inertias[3] , -inertias[5] ],
637
- [ volume*cog[2] , 0 , -volume*cog[0] ,
638
- -inertias[3] , inertias[1] , -inertias[4] ],
639
- [-volume*cog[1] , volume*cog[0] , 0 ,
640
- -inertias[5] , -inertias[4] , inertias[2] ],
641
- ])
642
-
643
- density = rho if self.mass is None else self.mass/volume
644
- inertia_matrix = density * volumic_inertia_matrix
645
-
646
- # Rigid DOFs
647
- rigid_dof_names = ["Surge", "Sway", "Heave", "Roll", "Pitch", "Yaw"]
648
- rigid_inertia_matrix_xr = xr.DataArray(data=np.asarray(inertia_matrix),
649
- dims=['influenced_dof', 'radiating_dof'],
650
- coords={'influenced_dof': rigid_dof_names,
651
- 'radiating_dof': rigid_dof_names},
652
- name="inertia_matrix")
653
-
654
- # Body DOFs (Default as np.nan)
655
- body_dof_names = list(self.dofs)
656
- body_dof_count = len(body_dof_names)
657
- other_dofs_inertia_matrix_xr = xr.DataArray(np.nan * np.zeros([body_dof_count, body_dof_count]),
658
- dims=['influenced_dof', 'radiating_dof'],
659
- coords={'influenced_dof': body_dof_names,
660
- 'radiating_dof': body_dof_names},
661
- name="inertia_matrix")
662
-
663
- total_mass_xr = xr.merge([rigid_inertia_matrix_xr, other_dofs_inertia_matrix_xr], compat="override").inertia_matrix
664
-
665
- non_rigid_dofs = set(body_dof_names) - set(rigid_dof_names)
666
-
667
- if output_type == "body_dofs":
668
- if len(non_rigid_dofs) > 0:
669
- LOG.warning(f"Non-rigid dofs {non_rigid_dofs} detected: their \
670
- inertia coefficients are assigned as NaN.")
671
-
672
- inertia_matrix_xr = total_mass_xr.sel(influenced_dof=body_dof_names,
673
- radiating_dof=body_dof_names)
674
- elif output_type == "rigid_dofs":
675
- inertia_matrix_xr = total_mass_xr.sel(influenced_dof=rigid_dof_names,
676
- radiating_dof=rigid_dof_names)
677
- elif output_type == "all_dofs":
678
- if len(non_rigid_dofs) > 0:
679
- LOG.warning("Non-rigid dofs: {non_rigid_dofs} are detected and \
680
- respective inertia coefficients are assigned as NaN.")
681
-
682
- inertia_matrix_xr = total_mass_xr
683
- else:
684
- raise ValueError(f"output_type should be either 'body_dofs', \
685
- 'all_dofs' or 'rigid_dofs'. Given output_type = '{output_type}'.")
686
-
687
- return inertia_matrix_xr
688
-
689
-
690
- def compute_hydrostatics(self, *, rho=1000.0, g=9.81, divergence=None):
691
- """Compute hydrostatics of the FloatingBody.
692
-
693
- Parameters
694
- ----------
695
- rho : float, optional
696
- Density of Water. The default is 1000.
697
- g: float, optional
698
- Gravity acceleration. The default is 9.81.
699
- divergence : np.ndarray, optional
700
- Divergence of the DOFs.
701
-
702
- Returns
703
- -------
704
- hydrostatics : dict
705
- All hydrostatics values of the FloatingBody.
706
- """
707
- if self.center_of_mass is None:
708
- raise ValueError(f"Trying to compute hydrostatics for {self.name}, but no center of mass has been defined.\n"
709
- f"Suggested solution: define a `center_of_mass` attribute for the FloatingBody {self.name}.")
710
-
711
- immersed_self = self.immersed_part()
712
-
713
- full_mesh_vertices = self.mesh.vertices
714
- coord_max = full_mesh_vertices.max(axis=0)
715
- coord_min = full_mesh_vertices.min(axis=0)
716
- full_length, full_breadth, depth = full_mesh_vertices.max(axis=0) - full_mesh_vertices.min(axis=0)
717
-
718
- vertices = immersed_self.mesh.vertices
719
- sub_length, sub_breadth, _ = vertices.max(axis=0) - vertices.min(axis=0)
720
-
721
- if abs(immersed_self.waterplane_area) > 1e-10:
722
- water_plane_idx = np.isclose(vertices[:,2], 0.0)
723
- water_plane = vertices[water_plane_idx][:,:-1]
724
- wl_length, wl_breadth = water_plane.max(axis=0) - water_plane.min(axis=0)
725
- else:
726
- wl_length, wl_breadth = 0.0, 0.0
727
-
728
- hydrostatics = {}
729
- hydrostatics["g"] = g
730
- hydrostatics["rho"] = rho
731
- hydrostatics["center_of_mass"] = self.center_of_mass
732
-
733
- hydrostatics["wet_surface_area"] = immersed_self.wet_surface_area
734
- hydrostatics["disp_volumes"] = immersed_self.volumes
735
- hydrostatics["disp_volume"] = immersed_self.volume
736
- hydrostatics["disp_mass"] = immersed_self.disp_mass(rho=rho)
737
- hydrostatics["center_of_buoyancy"] = immersed_self.center_of_buoyancy
738
- hydrostatics["waterplane_center"] = np.append(immersed_self.waterplane_center, 0.0)
739
- hydrostatics["waterplane_area"] = immersed_self.waterplane_area
740
- hydrostatics["transversal_metacentric_radius"] = immersed_self.transversal_metacentric_radius
741
- hydrostatics["longitudinal_metacentric_radius"] = immersed_self.longitudinal_metacentric_radius
742
- hydrostatics["transversal_metacentric_height"] = immersed_self.transversal_metacentric_height
743
- hydrostatics["longitudinal_metacentric_height"] = immersed_self.longitudinal_metacentric_height
744
- self.hydrostatic_stiffness = hydrostatics["hydrostatic_stiffness"] = immersed_self.compute_hydrostatic_stiffness(
745
- divergence=divergence, rho=rho, g=g)
746
-
747
- hydrostatics["length_overall"] = full_length
748
- hydrostatics["breadth_overall"] = full_breadth
749
- hydrostatics["depth"] = depth
750
- hydrostatics["draught"] = np.abs(coord_min[2])
751
- hydrostatics["length_at_waterline"] = wl_length
752
- hydrostatics["breadth_at_waterline"] = wl_breadth
753
- hydrostatics["length_overall_submerged"] = sub_length
754
- hydrostatics["breadth_overall_submerged"] = sub_breadth
755
- if any(dof.lower() in {"surge", "sway", "heave", "roll", "pitch", "yaw"}
756
- for dof in self.dofs) > 0: # If there is at least one rigid body dof:
757
- self.inertia_matrix = hydrostatics["inertia_matrix"] = self.compute_rigid_body_inertia(rho=rho)
758
-
759
- return hydrostatics
760
-
761
-
762
272
  ###################
763
273
  # Transformations #
764
274
  ###################
765
275
 
766
- def __add__(self, body_to_add: 'FloatingBody') -> 'FloatingBody':
276
+ def __add__(self, body_to_add: 'FloatingBody'):
767
277
  return self.join_bodies(body_to_add)
768
278
 
769
- def join_bodies(*bodies, name=None) -> 'FloatingBody':
770
- if name is None:
771
- name = "+".join(body.name for body in bodies)
772
- meshes = CollectionOfMeshes(
773
- [body.mesh.copy() for body in bodies],
774
- name=f"{name}_mesh"
775
- )
776
- if all(body.lid_mesh is None for body in bodies):
777
- lid_meshes = None
778
- else:
779
- lid_meshes = CollectionOfMeshes(
780
- [body.lid_mesh.copy() for body in bodies if body.lid_mesh is not None],
781
- name=f"{name}_lid_mesh"
782
- )
783
- dofs = FloatingBody.combine_dofs(bodies)
784
-
785
- if all(body.mass is not None for body in bodies):
786
- new_mass = sum(body.mass for body in bodies)
787
- else:
788
- new_mass = None
789
-
790
- if (all(body.mass is not None for body in bodies)
791
- and all(body.center_of_mass is not None for body in bodies)):
792
- new_cog = sum(body.mass*np.asarray(body.center_of_mass) for body in bodies)/new_mass
793
- else:
794
- new_cog = None
795
-
796
- joined_bodies = FloatingBody(
797
- mesh=meshes, lid_mesh=lid_meshes, dofs=dofs,
798
- mass=new_mass, center_of_mass=new_cog, name=name
799
- )
800
-
801
- for matrix_name in ["inertia_matrix", "hydrostatic_stiffness"]:
802
- if all(hasattr(body, matrix_name) for body in bodies):
803
- from scipy.linalg import block_diag
804
- setattr(joined_bodies, matrix_name, joined_bodies.add_dofs_labels_to_matrix(
805
- block_diag(*[getattr(body, matrix_name) for body in bodies])
806
- ))
807
-
808
- return joined_bodies
809
-
810
- @staticmethod
811
- def combine_dofs(bodies) -> dict:
812
- """Combine the degrees of freedom of several bodies."""
813
- for body in bodies:
814
- body._check_dofs_shape_consistency()
815
- dofs = {}
816
- cum_nb_faces = accumulate(chain([0], (body.mesh.nb_faces for body in bodies)))
817
- total_nb_faces = sum(body.mesh.nb_faces for body in bodies)
818
- for body, nbf in zip(bodies, cum_nb_faces):
819
- # nbf is the cumulative number of faces of the previous subbodies,
820
- # that is the offset of the indices of the faces of the current body.
821
- for name, dof in body.dofs.items():
822
- new_dof = np.zeros((total_nb_faces, 3))
823
- new_dof[nbf:nbf+len(dof), :] = dof
824
- if '__' not in name:
825
- new_dof_name = '__'.join([body.name, name])
826
- else:
827
- # The body is probably a combination of bodies already.
828
- # So for the associativity of the + operation,
829
- # it is better to keep the same name.
830
- new_dof_name = name
831
- dofs[new_dof_name] = new_dof
832
- return dofs
279
+ def join_bodies(*bodies, name=None):
280
+ from capytaine.bodies.multibodies import Multibody
281
+ return Multibody(bodies, name=name)
833
282
 
834
283
  def copy(self, name=None) -> 'FloatingBody':
835
284
  """Return a deep copy of the body.
@@ -866,173 +315,185 @@ respective inertia coefficients are assigned as NaN.")
866
315
  """
867
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]))
868
317
  array = FloatingBody.join_bodies(*bodies)
869
- array.mesh = build_regular_array_of_meshes(self.mesh, distance, nb_bodies)
870
318
  array.name = f"array_of_{self.name}"
871
319
  return array
872
320
 
873
321
  def assemble_arbitrary_array(self, locations:np.ndarray):
874
-
875
322
  if not isinstance(locations, np.ndarray):
876
323
  raise TypeError('locations must be of type np.ndarray')
877
324
  assert locations.shape[1] == 2, 'locations must be of shape nx2, received {:}'.format(locations.shape)
878
325
 
879
326
  fb_list = []
880
327
  for idx, li in enumerate(locations):
881
- fb1 = self.copy()
882
- fb1.translate(np.append(li,0))
883
- fb1.name = 'arbitrary_array_body{:02d}'.format(idx)
884
- fb_list.append(fb1)
885
-
886
- arbitrary_array = fb_list[0].join_bodies(*fb_list[1:])
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)
887
330
 
888
331
  return arbitrary_array
889
332
 
890
- def extract_faces(self, id_faces_to_extract, return_index=False):
891
- """Create a new FloatingBody by extracting some faces from the mesh.
892
- The dofs evolve accordingly.
893
- The lid_mesh, center_of_mass, mass and hydrostatics data are discarded.
894
- """
895
- if isinstance(self.mesh, CollectionOfMeshes):
896
- raise NotImplementedError # TODO
897
-
898
- if return_index:
899
- new_mesh, id_v = self.mesh.extract_faces(id_faces_to_extract, return_index)
900
- else:
901
- new_mesh = self.mesh.extract_faces(id_faces_to_extract, return_index)
902
- new_body = FloatingBody(new_mesh)
903
- LOG.info(f"Extract floating body from {self.name}.")
904
-
905
- new_body.dofs = {}
906
- for name, dof in self.dofs.items():
907
- new_body.dofs[name] = dof[id_faces_to_extract, :]
908
-
909
- if return_index:
910
- return new_body, id_v
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
911
338
  else:
912
- return new_body
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
913
388
 
914
- def sliced_by_plane(self, plane):
915
- """Return the same body, but replace the mesh by a set of two meshes
916
- corresponding to each sides of the plane."""
917
- return FloatingBody(mesh=self.mesh.sliced_by_plane(plane),
918
- lid_mesh=self.lid_mesh.sliced_by_plane(plane)
919
- if self.lid_mesh is not None else None,
920
- dofs=self.dofs, name=self.name)
389
+ def translated_x(self, dx: float, *, name=None) -> "FloatingBody":
390
+ return self.translated([dx, 0.0, 0.0], name=name)
921
391
 
922
- def minced(self, nb_slices=(8, 8, 4)):
923
- """Experimental method decomposing the mesh as a hierarchical structure.
392
+ def translated_y(self, dy: float, *, name=None) -> "FloatingBody":
393
+ return self.translated([0.0, dy, 0.0], name=name)
924
394
 
925
- Parameters
926
- ----------
927
- nb_slices: Tuple[int, int, int]
928
- The number of slices in each of the x, y and z directions.
929
- Only powers of 2 are supported at the moment.
395
+ def translated_z(self, dz: float, *, name=None) -> "FloatingBody":
396
+ return self.translated([0.0, 0.0, dz], name=name)
930
397
 
931
- Returns
932
- -------
933
- FloatingBody
934
- """
935
- minced_body = self.copy()
936
-
937
- # Extreme points of the mesh in each directions.
938
- x_min, x_max, y_min, y_max, z_min, z_max = self.mesh.axis_aligned_bbox
939
- sizes = [(x_min, x_max), (y_min, y_max), (z_min, z_max)]
940
-
941
- directions = [np.array(d) for d in [(1, 0, 0), (0, 1, 0), (0, 0, 1)]]
942
-
943
- def _slice_positions_at_depth(i):
944
- """Helper function.
945
-
946
- Returns a list of floats as follows:
947
- i=1 -> [1/2]
948
- i=2 -> [1/4, 3/4]
949
- i=3 -> [1/8, 3/8, 5/8, 7/8]
950
- ...
951
- """
952
- denominator = 2**i
953
- return [numerator/denominator for numerator in range(1, denominator, 2)]
954
-
955
- # GENERATE ALL THE PLANES THAT WILL BE USED TO MINCE THE MESH
956
- planes = []
957
- for direction, nb_slices_in_dir, (min_coord, max_coord) in zip(directions, nb_slices, sizes):
958
- planes_in_dir = []
959
-
960
- depth_of_treelike_structure = int(np.log2(nb_slices_in_dir))
961
- for i_depth in range(1, depth_of_treelike_structure+1):
962
- planes_in_dir_at_depth = []
963
- for relative_position in _slice_positions_at_depth(i_depth):
964
- slice_position = (min_coord + relative_position*(max_coord-min_coord))*direction
965
- plane = Plane(normal=direction, point=slice_position)
966
- planes_in_dir_at_depth.append(plane)
967
- planes_in_dir.append(planes_in_dir_at_depth)
968
- planes.append(planes_in_dir)
969
-
970
- # SLICE THE MESH
971
- intermingled_x_y_z = chain.from_iterable(zip_longest(*planes))
972
- for planes in intermingled_x_y_z:
973
- if planes is not None:
974
- for plane in planes:
975
- minced_body = minced_body.sliced_by_plane(plane)
976
- return minced_body
977
-
978
- @inplace_transformation
979
- def mirror(self, plane):
980
- self.mesh.mirror(plane)
981
- if self.lid_mesh is not None:
982
- self.lid_mesh.mirror(plane)
983
- for dof in self.dofs:
984
- self.dofs[dof] -= 2 * np.outer(np.dot(self.dofs[dof], plane.normal), plane.normal)
985
- for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
986
- if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
987
- point = np.array(self.__dict__[point_attr])
988
- shift = - 2 * (np.dot(point, plane.normal) - plane.c) * plane.normal
989
- self.__dict__[point_attr] = point + shift
990
- return self
991
-
992
- @inplace_transformation
993
- def translate(self, vector, *args, **kwargs):
994
- self.mesh.translate(vector, *args, **kwargs)
995
- if self.lid_mesh is not None:
996
- self.lid_mesh.translate(vector, *args, **kwargs)
997
- for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
998
- if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
999
- self.__dict__[point_attr] = np.array(self.__dict__[point_attr]) + vector
1000
- return self
1001
-
1002
- @inplace_transformation
1003
- def rotate(self, axis, angle):
1004
- self.mesh.rotate(axis, angle)
1005
- if self.lid_mesh is not None:
1006
- self.lid_mesh.rotate(axis, angle)
1007
- for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
1008
- if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
1009
- self.__dict__[point_attr] = axis.rotate_points([self.__dict__[point_attr]], angle)[0, :]
1010
- for dof in self.dofs:
1011
- self.dofs[dof] = axis.rotate_vectors(self.dofs[dof], angle)
1012
- return self
1013
-
1014
- @inplace_transformation
1015
- def clip(self, plane):
1016
- self._check_dofs_shape_consistency()
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")
1017
442
 
1018
- # Clip mesh
1019
- LOG.info(f"Clipping {self.name} with respect to {plane}")
1020
- self.mesh.clip(plane)
1021
443
  if self.lid_mesh is not None:
1022
- self.lid_mesh.clip(plane)
1023
- if self.lid_mesh.nb_faces == 0:
1024
- LOG.warning("Lid mesh %s is empty after clipping. The lid mesh is removed.", self.lid_mesh)
1025
- self.lid_mesh = 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
1026
450
 
1027
- # Clip dofs
1028
- ids = self.mesh._clipping_data['faces_ids']
1029
- for dof in self.dofs:
1030
- if len(ids) > 0:
1031
- self.dofs[dof] = np.array(self.dofs[dof])[ids]
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
1032
460
  else:
1033
- self.dofs[dof] = np.empty((0, 3))
1034
- return self
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
1035
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
+ )
1036
497
 
1037
498
  #############
1038
499
  # Display #
@@ -1085,12 +546,11 @@ respective inertia coefficients are assigned as NaN.")
1085
546
  yield "center_of_mass", tuple(self.center_of_mass)
1086
547
  yield "name", self.name
1087
548
 
1088
- def show(self, **kwargs):
1089
- from capytaine.ui.vtk.body_viewer import FloatingBodyViewer
1090
- viewer = FloatingBodyViewer()
1091
- viewer.add_body(self, **kwargs)
1092
- viewer.show()
1093
- viewer.finalize()
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)
1094
554
 
1095
555
  def show_matplotlib(self, *args, **kwargs):
1096
556
  return self.mesh.show_matplotlib(*args, **kwargs)
@@ -1119,6 +579,10 @@ respective inertia coefficients are assigned as NaN.")
1119
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))
1120
580
  return animation
1121
581
 
582
+ #################################
583
+ # Irregular frequencies removal #
584
+ #################################
585
+
1122
586
  @property
1123
587
  def minimal_computable_wavelength(self):
1124
588
  """For accuracy of the resolution, wavelength should not be smaller than this value."""
@@ -1164,50 +628,3 @@ respective inertia coefficients are assigned as NaN.")
1164
628
  omega_comp = np.sqrt(np.pi*g*p/(np.tanh(np.pi*draft*p)))
1165
629
  omega = min(omega, omega_comp)
1166
630
  return omega
1167
-
1168
- def cluster_bodies(*bodies, name=None):
1169
- """
1170
- Builds a hierarchical clustering from a group of bodies
1171
-
1172
- Parameters
1173
- ----------
1174
- bodies: list
1175
- a list of bodies
1176
- name: str, optional
1177
- a name for the new body
1178
-
1179
- Returns
1180
- -------
1181
- FloatingBody
1182
- Array built from the provided bodies
1183
- """
1184
- from scipy.cluster.hierarchy import linkage
1185
- nb_buoys = len(bodies)
1186
-
1187
- if any(body.center_of_buoyancy is None for body in bodies):
1188
- raise ValueError("The center of buoyancy of each body needs to be known for clustering")
1189
- buoys_positions = np.stack([body.center_of_buoyancy for body in bodies])[:,:2]
1190
-
1191
- ln_matrix = linkage(buoys_positions, method='centroid', metric='euclidean')
1192
-
1193
- node_list = list(bodies) # list of nodes of the tree: the first nodes are single bodies
1194
-
1195
- # Join the bodies, with an ordering consistent with the dendrogram.
1196
- # Done by reading the linkage matrix: its i-th row contains the labels
1197
- # of the two nodes that are merged to form the (n + i)-th node
1198
- for ii in range(len(ln_matrix)):
1199
- node_tag = ii + nb_buoys # the first nb_buoys tags are already taken
1200
- merge_left = int(ln_matrix[ii,0])
1201
- merge_right = int(ln_matrix[ii,1])
1202
- # The new node is the parent of merge_left and merge_right
1203
- new_node_ls = [node_list[merge_left], node_list[merge_right]]
1204
- new_node = FloatingBody.join_bodies(*new_node_ls, name='node_{:d}'.format(node_tag))
1205
- node_list.append(new_node)
1206
-
1207
- # The last node is the parent of all others
1208
- all_buoys = new_node
1209
-
1210
- if name is not None:
1211
- all_buoys.name = name
1212
-
1213
- return all_buoys