capytaine 2.2.1__cp311-cp311-macosx_14_0_arm64.whl → 2.3__cp311-cp311-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 (48) 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 +1 -1
  5. capytaine/__init__.py +2 -1
  6. capytaine/bem/airy_waves.py +7 -2
  7. capytaine/bem/problems_and_results.py +78 -34
  8. capytaine/bem/solver.py +127 -39
  9. capytaine/bodies/bodies.py +30 -10
  10. capytaine/bodies/predefined/rectangles.py +2 -0
  11. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  12. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  13. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  14. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  15. capytaine/green_functions/FinGreen3D/README.md +24 -0
  16. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  17. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  18. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  19. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  20. capytaine/green_functions/LiangWuNoblesse/Makefile +18 -0
  21. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  22. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  23. capytaine/green_functions/abstract_green_function.py +55 -3
  24. capytaine/green_functions/delhommeau.py +186 -115
  25. capytaine/green_functions/hams.py +204 -0
  26. capytaine/green_functions/libs/Delhommeau_float32.cpython-311-darwin.so +0 -0
  27. capytaine/green_functions/libs/Delhommeau_float64.cpython-311-darwin.so +0 -0
  28. capytaine/io/bemio.py +14 -2
  29. capytaine/io/mesh_loaders.py +1 -1
  30. capytaine/io/wamit.py +479 -0
  31. capytaine/io/xarray.py +257 -113
  32. capytaine/matrices/linear_solvers.py +1 -1
  33. capytaine/meshes/clipper.py +1 -0
  34. capytaine/meshes/collections.py +11 -1
  35. capytaine/meshes/mesh_like_protocol.py +37 -0
  36. capytaine/meshes/meshes.py +17 -6
  37. capytaine/meshes/symmetric.py +11 -2
  38. capytaine/post_pro/kochin.py +4 -4
  39. capytaine/tools/lists_of_points.py +3 -3
  40. capytaine/tools/prony_decomposition.py +60 -4
  41. capytaine/tools/symbolic_multiplication.py +12 -0
  42. capytaine/tools/timer.py +64 -0
  43. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/METADATA +9 -2
  44. capytaine-2.3.dist-info/RECORD +92 -0
  45. capytaine-2.2.1.dist-info/RECORD +0 -76
  46. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/LICENSE +0 -0
  47. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/WHEEL +0 -0
  48. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/entry_points.txt +0 -0
capytaine/bem/solver.py CHANGED
@@ -9,21 +9,24 @@
9
9
 
10
10
  """
11
11
 
12
+ import os
12
13
  import logging
13
14
 
14
15
  import numpy as np
16
+ import pandas as pd
15
17
 
16
18
  from datetime import datetime
17
19
 
18
20
  from rich.progress import track
19
21
 
20
- from capytaine.bem.problems_and_results import LinearPotentialFlowProblem
22
+ from capytaine.bem.problems_and_results import LinearPotentialFlowProblem, DiffractionProblem
21
23
  from capytaine.green_functions.delhommeau import Delhommeau
22
24
  from capytaine.bem.engines import BasicMatrixEngine
23
25
  from capytaine.io.xarray import problems_from_dataset, assemble_dataset, kochin_data_array
24
26
  from capytaine.tools.optional_imports import silently_import_optional_dependency
25
27
  from capytaine.tools.lists_of_points import _normalize_points, _normalize_free_surface_points
26
28
  from capytaine.tools.symbolic_multiplication import supporting_symbolic_multiplication
29
+ from capytaine.tools.timer import Timer
27
30
 
28
31
  LOG = logging.getLogger(__name__)
29
32
 
@@ -39,24 +42,39 @@ class BEMSolver:
39
42
  engine: MatrixEngine, optional
40
43
  Object handling the building of matrices and the resolution of linear systems with these matrices.
41
44
  (default: :class:`~capytaine.bem.engines.BasicMatrixEngine`)
45
+ method: string, optional
46
+ select boundary integral equation used to solve the problems.
47
+ Accepted values: "indirect" (as in e.g. Nemoh), "direct" (as in e.g. WAMIT)
48
+ Default value: "indirect"
42
49
 
43
50
  Attributes
44
51
  ----------
52
+ timer: dict[str, Timer]
53
+ Storing the time spent on each subtasks of the resolution
45
54
  exportable_settings : dict
46
55
  Settings of the solver that can be saved to reinit the same solver later.
47
56
  """
48
57
 
49
- def __init__(self, *, green_function=None, engine=None):
58
+ def __init__(self, *, green_function=None, engine=None, method="indirect"):
50
59
  self.green_function = Delhommeau() if green_function is None else green_function
51
60
  self.engine = BasicMatrixEngine() if engine is None else engine
52
61
 
62
+ if method.lower() not in {"direct", "indirect"}:
63
+ raise ValueError(f"Unrecognized method when initializing solver: {repr(method)}. Expected \"direct\" or \"indirect\".")
64
+ self.method = method.lower()
65
+
66
+ self.timer = {"Solve total": Timer(), " Green function": Timer(), " Linear solver": Timer()}
67
+
68
+ self.solve = self.timer["Solve total"].wraps_function(self.solve)
69
+
53
70
  try:
54
71
  self.exportable_settings = {
55
72
  **self.green_function.exportable_settings,
56
- **self.engine.exportable_settings
73
+ **self.engine.exportable_settings,
74
+ "method": self.method,
57
75
  }
58
76
  except AttributeError:
59
- pass
77
+ self.exportable_settings = {}
60
78
 
61
79
  def __str__(self):
62
80
  return f"BEMSolver(engine={self.engine}, green_function={self.green_function})"
@@ -64,6 +82,15 @@ class BEMSolver:
64
82
  def __repr__(self):
65
83
  return self.__str__()
66
84
 
85
+ def timer_summary(self):
86
+ return pd.DataFrame([
87
+ {
88
+ "task": name,
89
+ "total": self.timer[name].total,
90
+ "nb_calls": self.timer[name].nb_timings,
91
+ "mean": self.timer[name].mean
92
+ } for name in self.timer]).set_index("task")
93
+
67
94
  def _repr_pretty_(self, p, cycle):
68
95
  p.text(self.__str__())
69
96
 
@@ -71,7 +98,7 @@ class BEMSolver:
71
98
  def from_exported_settings(settings):
72
99
  raise NotImplementedError
73
100
 
74
- def solve(self, problem, method='indirect', keep_details=True, _check_wavelength=True):
101
+ def solve(self, problem, method=None, keep_details=True, _check_wavelength=True):
75
102
  """Solve the linear potential flow problem.
76
103
 
77
104
  Parameters
@@ -79,7 +106,9 @@ class BEMSolver:
79
106
  problem: LinearPotentialFlowProblem
80
107
  the problem to be solved
81
108
  method: string, optional
82
- select boundary integral approach indirect (i.e.Nemoh)/direct (i.e.WAMIT) (default: indirect)
109
+ select boundary integral equation used to solve the problem.
110
+ It is recommended to set the method more globally when initializing the solver.
111
+ If provided here, the value in argument of `solve` overrides the global one.
83
112
  keep_details: bool, optional
84
113
  if True, store the sources and the potential on the floating body in the output object
85
114
  (default: True)
@@ -98,33 +127,54 @@ class BEMSolver:
98
127
  self._check_wavelength_and_mesh_resolution([problem])
99
128
  self._check_wavelength_and_irregular_frequencies([problem])
100
129
 
130
+ if isinstance(problem, DiffractionProblem) and float(problem.encounter_omega) in {0.0, np.inf}:
131
+ raise ValueError("Diffraction problems at zero or infinite frequency are not defined")
132
+ # This error used to be raised when initializing the problem.
133
+ # It is now raised here, in order to be catchable by
134
+ # _solve_and_catch_errors, such that batch resolution
135
+ # can include this kind of problems without the full batch
136
+ # failing.
137
+ # Note that if this error was not raised here, the resolution
138
+ # would still fail with a less explicit error message.
139
+
101
140
  if problem.forward_speed != 0.0:
102
141
  omega, wavenumber = problem.encounter_omega, problem.encounter_wavenumber
103
142
  else:
104
143
  omega, wavenumber = problem.omega, problem.wavenumber
105
144
 
106
145
  linear_solver = supporting_symbolic_multiplication(self.engine.linear_solver)
146
+ method = method if method is not None else self.method
107
147
  if (method == 'direct'):
108
148
  if problem.forward_speed != 0.0:
109
149
  raise NotImplementedError("Direct solver is not able to solve problems with forward speed.")
110
150
 
111
- S, D = self.engine.build_matrices(
112
- problem.body.mesh_including_lid, problem.body.mesh_including_lid,
113
- problem.free_surface, problem.water_depth, wavenumber,
114
- self.green_function, adjoint_double_layer=False
115
- )
116
-
117
- potential = linear_solver(D, S @ problem.boundary_condition)
151
+ with self.timer[" Green function"]:
152
+ S, D = self.engine.build_matrices(
153
+ problem.body.mesh_including_lid, problem.body.mesh_including_lid,
154
+ problem.free_surface, problem.water_depth, wavenumber,
155
+ self.green_function, adjoint_double_layer=False
156
+ )
157
+ rhs = S @ problem.boundary_condition
158
+ with self.timer[" Linear solver"]:
159
+ potential = linear_solver(D, rhs)
160
+ if not potential.shape == problem.boundary_condition.shape:
161
+ raise ValueError(f"Error in linear solver of {self.engine}: the shape of the output ({potential.shape}) "
162
+ f"does not match the expected shape ({problem.boundary_condition.shape})")
118
163
  pressure = 1j * omega * problem.rho * potential
119
164
  sources = None
120
165
  else:
121
- S, K = self.engine.build_matrices(
122
- problem.body.mesh_including_lid, problem.body.mesh_including_lid,
123
- problem.free_surface, problem.water_depth, wavenumber,
124
- self.green_function, adjoint_double_layer=True
125
- )
166
+ with self.timer[" Green function"]:
167
+ S, K = self.engine.build_matrices(
168
+ problem.body.mesh_including_lid, problem.body.mesh_including_lid,
169
+ problem.free_surface, problem.water_depth, wavenumber,
170
+ self.green_function, adjoint_double_layer=True
171
+ )
126
172
 
127
- sources = linear_solver(K, problem.boundary_condition)
173
+ with self.timer[" Linear solver"]:
174
+ sources = linear_solver(K, problem.boundary_condition)
175
+ if not sources.shape == problem.boundary_condition.shape:
176
+ raise ValueError(f"Error in linear solver of {self.engine}: the shape of the output ({sources.shape}) "
177
+ f"does not match the expected shape ({problem.boundary_condition.shape})")
128
178
  potential = S @ sources
129
179
  pressure = 1j * omega * problem.rho * potential
130
180
  if problem.forward_speed != 0.0:
@@ -145,7 +195,17 @@ class BEMSolver:
145
195
 
146
196
  return result
147
197
 
148
- def solve_all(self, problems, *, method='indirect', n_jobs=1, progress_bar=True, _check_wavelength=True, **kwargs):
198
+ def _solve_and_catch_errors(self, problem, *args, **kwargs):
199
+ """Same as BEMSolver.solve() but returns a
200
+ FailedLinearPotentialFlowResult when the resolution failed."""
201
+ try:
202
+ res = self.solve(problem, *args, **kwargs)
203
+ except Exception as e:
204
+ LOG.info(f"Skipped {problem}\nbecause of {repr(e)}")
205
+ res = problem.make_failed_results_container(e)
206
+ return res
207
+
208
+ def solve_all(self, problems, *, method=None, n_jobs=1, progress_bar=None, _check_wavelength=True, **kwargs):
149
209
  """Solve several problems.
150
210
  Optional keyword arguments are passed to `BEMSolver.solve`.
151
211
 
@@ -154,12 +214,17 @@ class BEMSolver:
154
214
  problems: list of LinearPotentialFlowProblem
155
215
  several problems to be solved
156
216
  method: string, optional
157
- select boundary integral approach indirect (i.e.Nemoh)/direct (i.e.WAMIT) (default: indirect)
217
+ select boundary integral equation used to solve the problems.
218
+ It is recommended to set the method more globally when initializing the solver.
219
+ If provided here, the value in argument of `solve_all` overrides the global one.
158
220
  n_jobs: int, optional (default: 1)
159
221
  the number of jobs to run in parallel using the optional dependency `joblib`
160
222
  By defaults: do not use joblib and solve sequentially.
161
- progress_bar: bool, optional (default: True)
162
- Display a progress bar while solving
223
+ progress_bar: bool, optional
224
+ Display a progress bar while solving.
225
+ If no value is provided to this method directly,
226
+ check whether the environment variable `CAPYTAINE_PROGRESS_BAR` is defined
227
+ and otherwise default to True.
163
228
  _check_wavelength: bool, optional (default: True)
164
229
  If True, the frequencies are compared to the mesh resolution and
165
230
  the estimated first irregular frequency to warn the user.
@@ -173,11 +238,23 @@ class BEMSolver:
173
238
  self._check_wavelength_and_mesh_resolution(problems)
174
239
  self._check_wavelength_and_irregular_frequencies(problems)
175
240
 
241
+ if progress_bar is None:
242
+ if "CAPYTAINE_PROGRESS_BAR" in os.environ:
243
+ env_var = os.environ["CAPYTAINE_PROGRESS_BAR"].lower()
244
+ if env_var in {'true', '1', 't'}:
245
+ progress_bar = True
246
+ elif env_var in {'false', '0', 'f'}:
247
+ progress_bar = False
248
+ else:
249
+ raise ValueError("Invalid value '{}' for the environment variable CAPYTAINE_PROGRESS_BAR.".format(os.environ["CAPYTAINE_PROGRESS_BAR"]))
250
+ else:
251
+ progress_bar = True
252
+
176
253
  if n_jobs == 1: # force sequential resolution
177
254
  problems = sorted(problems)
178
255
  if progress_bar:
179
256
  problems = track(problems, total=len(problems), description="Solving BEM problems")
180
- return [self.solve(pb, method=method, _check_wavelength=False, **kwargs) for pb in problems]
257
+ results = [self._solve_and_catch_errors(pb, method=method, _check_wavelength=False, **kwargs) for pb in problems]
181
258
  else:
182
259
  joblib = silently_import_optional_dependency("joblib")
183
260
  if joblib is None:
@@ -190,7 +267,8 @@ class BEMSolver:
190
267
  total=len(groups_of_problems),
191
268
  description=f"Solving BEM problems with {n_jobs} threads:")
192
269
  results = [res for grp in groups_of_results for res in grp] # flatten the nested list
193
- return results
270
+ LOG.info("Solver timer summary:\n%s", self.timer_summary())
271
+ return results
194
272
 
195
273
  @staticmethod
196
274
  def _check_wavelength_and_mesh_resolution(problems):
@@ -225,7 +303,8 @@ class BEMSolver:
225
303
  """Display a warning if some of the problems might encounter irregular frequencies."""
226
304
  LOG.debug("Check wavelength with estimated irregular frequency.")
227
305
  risky_problems = [pb for pb in problems
228
- if pb.body.first_irregular_frequency_estimate(g=pb.g) < pb.omega < np.inf]
306
+ if pb.free_surface != np.inf and
307
+ pb.body.first_irregular_frequency_estimate(g=pb.g) < pb.omega < np.inf]
229
308
  nb_risky_problems = len(risky_problems)
230
309
  if nb_risky_problems >= 1:
231
310
  if any(pb.body.lid_mesh is None for pb in problems):
@@ -250,7 +329,7 @@ class BEMSolver:
250
329
  + recommendation
251
330
  )
252
331
 
253
- def fill_dataset(self, dataset, bodies, *, method='indirect', n_jobs=1, _check_wavelength=True, **kwargs):
332
+ def fill_dataset(self, dataset, bodies, *, method=None, n_jobs=1, _check_wavelength=True, progress_bar=None, **kwargs):
254
333
  """Solve a set of problems defined by the coordinates of an xarray dataset.
255
334
 
256
335
  Parameters
@@ -261,12 +340,17 @@ class BEMSolver:
261
340
  The body or bodies involved in the problems
262
341
  They should all have different names.
263
342
  method: string, optional
264
- select boundary integral approach indirect (i.e.Nemoh)/direct (i.e.WAMIT) (default: indirect)
343
+ select boundary integral equation used to solve the problems.
344
+ It is recommended to set the method more globally when initializing the solver.
345
+ If provided here, the value in argument of `fill_dataset` overrides the global one.
265
346
  n_jobs: int, optional (default: 1)
266
347
  the number of jobs to run in parallel using the optional dependency `joblib`
267
348
  By defaults: do not use joblib and solve sequentially.
268
- progress_bar: bool, optional (default: True)
269
- Display a progress bar while solving
349
+ progress_bar: bool, optional
350
+ Display a progress bar while solving.
351
+ If no value is provided to this method directly,
352
+ check whether the environment variable `CAPYTAINE_PROGRESS_BAR` is defined
353
+ and otherwise default to True.
270
354
  _check_wavelength: bool, optional (default: True)
271
355
  If True, the frequencies are compared to the mesh resolution and
272
356
  the estimated first irregular frequency to warn the user.
@@ -277,14 +361,16 @@ class BEMSolver:
277
361
  """
278
362
  attrs = {'start_of_computation': datetime.now().isoformat(),
279
363
  **self.exportable_settings}
364
+ if method is not None: # Overrides the method in self.exportable_settings
365
+ attrs["method"] = method
280
366
  problems = problems_from_dataset(dataset, bodies)
281
367
  if 'theta' in dataset.coords:
282
- results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength)
368
+ results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
283
369
  kochin = kochin_data_array(results, dataset.coords['theta'])
284
370
  dataset = assemble_dataset(results, attrs=attrs, **kwargs)
285
371
  dataset.update(kochin)
286
372
  else:
287
- results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength)
373
+ results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
288
374
  dataset = assemble_dataset(results, attrs=attrs, **kwargs)
289
375
  return dataset
290
376
 
@@ -294,7 +380,7 @@ class BEMSolver:
294
380
 
295
381
  Parameters
296
382
  ----------
297
- points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or cpt.Mesh or cpt.CollectionOfMeshes object
383
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
298
384
  Coordinates of the point(s) at which the potential should be computed
299
385
  result: LinearPotentialFlowResult
300
386
  The return of the BEM solver
@@ -314,7 +400,8 @@ class BEMSolver:
314
400
  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.
315
401
  Please re-run the resolution with the indirect method and keep_details=True.""")
316
402
 
317
- S, _ = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber)
403
+ with self.timer[" Green function"]:
404
+ S, _ = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber)
318
405
  potential = S @ result.sources # Sum the contributions of all panels in the mesh
319
406
  return potential.reshape(output_shape)
320
407
 
@@ -326,7 +413,8 @@ class BEMSolver:
326
413
  They probably have not been stored by the solver because the option keep_details=True have not been set.
327
414
  Please re-run the resolution with this option.""")
328
415
 
329
- _, gradG = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber,
416
+ with self.timer[" Green function"]:
417
+ _, gradG = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber,
330
418
  early_dot_product=False)
331
419
  velocities = np.einsum('ijk,j->ik', gradG, result.sources) # Sum the contributions of all panels in the mesh
332
420
  return velocities.reshape((*output_shape, 3))
@@ -336,7 +424,7 @@ class BEMSolver:
336
424
 
337
425
  Parameters
338
426
  ----------
339
- points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or cpt.Mesh or cpt.CollectionOfMeshes object
427
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
340
428
  Coordinates of the point(s) at which the velocity should be computed
341
429
  result: LinearPotentialFlowResult
342
430
  The return of the BEM solver
@@ -360,7 +448,7 @@ class BEMSolver:
360
448
 
361
449
  Parameters
362
450
  ----------
363
- points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or cpt.Mesh or cpt.CollectionOfMeshes object
451
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
364
452
  Coordinates of the point(s) at which the pressure should be computed
365
453
  result: LinearPotentialFlowResult
366
454
  The return of the BEM solver
@@ -388,7 +476,7 @@ class BEMSolver:
388
476
 
389
477
  Parameters
390
478
  ----------
391
- points: array of shape (2,) or (N, 2), or 2-ple of arrays returned by meshgrid, or cpt.Mesh or cpt.CollectionOfMeshes object
479
+ points: array of shape (2,) or (N, 2), or 2-ple of arrays returned by meshgrid, or MeshLike object
392
480
  Coordinates of the point(s) at which the free surface elevation should be computed
393
481
  result: LinearPotentialFlowResult
394
482
  The return of the BEM solver
@@ -427,7 +515,7 @@ class BEMSolver:
427
515
  ----------
428
516
  result : LinearPotentialFlowResult
429
517
  the return of the BEM solver
430
- mesh : Mesh or CollectionOfMeshes
518
+ mesh : MeshLike
431
519
  a mesh
432
520
  chunk_size: int, optional
433
521
  Number of lines to compute in the matrix.
@@ -10,6 +10,7 @@ from functools import cached_property, lru_cache
10
10
  import numpy as np
11
11
  import xarray as xr
12
12
 
13
+ from capytaine.meshes.mesh_like_protocol import MeshLike
13
14
  from capytaine.meshes.collections import CollectionOfMeshes
14
15
  from capytaine.meshes.geometry import Abstract3DObject, ClippableMixin, Plane, inplace_transformation
15
16
  from capytaine.meshes.properties import connected_components, connected_components_of_waterline
@@ -39,10 +40,10 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
39
40
 
40
41
  Parameters
41
42
  ----------
42
- mesh : Mesh or CollectionOfMeshes, optional
43
+ mesh : MeshLike, optional
43
44
  the mesh describing the geometry of the hull of the floating body.
44
45
  If none is given, a empty one is created.
45
- lid_mesh : Mesh or CollectionOfMeshes or None, optional
46
+ lid_mesh : MeshLike or None, optional
46
47
  a mesh of an internal lid for irregular frequencies removal.
47
48
  Unlike the mesh of the hull, no dof is defined on the lid_mesh.
48
49
  If none is given, none is used when solving the Boundary Integral Equation.
@@ -69,7 +70,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
69
70
  from capytaine.io.meshio import load_from_meshio
70
71
  self.mesh = load_from_meshio(mesh)
71
72
 
72
- elif isinstance(mesh, Mesh) or isinstance(mesh, CollectionOfMeshes):
73
+ elif isinstance(mesh, MeshLike):
73
74
  self.mesh = mesh
74
75
 
75
76
  else:
@@ -87,7 +88,10 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
87
88
  if name is None and mesh is None:
88
89
  self.name = "dummy_body"
89
90
  elif name is None:
90
- self.name = self.mesh.name
91
+ if hasattr(self.mesh, "name"):
92
+ self.name = self.mesh.name
93
+ else:
94
+ self.name = "anonymous_body"
91
95
  else:
92
96
  self.name = name
93
97
 
@@ -97,7 +101,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
97
101
  else:
98
102
  self.center_of_mass = None
99
103
 
100
- if self.mesh.nb_vertices > 0 and self.mesh.nb_faces > 0:
104
+ if hasattr(self.mesh, "heal_mesh") and self.mesh.nb_vertices > 0 and self.mesh.nb_faces > 0:
101
105
  self.mesh.heal_mesh()
102
106
 
103
107
  if dofs is None:
@@ -112,6 +116,8 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
112
116
 
113
117
  LOG.info(f"New floating body: {self.__str__()}.")
114
118
 
119
+ self._check_dofs_shape_consistency()
120
+
115
121
  @staticmethod
116
122
  def from_meshio(mesh, name=None) -> 'FloatingBody':
117
123
  """Create a FloatingBody from a meshio mesh object.
@@ -135,7 +141,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
135
141
  @cached_property
136
142
  def mesh_including_lid(self):
137
143
  if self.lid_mesh is not None:
138
- return CollectionOfMeshes([self.mesh, self.lid_mesh])
144
+ return self.mesh.join_meshes(self.lid_mesh)
139
145
  else:
140
146
  return self.mesh
141
147
 
@@ -264,6 +270,14 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
264
270
  coords={'influenced_dof': list(self.dofs), 'radiating_dof': list(self.dofs)},
265
271
  )
266
272
 
273
+ def _check_dofs_shape_consistency(self):
274
+ 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"
277
+ f"Expected shape: ({self.mesh.nb_faces}, 3)\n"
278
+ f" Actual shape: {dof.shape}")
279
+
280
+
267
281
  ###################
268
282
  # Hydrostatics #
269
283
  ###################
@@ -756,14 +770,14 @@ respective inertia coefficients are assigned as NaN.")
756
770
  if name is None:
757
771
  name = "+".join(body.name for body in bodies)
758
772
  meshes = CollectionOfMeshes(
759
- [body.mesh for body in bodies],
773
+ [body.mesh.copy() for body in bodies],
760
774
  name=f"{name}_mesh"
761
775
  )
762
776
  if all(body.lid_mesh is None for body in bodies):
763
777
  lid_meshes = None
764
778
  else:
765
779
  lid_meshes = CollectionOfMeshes(
766
- [body.lid_mesh for body in bodies if body.lid_mesh is not None],
780
+ [body.lid_mesh.copy() for body in bodies if body.lid_mesh is not None],
767
781
  name=f"{name}_lid_mesh"
768
782
  )
769
783
  dofs = FloatingBody.combine_dofs(bodies)
@@ -796,6 +810,8 @@ respective inertia coefficients are assigned as NaN.")
796
810
  @staticmethod
797
811
  def combine_dofs(bodies) -> dict:
798
812
  """Combine the degrees of freedom of several bodies."""
813
+ for body in bodies:
814
+ body._check_dofs_shape_consistency()
799
815
  dofs = {}
800
816
  cum_nb_faces = accumulate(chain([0], (body.mesh.nb_faces for body in bodies)))
801
817
  total_nb_faces = sum(body.mesh.nb_faces for body in bodies)
@@ -823,6 +839,8 @@ respective inertia coefficients are assigned as NaN.")
823
839
  name : str, optional
824
840
  a name for the new copy
825
841
  """
842
+ self._check_dofs_shape_consistency()
843
+
826
844
  new_body = copy.deepcopy(self)
827
845
  if name is None:
828
846
  new_body.name = f"copy_of_{self.name}"
@@ -878,9 +896,9 @@ respective inertia coefficients are assigned as NaN.")
878
896
  raise NotImplementedError # TODO
879
897
 
880
898
  if return_index:
881
- new_mesh, id_v = Mesh.extract_faces(self.mesh, id_faces_to_extract, return_index)
899
+ new_mesh, id_v = self.mesh.extract_faces(id_faces_to_extract, return_index)
882
900
  else:
883
- new_mesh = Mesh.extract_faces(self.mesh, id_faces_to_extract, return_index)
901
+ new_mesh = self.mesh.extract_faces(id_faces_to_extract, return_index)
884
902
  new_body = FloatingBody(new_mesh)
885
903
  LOG.info(f"Extract floating body from {self.name}.")
886
904
 
@@ -995,6 +1013,8 @@ respective inertia coefficients are assigned as NaN.")
995
1013
 
996
1014
  @inplace_transformation
997
1015
  def clip(self, plane):
1016
+ self._check_dofs_shape_consistency()
1017
+
998
1018
  # Clip mesh
999
1019
  LOG.info(f"Clipping {self.name} with respect to {plane}")
1000
1020
  self.mesh.clip(plane)
@@ -101,6 +101,8 @@ class RectangularParallelepiped(FloatingBody):
101
101
  translation_symmetry=translational_symmetry, reflection_symmetry=reflection_symmetry,
102
102
  name=f"{name}_mesh")
103
103
 
104
+ self.geometric_center = np.asarray(center, dtype=float)
105
+
104
106
  FloatingBody.__init__(self, mesh=mesh, name=name)
105
107
 
106
108
  class OpenRectangularParallelepiped(RectangularParallelepiped):
@@ -0,0 +1 @@
1
+ build/