capytaine 2.2.1__cp312-cp312-macosx_14_0_arm64.whl → 2.3.1__cp312-cp312-macosx_14_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
- capytaine/.dylibs/libgfortran.5.dylib +0 -0
- capytaine/.dylibs/libquadmath.0.dylib +0 -0
- capytaine/__about__.py +1 -1
- capytaine/__init__.py +2 -1
- capytaine/bem/airy_waves.py +7 -2
- capytaine/bem/problems_and_results.py +91 -39
- capytaine/bem/solver.py +128 -40
- capytaine/bodies/bodies.py +46 -18
- capytaine/bodies/predefined/rectangles.py +2 -0
- capytaine/green_functions/FinGreen3D/.gitignore +1 -0
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
- capytaine/green_functions/FinGreen3D/LICENSE +165 -0
- capytaine/green_functions/FinGreen3D/Makefile +16 -0
- capytaine/green_functions/FinGreen3D/README.md +24 -0
- capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
- capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
- capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
- capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
- capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
- capytaine/green_functions/abstract_green_function.py +55 -3
- capytaine/green_functions/delhommeau.py +205 -130
- capytaine/green_functions/hams.py +204 -0
- capytaine/green_functions/libs/Delhommeau_float32.cpython-312-darwin.so +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cpython-312-darwin.so +0 -0
- capytaine/io/bemio.py +14 -2
- capytaine/io/mesh_loaders.py +1 -1
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +261 -117
- capytaine/matrices/linear_solvers.py +1 -1
- capytaine/meshes/clipper.py +1 -0
- capytaine/meshes/collections.py +19 -1
- capytaine/meshes/mesh_like_protocol.py +37 -0
- capytaine/meshes/meshes.py +28 -8
- capytaine/meshes/symmetric.py +89 -10
- capytaine/post_pro/kochin.py +4 -4
- capytaine/tools/lists_of_points.py +3 -3
- capytaine/tools/prony_decomposition.py +60 -4
- capytaine/tools/symbolic_multiplication.py +30 -4
- capytaine/tools/timer.py +66 -0
- {capytaine-2.2.1.dist-info → capytaine-2.3.1.dist-info}/METADATA +6 -10
- capytaine-2.3.1.dist-info/RECORD +92 -0
- capytaine-2.3.1.dist-info/WHEEL +6 -0
- capytaine-2.2.1.dist-info/RECORD +0 -76
- capytaine-2.2.1.dist-info/WHEEL +0 -4
- {capytaine-2.2.1.dist-info → capytaine-2.3.1.dist-info}/LICENSE +0 -0
- {capytaine-2.2.1.dist-info → capytaine-2.3.1.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
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -133,7 +183,7 @@ class BEMSolver:
|
|
|
133
183
|
nabla_phi = self._compute_potential_gradient(problem.body.mesh_including_lid, result)
|
|
134
184
|
pressure += problem.rho * problem.forward_speed * nabla_phi[:, 0]
|
|
135
185
|
|
|
136
|
-
pressure_on_hull = pressure[
|
|
186
|
+
pressure_on_hull = pressure[problem.body.hull_mask] # Discards pressure on lid if any
|
|
137
187
|
forces = problem.body.integrate_pressure(pressure_on_hull)
|
|
138
188
|
|
|
139
189
|
if not keep_details:
|
|
@@ -145,7 +195,17 @@ class BEMSolver:
|
|
|
145
195
|
|
|
146
196
|
return result
|
|
147
197
|
|
|
148
|
-
def
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
518
|
+
mesh : MeshLike
|
|
431
519
|
a mesh
|
|
432
520
|
chunk_size: int, optional
|
|
433
521
|
Number of lines to compute in the matrix.
|
capytaine/bodies/bodies.py
CHANGED
|
@@ -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 :
|
|
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 :
|
|
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,
|
|
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
|
-
|
|
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:
|
|
@@ -110,8 +114,21 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
110
114
|
else:
|
|
111
115
|
self.dofs = dofs
|
|
112
116
|
|
|
117
|
+
self._evaluate_full_mesh()
|
|
118
|
+
|
|
113
119
|
LOG.info(f"New floating body: {self.__str__()}.")
|
|
114
120
|
|
|
121
|
+
self._check_dofs_shape_consistency()
|
|
122
|
+
|
|
123
|
+
def _evaluate_full_mesh(self):
|
|
124
|
+
"""Merge the mesh and lid_mesh, while keeping track of where each panel came from."""
|
|
125
|
+
if self.lid_mesh is None:
|
|
126
|
+
self.mesh_including_lid = self.mesh
|
|
127
|
+
self.hull_mask = np.full((self.mesh.nb_faces,), True)
|
|
128
|
+
else:
|
|
129
|
+
self.mesh_including_lid, masks = self.mesh.join_meshes(self.lid_mesh, return_masks=True)
|
|
130
|
+
self.hull_mask = masks[0]
|
|
131
|
+
|
|
115
132
|
@staticmethod
|
|
116
133
|
def from_meshio(mesh, name=None) -> 'FloatingBody':
|
|
117
134
|
"""Create a FloatingBody from a meshio mesh object.
|
|
@@ -132,13 +149,6 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
132
149
|
"""Arbitrary order. The point is to sort together the problems involving the same body."""
|
|
133
150
|
return self.name < other.name
|
|
134
151
|
|
|
135
|
-
@cached_property
|
|
136
|
-
def mesh_including_lid(self):
|
|
137
|
-
if self.lid_mesh is not None:
|
|
138
|
-
return CollectionOfMeshes([self.mesh, self.lid_mesh])
|
|
139
|
-
else:
|
|
140
|
-
return self.mesh
|
|
141
|
-
|
|
142
152
|
##########
|
|
143
153
|
# Dofs #
|
|
144
154
|
##########
|
|
@@ -264,6 +274,14 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
264
274
|
coords={'influenced_dof': list(self.dofs), 'radiating_dof': list(self.dofs)},
|
|
265
275
|
)
|
|
266
276
|
|
|
277
|
+
def _check_dofs_shape_consistency(self):
|
|
278
|
+
for dof_name, dof in self.dofs.items():
|
|
279
|
+
if np.array(dof).shape != (self.mesh.nb_faces, 3):
|
|
280
|
+
raise ValueError(f"The array defining the dof {dof_name} of body {self.name} does not have the expected shape.\n"
|
|
281
|
+
f"Expected shape: ({self.mesh.nb_faces}, 3)\n"
|
|
282
|
+
f" Actual shape: {dof.shape}")
|
|
283
|
+
|
|
284
|
+
|
|
267
285
|
###################
|
|
268
286
|
# Hydrostatics #
|
|
269
287
|
###################
|
|
@@ -556,7 +574,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
556
574
|
)
|
|
557
575
|
for radiating_dof_name in self.dofs
|
|
558
576
|
for influenced_dof_name in self.dofs
|
|
559
|
-
])
|
|
577
|
+
], compat='no_conflicts', join="outer")
|
|
560
578
|
|
|
561
579
|
# Reorder dofs
|
|
562
580
|
K = hs_set.hydrostatic_stiffness.sel(influenced_dof=list(self.dofs.keys()), radiating_dof=list(self.dofs.keys()))
|
|
@@ -646,7 +664,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
646
664
|
'radiating_dof': body_dof_names},
|
|
647
665
|
name="inertia_matrix")
|
|
648
666
|
|
|
649
|
-
total_mass_xr = xr.merge([rigid_inertia_matrix_xr, other_dofs_inertia_matrix_xr], compat="override").inertia_matrix
|
|
667
|
+
total_mass_xr = xr.merge([rigid_inertia_matrix_xr, other_dofs_inertia_matrix_xr], compat="override", join="outer").inertia_matrix
|
|
650
668
|
|
|
651
669
|
non_rigid_dofs = set(body_dof_names) - set(rigid_dof_names)
|
|
652
670
|
|
|
@@ -756,14 +774,14 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
756
774
|
if name is None:
|
|
757
775
|
name = "+".join(body.name for body in bodies)
|
|
758
776
|
meshes = CollectionOfMeshes(
|
|
759
|
-
[body.mesh for body in bodies],
|
|
777
|
+
[body.mesh.copy() for body in bodies],
|
|
760
778
|
name=f"{name}_mesh"
|
|
761
779
|
)
|
|
762
780
|
if all(body.lid_mesh is None for body in bodies):
|
|
763
781
|
lid_meshes = None
|
|
764
782
|
else:
|
|
765
783
|
lid_meshes = CollectionOfMeshes(
|
|
766
|
-
[body.lid_mesh for body in bodies if body.lid_mesh is not None],
|
|
784
|
+
[body.lid_mesh.copy() for body in bodies if body.lid_mesh is not None],
|
|
767
785
|
name=f"{name}_lid_mesh"
|
|
768
786
|
)
|
|
769
787
|
dofs = FloatingBody.combine_dofs(bodies)
|
|
@@ -796,6 +814,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
796
814
|
@staticmethod
|
|
797
815
|
def combine_dofs(bodies) -> dict:
|
|
798
816
|
"""Combine the degrees of freedom of several bodies."""
|
|
817
|
+
for body in bodies:
|
|
818
|
+
body._check_dofs_shape_consistency()
|
|
799
819
|
dofs = {}
|
|
800
820
|
cum_nb_faces = accumulate(chain([0], (body.mesh.nb_faces for body in bodies)))
|
|
801
821
|
total_nb_faces = sum(body.mesh.nb_faces for body in bodies)
|
|
@@ -823,6 +843,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
823
843
|
name : str, optional
|
|
824
844
|
a name for the new copy
|
|
825
845
|
"""
|
|
846
|
+
self._check_dofs_shape_consistency()
|
|
847
|
+
|
|
826
848
|
new_body = copy.deepcopy(self)
|
|
827
849
|
if name is None:
|
|
828
850
|
new_body.name = f"copy_of_{self.name}"
|
|
@@ -878,9 +900,9 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
878
900
|
raise NotImplementedError # TODO
|
|
879
901
|
|
|
880
902
|
if return_index:
|
|
881
|
-
new_mesh, id_v =
|
|
903
|
+
new_mesh, id_v = self.mesh.extract_faces(id_faces_to_extract, return_index)
|
|
882
904
|
else:
|
|
883
|
-
new_mesh =
|
|
905
|
+
new_mesh = self.mesh.extract_faces(id_faces_to_extract, return_index)
|
|
884
906
|
new_body = FloatingBody(new_mesh)
|
|
885
907
|
LOG.info(f"Extract floating body from {self.name}.")
|
|
886
908
|
|
|
@@ -962,6 +984,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
962
984
|
self.mesh.mirror(plane)
|
|
963
985
|
if self.lid_mesh is not None:
|
|
964
986
|
self.lid_mesh.mirror(plane)
|
|
987
|
+
self._evaluate_full_mesh()
|
|
965
988
|
for dof in self.dofs:
|
|
966
989
|
self.dofs[dof] -= 2 * np.outer(np.dot(self.dofs[dof], plane.normal), plane.normal)
|
|
967
990
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
@@ -976,6 +999,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
976
999
|
self.mesh.translate(vector, *args, **kwargs)
|
|
977
1000
|
if self.lid_mesh is not None:
|
|
978
1001
|
self.lid_mesh.translate(vector, *args, **kwargs)
|
|
1002
|
+
self._evaluate_full_mesh()
|
|
979
1003
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
980
1004
|
if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
|
|
981
1005
|
self.__dict__[point_attr] = np.array(self.__dict__[point_attr]) + vector
|
|
@@ -986,6 +1010,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
986
1010
|
self.mesh.rotate(axis, angle)
|
|
987
1011
|
if self.lid_mesh is not None:
|
|
988
1012
|
self.lid_mesh.rotate(axis, angle)
|
|
1013
|
+
self._evaluate_full_mesh()
|
|
989
1014
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
990
1015
|
if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
|
|
991
1016
|
self.__dict__[point_attr] = axis.rotate_points([self.__dict__[point_attr]], angle)[0, :]
|
|
@@ -995,6 +1020,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
995
1020
|
|
|
996
1021
|
@inplace_transformation
|
|
997
1022
|
def clip(self, plane):
|
|
1023
|
+
self._check_dofs_shape_consistency()
|
|
1024
|
+
|
|
998
1025
|
# Clip mesh
|
|
999
1026
|
LOG.info(f"Clipping {self.name} with respect to {plane}")
|
|
1000
1027
|
self.mesh.clip(plane)
|
|
@@ -1003,6 +1030,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
1003
1030
|
if self.lid_mesh.nb_faces == 0:
|
|
1004
1031
|
LOG.warning("Lid mesh %s is empty after clipping. The lid mesh is removed.", self.lid_mesh)
|
|
1005
1032
|
self.lid_mesh = None
|
|
1033
|
+
self._evaluate_full_mesh()
|
|
1006
1034
|
|
|
1007
1035
|
# Clip dofs
|
|
1008
1036
|
ids = self.mesh._clipping_data['faces_ids']
|
|
@@ -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/
|