capytaine 2.2__cp312-cp312-win_amd64.whl → 2.3__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 (54) hide show
  1. capytaine/__about__.py +1 -1
  2. capytaine/__init__.py +6 -6
  3. capytaine/bem/airy_waves.py +7 -2
  4. capytaine/bem/engines.py +2 -2
  5. capytaine/bem/problems_and_results.py +82 -35
  6. capytaine/bem/solver.py +138 -41
  7. capytaine/bodies/bodies.py +40 -12
  8. capytaine/bodies/predefined/rectangles.py +2 -0
  9. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  10. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  11. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  12. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  13. capytaine/green_functions/FinGreen3D/README.md +24 -0
  14. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  15. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  16. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  17. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  18. capytaine/green_functions/LiangWuNoblesse/Makefile +18 -0
  19. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  20. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  21. capytaine/green_functions/abstract_green_function.py +55 -3
  22. capytaine/green_functions/delhommeau.py +186 -115
  23. capytaine/green_functions/hams.py +204 -0
  24. capytaine/green_functions/libs/Delhommeau_float32.cp312-win_amd64.dll.a +0 -0
  25. capytaine/green_functions/libs/Delhommeau_float32.cp312-win_amd64.pyd +0 -0
  26. capytaine/green_functions/libs/Delhommeau_float64.cp312-win_amd64.dll.a +0 -0
  27. capytaine/green_functions/libs/Delhommeau_float64.cp312-win_amd64.pyd +0 -0
  28. capytaine/io/bemio.py +14 -2
  29. capytaine/io/mesh_loaders.py +2 -1
  30. capytaine/io/wamit.py +479 -0
  31. capytaine/io/xarray.py +252 -100
  32. capytaine/matrices/block.py +4 -2
  33. capytaine/matrices/linear_solvers.py +1 -1
  34. capytaine/matrices/low_rank.py +3 -1
  35. capytaine/meshes/clipper.py +4 -3
  36. capytaine/meshes/collections.py +11 -1
  37. capytaine/meshes/mesh_like_protocol.py +37 -0
  38. capytaine/meshes/meshes.py +22 -9
  39. capytaine/meshes/properties.py +58 -24
  40. capytaine/meshes/symmetric.py +11 -2
  41. capytaine/post_pro/kochin.py +4 -4
  42. capytaine/tools/lists_of_points.py +3 -3
  43. capytaine/tools/prony_decomposition.py +60 -4
  44. capytaine/tools/symbolic_multiplication.py +30 -2
  45. capytaine/tools/timer.py +64 -0
  46. capytaine-2.3.dist-info/DELVEWHEEL +2 -0
  47. capytaine-2.3.dist-info/METADATA +761 -0
  48. capytaine-2.3.dist-info/RECORD +97 -0
  49. capytaine-2.2.dist-info/DELVEWHEEL +0 -2
  50. capytaine-2.2.dist-info/METADATA +0 -751
  51. capytaine-2.2.dist-info/RECORD +0 -81
  52. {capytaine-2.2.dist-info → capytaine-2.3.dist-info}/LICENSE +0 -0
  53. {capytaine-2.2.dist-info → capytaine-2.3.dist-info}/WHEEL +0 -0
  54. {capytaine-2.2.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,12 +106,15 @@ 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)
86
- _check_wavelength: bool, optional
87
- if True, check the mesh resolution with respect to the wavelength
115
+ _check_wavelength: bool, optional (default: True)
116
+ If True, the frequencies are compared to the mesh resolution and
117
+ the estimated first irregular frequency to warn the user.
88
118
 
89
119
  Returns
90
120
  -------
@@ -97,33 +127,54 @@ class BEMSolver:
97
127
  self._check_wavelength_and_mesh_resolution([problem])
98
128
  self._check_wavelength_and_irregular_frequencies([problem])
99
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
+
100
140
  if problem.forward_speed != 0.0:
101
141
  omega, wavenumber = problem.encounter_omega, problem.encounter_wavenumber
102
142
  else:
103
143
  omega, wavenumber = problem.omega, problem.wavenumber
104
144
 
105
145
  linear_solver = supporting_symbolic_multiplication(self.engine.linear_solver)
146
+ method = method if method is not None else self.method
106
147
  if (method == 'direct'):
107
148
  if problem.forward_speed != 0.0:
108
149
  raise NotImplementedError("Direct solver is not able to solve problems with forward speed.")
109
150
 
110
- S, D = self.engine.build_matrices(
111
- problem.body.mesh_including_lid, problem.body.mesh_including_lid,
112
- problem.free_surface, problem.water_depth, wavenumber,
113
- self.green_function, adjoint_double_layer=False
114
- )
115
-
116
- 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})")
117
163
  pressure = 1j * omega * problem.rho * potential
118
164
  sources = None
119
165
  else:
120
- S, K = self.engine.build_matrices(
121
- problem.body.mesh_including_lid, problem.body.mesh_including_lid,
122
- problem.free_surface, problem.water_depth, wavenumber,
123
- self.green_function, adjoint_double_layer=True
124
- )
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
+ )
125
172
 
126
- 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})")
127
178
  potential = S @ sources
128
179
  pressure = 1j * omega * problem.rho * potential
129
180
  if problem.forward_speed != 0.0:
@@ -144,7 +195,17 @@ class BEMSolver:
144
195
 
145
196
  return result
146
197
 
147
- 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):
148
209
  """Solve several problems.
149
210
  Optional keyword arguments are passed to `BEMSolver.solve`.
150
211
 
@@ -153,12 +214,20 @@ class BEMSolver:
153
214
  problems: list of LinearPotentialFlowProblem
154
215
  several problems to be solved
155
216
  method: string, optional
156
- 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.
157
220
  n_jobs: int, optional (default: 1)
158
221
  the number of jobs to run in parallel using the optional dependency `joblib`
159
222
  By defaults: do not use joblib and solve sequentially.
160
- progress_bar: bool, optional (default: True)
161
- 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.
228
+ _check_wavelength: bool, optional (default: True)
229
+ If True, the frequencies are compared to the mesh resolution and
230
+ the estimated first irregular frequency to warn the user.
162
231
 
163
232
  Returns
164
233
  -------
@@ -169,11 +238,23 @@ class BEMSolver:
169
238
  self._check_wavelength_and_mesh_resolution(problems)
170
239
  self._check_wavelength_and_irregular_frequencies(problems)
171
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
+
172
253
  if n_jobs == 1: # force sequential resolution
173
254
  problems = sorted(problems)
174
255
  if progress_bar:
175
256
  problems = track(problems, total=len(problems), description="Solving BEM problems")
176
- 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]
177
258
  else:
178
259
  joblib = silently_import_optional_dependency("joblib")
179
260
  if joblib is None:
@@ -186,12 +267,14 @@ class BEMSolver:
186
267
  total=len(groups_of_problems),
187
268
  description=f"Solving BEM problems with {n_jobs} threads:")
188
269
  results = [res for grp in groups_of_results for res in grp] # flatten the nested list
189
- return results
270
+ LOG.info("Solver timer summary:\n%s", self.timer_summary())
271
+ return results
190
272
 
191
273
  @staticmethod
192
274
  def _check_wavelength_and_mesh_resolution(problems):
193
275
  """Display a warning if some of the problems have a mesh resolution
194
276
  that might not be sufficient for the given wavelength."""
277
+ LOG.debug("Check wavelength with mesh resolution.")
195
278
  risky_problems = [pb for pb in problems
196
279
  if 0.0 < pb.wavelength < pb.body.minimal_computable_wavelength]
197
280
  nb_risky_problems = len(risky_problems)
@@ -218,8 +301,10 @@ class BEMSolver:
218
301
  @staticmethod
219
302
  def _check_wavelength_and_irregular_frequencies(problems):
220
303
  """Display a warning if some of the problems might encounter irregular frequencies."""
304
+ LOG.debug("Check wavelength with estimated irregular frequency.")
221
305
  risky_problems = [pb for pb in problems
222
- if pb.body.first_irregular_frequency_estimate() < 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]
223
308
  nb_risky_problems = len(risky_problems)
224
309
  if nb_risky_problems >= 1:
225
310
  if any(pb.body.lid_mesh is None for pb in problems):
@@ -244,7 +329,7 @@ class BEMSolver:
244
329
  + recommendation
245
330
  )
246
331
 
247
- def fill_dataset(self, dataset, bodies, *, method='indirect', n_jobs=1, **kwargs):
332
+ def fill_dataset(self, dataset, bodies, *, method=None, n_jobs=1, _check_wavelength=True, progress_bar=None, **kwargs):
248
333
  """Solve a set of problems defined by the coordinates of an xarray dataset.
249
334
 
250
335
  Parameters
@@ -255,12 +340,20 @@ class BEMSolver:
255
340
  The body or bodies involved in the problems
256
341
  They should all have different names.
257
342
  method: string, optional
258
- 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.
259
346
  n_jobs: int, optional (default: 1)
260
347
  the number of jobs to run in parallel using the optional dependency `joblib`
261
348
  By defaults: do not use joblib and solve sequentially.
262
- progress_bar: bool, optional (default: True)
263
- 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.
354
+ _check_wavelength: bool, optional (default: True)
355
+ If True, the frequencies are compared to the mesh resolution and
356
+ the estimated first irregular frequency to warn the user.
264
357
 
265
358
  Returns
266
359
  -------
@@ -268,14 +361,16 @@ class BEMSolver:
268
361
  """
269
362
  attrs = {'start_of_computation': datetime.now().isoformat(),
270
363
  **self.exportable_settings}
364
+ if method is not None: # Overrides the method in self.exportable_settings
365
+ attrs["method"] = method
271
366
  problems = problems_from_dataset(dataset, bodies)
272
367
  if 'theta' in dataset.coords:
273
- results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs)
368
+ results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
274
369
  kochin = kochin_data_array(results, dataset.coords['theta'])
275
370
  dataset = assemble_dataset(results, attrs=attrs, **kwargs)
276
371
  dataset.update(kochin)
277
372
  else:
278
- results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs)
373
+ results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
279
374
  dataset = assemble_dataset(results, attrs=attrs, **kwargs)
280
375
  return dataset
281
376
 
@@ -285,7 +380,7 @@ class BEMSolver:
285
380
 
286
381
  Parameters
287
382
  ----------
288
- 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
289
384
  Coordinates of the point(s) at which the potential should be computed
290
385
  result: LinearPotentialFlowResult
291
386
  The return of the BEM solver
@@ -305,7 +400,8 @@ class BEMSolver:
305
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.
306
401
  Please re-run the resolution with the indirect method and keep_details=True.""")
307
402
 
308
- 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)
309
405
  potential = S @ result.sources # Sum the contributions of all panels in the mesh
310
406
  return potential.reshape(output_shape)
311
407
 
@@ -317,7 +413,8 @@ class BEMSolver:
317
413
  They probably have not been stored by the solver because the option keep_details=True have not been set.
318
414
  Please re-run the resolution with this option.""")
319
415
 
320
- _, 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,
321
418
  early_dot_product=False)
322
419
  velocities = np.einsum('ijk,j->ik', gradG, result.sources) # Sum the contributions of all panels in the mesh
323
420
  return velocities.reshape((*output_shape, 3))
@@ -327,7 +424,7 @@ class BEMSolver:
327
424
 
328
425
  Parameters
329
426
  ----------
330
- 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
331
428
  Coordinates of the point(s) at which the velocity should be computed
332
429
  result: LinearPotentialFlowResult
333
430
  The return of the BEM solver
@@ -351,7 +448,7 @@ class BEMSolver:
351
448
 
352
449
  Parameters
353
450
  ----------
354
- 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
355
452
  Coordinates of the point(s) at which the pressure should be computed
356
453
  result: LinearPotentialFlowResult
357
454
  The return of the BEM solver
@@ -379,7 +476,7 @@ class BEMSolver:
379
476
 
380
477
  Parameters
381
478
  ----------
382
- 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
383
480
  Coordinates of the point(s) at which the free surface elevation should be computed
384
481
  result: LinearPotentialFlowResult
385
482
  The return of the BEM solver
@@ -418,7 +515,7 @@ class BEMSolver:
418
515
  ----------
419
516
  result : LinearPotentialFlowResult
420
517
  the return of the BEM solver
421
- mesh : Mesh or CollectionOfMeshes
518
+ mesh : MeshLike
422
519
  a mesh
423
520
  chunk_size: int, optional
424
521
  Number of lines to compute in the matrix.
@@ -5,11 +5,12 @@
5
5
  import logging
6
6
  import copy
7
7
  from itertools import chain, accumulate, zip_longest
8
- from functools import cached_property
8
+ from functools import cached_property, lru_cache
9
9
 
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,21 +70,28 @@ 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:
76
77
  raise TypeError("Unrecognized `mesh` object passed to the FloatingBody constructor.")
77
78
 
78
79
  if lid_mesh is not None:
79
- self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
80
+ if lid_mesh.nb_faces == 0:
81
+ LOG.warning("Lid mesh %s provided for body initialization is empty. The lid mesh is ignored.", lid_mesh)
82
+ self.lid_mesh = None
83
+ else:
84
+ self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
80
85
  else:
81
86
  self.lid_mesh = None
82
87
 
83
88
  if name is None and mesh is None:
84
89
  self.name = "dummy_body"
85
90
  elif name is None:
86
- self.name = self.mesh.name
91
+ if hasattr(self.mesh, "name"):
92
+ self.name = self.mesh.name
93
+ else:
94
+ self.name = "anonymous_body"
87
95
  else:
88
96
  self.name = name
89
97
 
@@ -93,7 +101,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
93
101
  else:
94
102
  self.center_of_mass = None
95
103
 
96
- 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:
97
105
  self.mesh.heal_mesh()
98
106
 
99
107
  if dofs is None:
@@ -108,6 +116,8 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
108
116
 
109
117
  LOG.info(f"New floating body: {self.__str__()}.")
110
118
 
119
+ self._check_dofs_shape_consistency()
120
+
111
121
  @staticmethod
112
122
  def from_meshio(mesh, name=None) -> 'FloatingBody':
113
123
  """Create a FloatingBody from a meshio mesh object.
@@ -131,7 +141,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
131
141
  @cached_property
132
142
  def mesh_including_lid(self):
133
143
  if self.lid_mesh is not None:
134
- return CollectionOfMeshes([self.mesh, self.lid_mesh])
144
+ return self.mesh.join_meshes(self.lid_mesh)
135
145
  else:
136
146
  return self.mesh
137
147
 
@@ -260,6 +270,14 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
260
270
  coords={'influenced_dof': list(self.dofs), 'radiating_dof': list(self.dofs)},
261
271
  )
262
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
+
263
281
  ###################
264
282
  # Hydrostatics #
265
283
  ###################
@@ -752,14 +770,14 @@ respective inertia coefficients are assigned as NaN.")
752
770
  if name is None:
753
771
  name = "+".join(body.name for body in bodies)
754
772
  meshes = CollectionOfMeshes(
755
- [body.mesh for body in bodies],
773
+ [body.mesh.copy() for body in bodies],
756
774
  name=f"{name}_mesh"
757
775
  )
758
776
  if all(body.lid_mesh is None for body in bodies):
759
777
  lid_meshes = None
760
778
  else:
761
779
  lid_meshes = CollectionOfMeshes(
762
- [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],
763
781
  name=f"{name}_lid_mesh"
764
782
  )
765
783
  dofs = FloatingBody.combine_dofs(bodies)
@@ -792,6 +810,8 @@ respective inertia coefficients are assigned as NaN.")
792
810
  @staticmethod
793
811
  def combine_dofs(bodies) -> dict:
794
812
  """Combine the degrees of freedom of several bodies."""
813
+ for body in bodies:
814
+ body._check_dofs_shape_consistency()
795
815
  dofs = {}
796
816
  cum_nb_faces = accumulate(chain([0], (body.mesh.nb_faces for body in bodies)))
797
817
  total_nb_faces = sum(body.mesh.nb_faces for body in bodies)
@@ -819,6 +839,8 @@ respective inertia coefficients are assigned as NaN.")
819
839
  name : str, optional
820
840
  a name for the new copy
821
841
  """
842
+ self._check_dofs_shape_consistency()
843
+
822
844
  new_body = copy.deepcopy(self)
823
845
  if name is None:
824
846
  new_body.name = f"copy_of_{self.name}"
@@ -874,9 +896,9 @@ respective inertia coefficients are assigned as NaN.")
874
896
  raise NotImplementedError # TODO
875
897
 
876
898
  if return_index:
877
- 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)
878
900
  else:
879
- 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)
880
902
  new_body = FloatingBody(new_mesh)
881
903
  LOG.info(f"Extract floating body from {self.name}.")
882
904
 
@@ -991,11 +1013,16 @@ respective inertia coefficients are assigned as NaN.")
991
1013
 
992
1014
  @inplace_transformation
993
1015
  def clip(self, plane):
1016
+ self._check_dofs_shape_consistency()
1017
+
994
1018
  # Clip mesh
995
1019
  LOG.info(f"Clipping {self.name} with respect to {plane}")
996
1020
  self.mesh.clip(plane)
997
1021
  if self.lid_mesh is not None:
998
1022
  self.lid_mesh.clip(plane)
1023
+ if self.lid_mesh.nb_faces == 0:
1024
+ LOG.warning("Lid mesh %s is empty after clipping. The lid mesh is removed.", self.lid_mesh)
1025
+ self.lid_mesh = None
999
1026
 
1000
1027
  # Clip dofs
1001
1028
  ids = self.mesh._clipping_data['faces_ids']
@@ -1100,6 +1127,7 @@ respective inertia coefficients are assigned as NaN.")
1100
1127
  else:
1101
1128
  return 8*self.mesh.faces_radiuses.max()
1102
1129
 
1130
+ @lru_cache
1103
1131
  def first_irregular_frequency_estimate(self, *, g=9.81):
1104
1132
  r"""Estimates the angular frequency of the lowest irregular
1105
1133
  frequency.
@@ -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/