capytaine 2.1__cp38-cp38-macosx_10_9_x86_64.whl → 2.2__cp38-cp38-macosx_10_9_x86_64.whl

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