capytaine 2.1__cp312-cp312-win_amd64.whl

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