capytaine 2.2__cp310-cp310-win_amd64.whl → 2.3__cp310-cp310-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.
- capytaine/__about__.py +1 -1
- capytaine/__init__.py +6 -6
- capytaine/bem/airy_waves.py +7 -2
- capytaine/bem/engines.py +2 -2
- capytaine/bem/problems_and_results.py +82 -35
- capytaine/bem/solver.py +138 -41
- capytaine/bodies/bodies.py +40 -12
- 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 +18 -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 +186 -115
- capytaine/green_functions/hams.py +204 -0
- capytaine/green_functions/libs/Delhommeau_float32.cp310-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp310-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
- capytaine/io/bemio.py +14 -2
- capytaine/io/mesh_loaders.py +2 -1
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +252 -100
- capytaine/matrices/block.py +4 -2
- capytaine/matrices/linear_solvers.py +1 -1
- capytaine/matrices/low_rank.py +3 -1
- capytaine/meshes/clipper.py +4 -3
- capytaine/meshes/collections.py +11 -1
- capytaine/meshes/mesh_like_protocol.py +37 -0
- capytaine/meshes/meshes.py +22 -9
- capytaine/meshes/properties.py +58 -24
- capytaine/meshes/symmetric.py +11 -2
- 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 -2
- capytaine/tools/timer.py +64 -0
- capytaine-2.3.dist-info/DELVEWHEEL +2 -0
- capytaine-2.3.dist-info/METADATA +761 -0
- capytaine-2.3.dist-info/RECORD +97 -0
- capytaine-2.2.dist-info/DELVEWHEEL +0 -2
- capytaine-2.2.dist-info/METADATA +0 -751
- capytaine-2.2.dist-info/RECORD +0 -81
- {capytaine-2.2.dist-info → capytaine-2.3.dist-info}/LICENSE +0 -0
- {capytaine-2.2.dist-info → capytaine-2.3.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
518
|
+
mesh : MeshLike
|
|
422
519
|
a mesh
|
|
423
520
|
chunk_size: int, optional
|
|
424
521
|
Number of lines to compute in the matrix.
|
capytaine/bodies/bodies.py
CHANGED
|
@@ -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 :
|
|
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,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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
899
|
+
new_mesh, id_v = self.mesh.extract_faces(id_faces_to_extract, return_index)
|
|
878
900
|
else:
|
|
879
|
-
new_mesh =
|
|
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/
|