capytaine 3.0.0a1__cp314-cp314-macosx_15_0_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
  2. capytaine/.dylibs/libgfortran.5.dylib +0 -0
  3. capytaine/.dylibs/libquadmath.0.dylib +0 -0
  4. capytaine/__about__.py +21 -0
  5. capytaine/__init__.py +32 -0
  6. capytaine/bem/__init__.py +0 -0
  7. capytaine/bem/airy_waves.py +111 -0
  8. capytaine/bem/engines.py +321 -0
  9. capytaine/bem/problems_and_results.py +601 -0
  10. capytaine/bem/solver.py +718 -0
  11. capytaine/bodies/__init__.py +4 -0
  12. capytaine/bodies/bodies.py +630 -0
  13. capytaine/bodies/dofs.py +146 -0
  14. capytaine/bodies/hydrostatics.py +540 -0
  15. capytaine/bodies/multibodies.py +216 -0
  16. capytaine/green_functions/Delhommeau_float32.cpython-314-darwin.so +0 -0
  17. capytaine/green_functions/Delhommeau_float64.cpython-314-darwin.so +0 -0
  18. capytaine/green_functions/__init__.py +2 -0
  19. capytaine/green_functions/abstract_green_function.py +64 -0
  20. capytaine/green_functions/delhommeau.py +522 -0
  21. capytaine/green_functions/hams.py +210 -0
  22. capytaine/io/__init__.py +0 -0
  23. capytaine/io/bemio.py +153 -0
  24. capytaine/io/legacy.py +228 -0
  25. capytaine/io/wamit.py +479 -0
  26. capytaine/io/xarray.py +673 -0
  27. capytaine/meshes/__init__.py +2 -0
  28. capytaine/meshes/abstract_meshes.py +375 -0
  29. capytaine/meshes/clean.py +302 -0
  30. capytaine/meshes/clip.py +347 -0
  31. capytaine/meshes/export.py +89 -0
  32. capytaine/meshes/geometry.py +259 -0
  33. capytaine/meshes/io.py +433 -0
  34. capytaine/meshes/meshes.py +826 -0
  35. capytaine/meshes/predefined/__init__.py +6 -0
  36. capytaine/meshes/predefined/cylinders.py +280 -0
  37. capytaine/meshes/predefined/rectangles.py +202 -0
  38. capytaine/meshes/predefined/spheres.py +55 -0
  39. capytaine/meshes/quality.py +159 -0
  40. capytaine/meshes/surface_integrals.py +82 -0
  41. capytaine/meshes/symmetric_meshes.py +641 -0
  42. capytaine/meshes/visualization.py +353 -0
  43. capytaine/post_pro/__init__.py +6 -0
  44. capytaine/post_pro/free_surfaces.py +85 -0
  45. capytaine/post_pro/impedance.py +92 -0
  46. capytaine/post_pro/kochin.py +54 -0
  47. capytaine/post_pro/rao.py +60 -0
  48. capytaine/tools/__init__.py +0 -0
  49. capytaine/tools/block_circulant_matrices.py +275 -0
  50. capytaine/tools/cache_on_disk.py +26 -0
  51. capytaine/tools/deprecation_handling.py +18 -0
  52. capytaine/tools/lists_of_points.py +52 -0
  53. capytaine/tools/memory_monitor.py +45 -0
  54. capytaine/tools/optional_imports.py +27 -0
  55. capytaine/tools/prony_decomposition.py +150 -0
  56. capytaine/tools/symbolic_multiplication.py +161 -0
  57. capytaine/tools/timer.py +90 -0
  58. capytaine/ui/__init__.py +0 -0
  59. capytaine/ui/cli.py +28 -0
  60. capytaine/ui/rich.py +5 -0
  61. capytaine-3.0.0a1.dist-info/LICENSE +674 -0
  62. capytaine-3.0.0a1.dist-info/METADATA +755 -0
  63. capytaine-3.0.0a1.dist-info/RECORD +65 -0
  64. capytaine-3.0.0a1.dist-info/WHEEL +6 -0
  65. capytaine-3.0.0a1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,718 @@
1
+ # Copyright (C) 2017-2024 Matthieu Ancellin
2
+ # See LICENSE file at <https://github.com/capytaine/capytaine>
3
+ """Solver for the BEM problem.
4
+
5
+ .. code-block:: python
6
+
7
+ problem = RadiationProblem(...)
8
+ result = BEMSolver(engine=..., method=...).solve(problem)
9
+
10
+ """
11
+
12
+ import os
13
+ import shutil
14
+ import textwrap
15
+ import logging
16
+
17
+ import numpy as np
18
+
19
+ from datetime import datetime
20
+ from collections import defaultdict
21
+
22
+ from rich.progress import track
23
+
24
+ from capytaine.bem.problems_and_results import LinearPotentialFlowProblem, DiffractionProblem
25
+ from capytaine.bem.engines import BasicMatrixEngine
26
+ from capytaine.io.xarray import problems_from_dataset, assemble_dataset, kochin_data_array
27
+ from capytaine.tools.memory_monitor import MemoryMonitor
28
+ from capytaine.tools.optional_imports import silently_import_optional_dependency
29
+ from capytaine.tools.lists_of_points import _normalize_points, _normalize_free_surface_points
30
+ from capytaine.tools.symbolic_multiplication import supporting_symbolic_multiplication
31
+ from capytaine.tools.timer import Timer
32
+
33
+ LOG = logging.getLogger(__name__)
34
+
35
+ # Mapping between a dtype and its complex version
36
+ COMPLEX_DTYPE = {
37
+ np.float32: np.complex64,
38
+ np.float64: np.complex128,
39
+ np.complex64 : np.complex64,
40
+ np.complex128 : np.complex128
41
+ }
42
+
43
+ class BEMSolver:
44
+ """
45
+ Solver for linear potential flow problems.
46
+
47
+ Parameters
48
+ ----------
49
+ engine: MatrixEngine, optional
50
+ Object handling the building of matrices and the resolution of linear systems with these matrices.
51
+ (default: :class:`~capytaine.bem.engines.BasicMatrixEngine`)
52
+ method: string, optional
53
+ select boundary integral equation used to solve the problems.
54
+ Accepted values: "indirect" (as in e.g. Nemoh), "direct" (as in e.g. WAMIT)
55
+ Default value: "indirect"
56
+ green_function: AbstractGreenFunction, optional
57
+ For convenience and backward compatibility, the Green function can be
58
+ set here if the engine is the default one.
59
+ This argument is just passed to the default engine at initialization.
60
+
61
+ Attributes
62
+ ----------
63
+ timer: Timer
64
+ Storing the time spent on each subtasks of the resolution
65
+ exportable_settings : dict
66
+ Settings of the solver that can be saved to reinit the same solver later.
67
+ """
68
+
69
+ def __init__(self, *, green_function=None, engine=None, method="indirect"):
70
+
71
+ if engine is None:
72
+ self.engine = BasicMatrixEngine(green_function=green_function)
73
+ else:
74
+ if green_function is not None:
75
+ raise ValueError("If you are not using the default engine, set the Green function in the engine.\n"
76
+ "Setting the Green function in the solver is only a shortcut to set up "
77
+ "the Green function of the default engine since Capytaine version 3.0")
78
+ self.engine = engine
79
+
80
+ if method.lower() not in {"direct", "indirect"}:
81
+ raise ValueError(f"Unrecognized method when initializing solver: {repr(method)}. Expected \"direct\" or \"indirect\".")
82
+ self.method = method.lower()
83
+
84
+ self.reset_timer()
85
+
86
+ self.exportable_settings = {
87
+ **self.engine.exportable_settings,
88
+ "method": self.method,
89
+ }
90
+
91
+ def __str__(self):
92
+ return f"BEMSolver(engine={self.engine}, method={self.method})"
93
+
94
+ def __repr__(self):
95
+ return self.__str__()
96
+
97
+ def reset_timer(self):
98
+ self.timer = Timer(default_tags={"process": 0})
99
+ self.solve = self.timer.wraps_function(step="Total solve function")(self._solve)
100
+
101
+ def timer_summary(self):
102
+ df = self.timer.as_dataframe()
103
+ df["step"] = df["step"].where(
104
+ df["step"].str.startswith("Total"), " " + df["step"]
105
+ )
106
+ total = (
107
+ df.groupby(["step", "process"])
108
+ ["timing"].sum()
109
+ .unstack()
110
+ )
111
+ return total
112
+
113
+ def displayed_total_summary(self, width=None):
114
+ total = self.timer_summary()
115
+ if width is None:
116
+ width = shutil.get_terminal_size(fallback=(80, 20)).columns - 25
117
+ total_str = total.to_string(
118
+ float_format="{:.2f}".format,
119
+ line_width=width,
120
+ )
121
+ return textwrap.indent(total_str, " ")
122
+
123
+ def _repr_pretty_(self, p, cycle):
124
+ p.text(self.__str__())
125
+
126
+ @classmethod
127
+ def from_exported_settings(settings):
128
+ raise NotImplementedError
129
+
130
+ def _solve(self, problem, method=None, keep_details=True, _check_wavelength=True):
131
+ """Solve the linear potential flow problem.
132
+
133
+ Parameters
134
+ ----------
135
+ problem: LinearPotentialFlowProblem
136
+ the problem to be solved
137
+ method: string, optional
138
+ select boundary integral equation used to solve the problem.
139
+ It is recommended to set the method more globally when initializing the solver.
140
+ If provided here, the value in argument of `solve` overrides the global one.
141
+ keep_details: bool, optional
142
+ if True, store the sources and the potential on the floating body in the output object
143
+ (default: True)
144
+ _check_wavelength: bool, optional (default: True)
145
+ If True, the frequencies are compared to the mesh resolution and
146
+ the estimated first irregular frequency to warn the user.
147
+
148
+ Returns
149
+ -------
150
+ LinearPotentialFlowResult
151
+ an object storing the problem data and its results
152
+ """
153
+ LOG.info("Solve %s.", problem)
154
+
155
+ if _check_wavelength:
156
+ self._check_wavelength_and_mesh_resolution([problem])
157
+ self._check_wavelength_and_irregular_frequencies([problem])
158
+
159
+ if isinstance(problem, DiffractionProblem) and float(problem.encounter_omega) in {0.0, np.inf}:
160
+ raise ValueError("Diffraction problems at zero or infinite frequency are not defined")
161
+ # This error used to be raised when initializing the problem.
162
+ # It is now raised here, in order to be catchable by
163
+ # _solve_and_catch_errors, such that batch resolution
164
+ # can include this kind of problems without the full batch
165
+ # failing.
166
+ # Note that if this error was not raised here, the resolution
167
+ # would still fail with a less explicit error message.
168
+
169
+ if problem.forward_speed != 0.0:
170
+ omega, wavenumber = problem.encounter_omega, problem.encounter_wavenumber
171
+ else:
172
+ omega, wavenumber = problem.omega, problem.wavenumber
173
+ gf_params = dict(free_surface=problem.free_surface, water_depth=problem.water_depth, wavenumber=wavenumber)
174
+
175
+ linear_solver = supporting_symbolic_multiplication(self.engine.linear_solver)
176
+ method = method if method is not None else self.method
177
+ if (method == 'direct'):
178
+ if problem.forward_speed != 0.0:
179
+ raise NotImplementedError("Direct solver is not able to solve problems with forward speed.")
180
+
181
+ with self.timer(step="Green function"):
182
+ S, D = self.engine.build_matrices(
183
+ problem.body.mesh_including_lid, problem.body.mesh_including_lid,
184
+ **gf_params, adjoint_double_layer=False, diagonal_term_in_double_layer=True,
185
+ )
186
+ with self.timer(step="Matrix-vector product"):
187
+ rhs = S @ problem.boundary_condition
188
+ with self.timer(step="Linear solver"):
189
+ rhs = rhs.astype(COMPLEX_DTYPE[D.dtype.type])
190
+ potential = linear_solver(D, rhs)
191
+ pressure = 1j * omega * problem.rho * potential
192
+ sources = None
193
+ else:
194
+ with self.timer(step="Green function"):
195
+ S, K = self.engine.build_matrices(
196
+ problem.body.mesh_including_lid, problem.body.mesh_including_lid,
197
+ **gf_params, adjoint_double_layer=True, diagonal_term_in_double_layer=True,
198
+ )
199
+ with self.timer(step="Linear solver"):
200
+ rhs = problem.boundary_condition.astype(COMPLEX_DTYPE[K.dtype.type])
201
+ sources = linear_solver(K, rhs)
202
+ with self.timer(step="Matrix-vector product"):
203
+ potential = S @ sources
204
+ pressure = 1j * omega * problem.rho * potential
205
+ if problem.forward_speed != 0.0:
206
+ result = problem.make_results_container(sources=sources)
207
+ # Temporary result object to compute the ∇Φ term
208
+ nabla_phi = self._compute_potential_gradient(problem.body.mesh_including_lid, result)
209
+ pressure += problem.rho * problem.forward_speed * nabla_phi[:, 0]
210
+
211
+ pressure_on_hull = pressure[problem.body.hull_mask] # Discards pressure on lid if any
212
+ forces = problem.body.integrate_pressure(pressure_on_hull)
213
+ if not keep_details:
214
+ result = problem.make_results_container(forces)
215
+ else:
216
+ result = problem.make_results_container(forces, sources, potential, pressure)
217
+
218
+ LOG.debug("Done!")
219
+
220
+ return result
221
+
222
+ def _solve_and_catch_errors(self, problem, *args, _display_errors, **kwargs):
223
+ """Same as BEMSolver.solve() but returns a
224
+ FailedLinearPotentialFlowResult when the resolution failed."""
225
+ try:
226
+ res = self.solve(problem, *args, **kwargs)
227
+ except Exception as e:
228
+ res = problem.make_failed_results_container(e)
229
+ if _display_errors:
230
+ self._display_errors([res])
231
+ return res
232
+
233
+ @staticmethod
234
+ def _display_errors(results):
235
+ """Displays errors that occur during the solver execution and groups them according
236
+ to the problem type and exception type for easier reading."""
237
+ failed_results = defaultdict(list)
238
+ for res in results:
239
+ if hasattr(res, "exception") and hasattr(res, "problem"):
240
+ key = (type(res.exception), str(res.exception), res.problem.omega, res.problem.water_depth, res.problem.forward_speed)
241
+ failed_results[key].append(res.problem)
242
+
243
+ for (exc_type, exc_msg, omega, water_depth, forward_speed), problems in failed_results.items():
244
+ nb = len(problems)
245
+ if nb > 1:
246
+ if forward_speed != 0.0:
247
+ LOG.warning("Skipped %d problems for body=%s, omega=%s, water_depth=%s, forward_speed=%s\nbecause of %s(%r)",
248
+ nb, problems[0].body.__short_str__(), omega, water_depth, forward_speed, exc_type.__name__, exc_msg)
249
+ else:
250
+ LOG.warning("Skipped %d problems for body=%s, omega=%s, water_depth=%s\nbecause of %s(%r)",
251
+ nb, problems[0].body.__short_str__(), omega, water_depth, exc_type.__name__, exc_msg)
252
+ else:
253
+ LOG.warning("Skipped %s\nbecause of %s(%r)", problems[0], exc_type.__name__, exc_msg)
254
+
255
+ def solve_all(self, problems, *, method=None, n_jobs=1, n_threads=None, progress_bar=None, _check_wavelength=True, _display_errors=True, **kwargs):
256
+ """Solve several problems.
257
+ Optional keyword arguments are passed to `BEMSolver.solve`.
258
+
259
+ Parameters
260
+ ----------
261
+ problems: list of LinearPotentialFlowProblem
262
+ several problems to be solved
263
+ method: string, optional
264
+ select boundary integral equation used to solve the problems.
265
+ It is recommended to set the method more globally when initializing the solver.
266
+ If provided here, the value in argument of `solve_all` overrides the global one.
267
+ n_jobs: int, optional (default: 1)
268
+ the number of jobs to run in parallel using the optional dependency ``joblib``
269
+ By defaults: do not use joblib and solve sequentially.
270
+ n_threads: int, optional
271
+ the number of threads used to solve each problem.
272
+ The total number of used CPU will be n_jobs×n_threads.
273
+ By default: use as much as possible.
274
+ Requires the optional dependency ``threadpoolctl`` if ``n_jobs==1``.
275
+ Also controlled by the environment variables ``OMP_NUM_THREADS`` and ``MKL_NUM_THREADS``.
276
+ progress_bar: bool, optional
277
+ Display a progress bar while solving.
278
+ If no value is provided to this method directly,
279
+ check whether the environment variable `CAPYTAINE_PROGRESS_BAR` is defined
280
+ and otherwise default to True.
281
+ _check_wavelength: bool, optional (default: True)
282
+ If True, the frequencies are compared to the mesh resolution and
283
+ the estimated first irregular frequency to warn the user.
284
+
285
+ Returns
286
+ -------
287
+ list of LinearPotentialFlowResult
288
+ the solved problems
289
+ """
290
+ if _check_wavelength:
291
+ self._check_wavelength_and_mesh_resolution(problems)
292
+ self._check_wavelength_and_irregular_frequencies(problems)
293
+
294
+ self._check_ram(problems, n_jobs)
295
+
296
+ if progress_bar is None:
297
+ if "CAPYTAINE_PROGRESS_BAR" in os.environ:
298
+ env_var = os.environ["CAPYTAINE_PROGRESS_BAR"].lower()
299
+ if env_var in {'true', '1', 't'}:
300
+ progress_bar = True
301
+ elif env_var in {'false', '0', 'f'}:
302
+ progress_bar = False
303
+ else:
304
+ raise ValueError("Invalid value '{}' for the environment variable CAPYTAINE_PROGRESS_BAR.".format(os.environ["CAPYTAINE_PROGRESS_BAR"]))
305
+ else:
306
+ progress_bar = True
307
+
308
+ monitor = MemoryMonitor()
309
+ if n_jobs == 1: # force sequential resolution
310
+ problems = sorted(problems)
311
+ if progress_bar:
312
+ problems = track(problems, total=len(problems), description="Solving BEM problems")
313
+ if n_threads is None:
314
+ results = [self._solve_and_catch_errors(pb, method=method, _display_errors=_display_errors, _check_wavelength=False, **kwargs) for pb in problems]
315
+ else:
316
+ threadpoolctl = silently_import_optional_dependency("threadpoolctl")
317
+ if threadpoolctl is None:
318
+ raise ImportError(f"Setting the `n_threads` argument to {n_threads} with `n_jobs=1` requires the missing optional dependency 'threadpoolctl'.")
319
+ with threadpoolctl.threadpool_limits(limits=n_threads):
320
+ results = [self._solve_and_catch_errors(pb, method=method, _display_errors=_display_errors, _check_wavelength=False, **kwargs) for pb in problems]
321
+ else:
322
+ joblib = silently_import_optional_dependency("joblib")
323
+ if joblib is None:
324
+ raise ImportError(f"Setting the `n_jobs` argument to {n_jobs} requires the missing optional dependency 'joblib'.")
325
+ groups_of_problems = LinearPotentialFlowProblem._group_for_parallel_resolution(problems)
326
+ with joblib.parallel_config(backend='loky', inner_max_num_threads=n_threads):
327
+ parallel = joblib.Parallel(return_as="generator", n_jobs=n_jobs)
328
+ groups_of_results = parallel(joblib.delayed(self._solve_all_and_return_timer)(grp, method=method, n_threads=None, progress_bar=False, _display_errors=False, _check_wavelength=False, **kwargs) for grp in groups_of_problems)
329
+ if progress_bar:
330
+ groups_of_results = track(groups_of_results,
331
+ total=len(groups_of_problems),
332
+ description=f"Solving BEM problems with {n_jobs} processes:")
333
+ results = []
334
+ process_id_mapping = {}
335
+ for grp_results, other_timer, process_id in groups_of_results:
336
+ results.extend(grp_results)
337
+ self._display_errors(grp_results)
338
+ if process_id not in process_id_mapping:
339
+ process_id_mapping[process_id] = len(process_id_mapping) + 1
340
+ self.timer.add_data_from_other_timer(other_timer, process=process_id_mapping[process_id])
341
+ memory_peak = monitor.get_memory_peak()
342
+ if memory_peak is None:
343
+ LOG.info("Actual peak RAM usage: Not measured since optional dependency `psutil` cannot be found.")
344
+ else:
345
+ LOG.info(f"Actual peak RAM usage: {memory_peak} GB.")
346
+ LOG.info("Solver timer summary (in seconds):\n%s", self.displayed_total_summary())
347
+ return results
348
+
349
+ def _solve_all_and_return_timer(
350
+ self, grp, *,
351
+ method, n_threads,
352
+ progress_bar, _check_wavelength, **kwargs
353
+ ):
354
+ # This method is only called in joblib's Parallel loop.
355
+ # It contains some pre-processing and post-processing that
356
+ # should be done in each process when solving the batch of problems.
357
+
358
+ self.reset_timer()
359
+ # Timer data will be concatenated to the Timer of process 0.
360
+ # We reset the timer at each call to avoid concatenating
361
+ # the same data twice in the main process.
362
+
363
+ results = self.solve_all(
364
+ grp, method=method, n_jobs=1,
365
+ n_threads=n_threads, progress_bar=progress_bar,
366
+ _check_wavelength=_check_wavelength, **kwargs
367
+ )
368
+ return results, self.timer, os.getpid()
369
+
370
+ @staticmethod
371
+ def _check_wavelength_and_mesh_resolution(problems):
372
+ """Display a warning if some of the problems have a mesh resolution
373
+ that might not be sufficient for the given wavelength."""
374
+ LOG.debug("Check wavelength with mesh resolution.")
375
+ risky_problems = [pb for pb in problems
376
+ if 0.0 < pb.wavelength < pb.body.minimal_computable_wavelength]
377
+ nb_risky_problems = len(risky_problems)
378
+ if nb_risky_problems == 1:
379
+ pb = risky_problems[0]
380
+ freq_type = risky_problems[0].provided_freq_type
381
+ freq = pb.__getattribute__(freq_type)
382
+ LOG.warning(f"Mesh resolution for {pb}:\n"
383
+ f"The resolution of the mesh of the body {pb.body.__short_str__()} might "
384
+ f"be insufficient for {freq_type}={freq}.\n"
385
+ "This warning appears because the largest panel of this mesh "
386
+ f"has radius {pb.body.mesh.faces_radiuses.max():.3f} > wavelength/8."
387
+ )
388
+ elif nb_risky_problems > 1:
389
+ freq_type = risky_problems[0].provided_freq_type
390
+ freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
391
+ LOG.warning(f"Mesh resolution for {nb_risky_problems} problems:\n"
392
+ "The resolution of the mesh might be insufficient "
393
+ f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
394
+ "This warning appears when the largest panel of this mesh "
395
+ "has radius > wavelength/8."
396
+ )
397
+
398
+ @staticmethod
399
+ def _check_wavelength_and_irregular_frequencies(problems):
400
+ """Display a warning if some of the problems might encounter irregular frequencies."""
401
+ LOG.debug("Check wavelength with estimated irregular frequency.")
402
+ risky_problems = [pb for pb in problems
403
+ if pb.free_surface != np.inf and
404
+ pb.body.first_irregular_frequency_estimate(g=pb.g) < pb.omega < np.inf]
405
+ nb_risky_problems = len(risky_problems)
406
+ if nb_risky_problems >= 1:
407
+ if any(pb.body.lid_mesh is None for pb in problems):
408
+ recommendation = "Setting a lid for the floating body is recommended."
409
+ else:
410
+ recommendation = "The lid might need to be closer to the free surface."
411
+ if nb_risky_problems == 1:
412
+ pb = risky_problems[0]
413
+ freq_type = risky_problems[0].provided_freq_type
414
+ freq = pb.__getattribute__(freq_type)
415
+ LOG.warning(f"Irregular frequencies for {pb}:\n"
416
+ f"The body {pb.body.__short_str__()} might display irregular frequencies "
417
+ f"for {freq_type}={freq}.\n"
418
+ + recommendation
419
+ )
420
+ elif nb_risky_problems > 1:
421
+ freq_type = risky_problems[0].provided_freq_type
422
+ freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
423
+ LOG.warning(f"Irregular frequencies for {nb_risky_problems} problems:\n"
424
+ "Irregular frequencies might be encountered "
425
+ f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
426
+ + recommendation
427
+ )
428
+
429
+ def _check_ram(self,problems, n_jobs = 1):
430
+ """Display a warning if the RAM estimation is larger than a certain limit."""
431
+ LOG.debug("Check RAM estimation.")
432
+ psutil = silently_import_optional_dependency("psutil")
433
+ if psutil is None:
434
+ ram_limit = 8
435
+ else :
436
+ ram_limit = psutil.virtual_memory().total / (1024**3) * 0.3
437
+
438
+ if n_jobs == - 1:
439
+ n_jobs = os.cpu_count()
440
+
441
+ estimated_peak_memory = n_jobs*max(self.engine.compute_ram_estimation(pb) for pb in problems)
442
+
443
+ if estimated_peak_memory < 0.5:
444
+ LOG.info("Estimated peak RAM usage: <1 GB.")
445
+
446
+ elif estimated_peak_memory < ram_limit:
447
+ LOG.info(f"Estimated peak RAM usage: {int(np.ceil(estimated_peak_memory))} GB.")
448
+
449
+ else:
450
+ LOG.warning(f"Estimated peak RAM usage: {int(np.ceil(estimated_peak_memory))} GB.")
451
+
452
+ def fill_dataset(self, dataset, bodies, *, method=None, n_jobs=1, n_threads=None, _check_wavelength=True, progress_bar=None, **kwargs):
453
+ """Solve a set of problems defined by the coordinates of an xarray dataset.
454
+
455
+ Parameters
456
+ ----------
457
+ dataset : xarray Dataset
458
+ dataset containing the problems parameters: frequency, radiating_dof, water_depth, ...
459
+ bodies : FloatingBody or Multibody or list of FloatingBody or list of Multibody
460
+ The body or bodies involved in the problems
461
+ They should all have different names.
462
+ method: string, optional
463
+ select boundary integral equation used to solve the problems.
464
+ It is recommended to set the method more globally when initializing the solver.
465
+ If provided here, the value in argument of `fill_dataset` overrides the global one.
466
+ n_jobs: int, optional (default: 1)
467
+ the number of jobs to run in parallel using the optional dependency ``joblib``.
468
+ By defaults: do not use joblib and solve sequentially.
469
+ n_threads: int, optional
470
+ the number of threads used to solve each problem.
471
+ The total number of used CPU will be n_jobs×n_threads.
472
+ By default: use as much as possible.
473
+ Requires the optional dependency ``threadpoolctl`` if ``n_jobs==1``.
474
+ Also controlled by the environment variables ``OMP_NUM_THREADS`` and ``MKL_NUM_THREADS``.
475
+ progress_bar: bool, optional
476
+ Display a progress bar while solving.
477
+ If no value is provided to this method directly,
478
+ check whether the environment variable `CAPYTAINE_PROGRESS_BAR` is defined
479
+ and otherwise default to True.
480
+ _check_wavelength: bool, optional (default: True)
481
+ If True, the frequencies are compared to the mesh resolution and
482
+ the estimated first irregular frequency to warn the user.
483
+
484
+ Returns
485
+ -------
486
+ xarray Dataset
487
+ """
488
+ attrs = {'start_of_computation': datetime.now().isoformat(),
489
+ **self.exportable_settings}
490
+ if method is not None: # Overrides the method in self.exportable_settings
491
+ attrs["method"] = method
492
+ problems = problems_from_dataset(dataset, bodies)
493
+ if 'theta' in dataset.coords:
494
+ results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs, n_threads=n_threads, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
495
+ kochin = kochin_data_array(results, dataset.coords['theta'])
496
+ dataset = assemble_dataset(results, attrs=attrs, **kwargs)
497
+ dataset.update(kochin)
498
+ else:
499
+ results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs, n_threads=n_threads, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
500
+ dataset = assemble_dataset(results, attrs=attrs, **kwargs)
501
+ return dataset
502
+
503
+
504
+ def compute_potential(self, points, result):
505
+ """Compute the value of the potential at given points for a previously solved potential flow problem.
506
+
507
+ Parameters
508
+ ----------
509
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
510
+ Coordinates of the point(s) at which the potential should be computed
511
+ result: LinearPotentialFlowResult
512
+ The return of the BEM solver
513
+
514
+ Returns
515
+ -------
516
+ complex-valued array of shape (1,) or (N,) or (nx, ny, nz) or (mesh.nb_faces,) depending of the kind of input
517
+ The value of the potential at the points
518
+
519
+ Raises
520
+ ------
521
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
522
+ """
523
+ gf_params = dict(free_surface=result.free_surface, water_depth=result.water_depth, wavenumber=result.encounter_wavenumber)
524
+
525
+ points, output_shape = _normalize_points(points)
526
+ if result.sources is None:
527
+ raise Exception(f"""The values of the sources of {result} cannot been found.
528
+ 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.
529
+ Please re-run the resolution with the indirect method and keep_details=True.""")
530
+
531
+ with self.timer(step="Post-processing potential"):
532
+ S = self.engine.build_S_matrix(points, result.body.mesh_including_lid, **gf_params)
533
+ potential = S @ result.sources # Sum the contributions of all panels in the mesh
534
+ return potential.reshape(output_shape)
535
+
536
+ def _compute_potential_gradient(self, points, result):
537
+ points, output_shape = _normalize_points(points, keep_mesh=True)
538
+ # keep_mesh, because we need the normal vectors associated with each collocation points to compute the fullK matrix
539
+
540
+ if result.sources is None:
541
+ raise Exception(f"""The values of the sources of {result} cannot been found.
542
+ They probably have not been stored by the solver because the option keep_details=True have not been set.
543
+ Please re-run the resolution with this option.""")
544
+
545
+ gf_params = dict(free_surface=result.free_surface, water_depth=result.water_depth, wavenumber=result.encounter_wavenumber)
546
+ with self.timer(step="Post-processing velocity"):
547
+ gradG = self.engine.build_fullK_matrix(points, result.body.mesh_including_lid, **gf_params)
548
+ velocities = np.einsum('ijk,j->ik', gradG, result.sources) # Sum the contributions of all panels in the mesh
549
+ return velocities.reshape((*output_shape, 3))
550
+
551
+ def compute_velocity(self, points, result):
552
+ """Compute the value of the velocity vector at given points for a previously solved potential flow problem.
553
+
554
+ Parameters
555
+ ----------
556
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
557
+ Coordinates of the point(s) at which the velocity should be computed
558
+ result: LinearPotentialFlowResult
559
+ The return of the BEM solver
560
+
561
+ Returns
562
+ -------
563
+ complex-valued array of shape (3,) or (N,, 3) or (nx, ny, nz, 3) or (mesh.nb_faces, 3) depending of the kind of input
564
+ The value of the velocity at the points
565
+
566
+ Raises
567
+ ------
568
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
569
+ """
570
+ nabla_phi = self._compute_potential_gradient(points, result)
571
+ if result.forward_speed != 0.0:
572
+ nabla_phi[..., 0] -= result.forward_speed
573
+ return nabla_phi
574
+
575
+ def compute_pressure(self, points, result):
576
+ """Compute the value of the pressure at given points for a previously solved potential flow problem.
577
+
578
+ Parameters
579
+ ----------
580
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
581
+ Coordinates of the point(s) at which the pressure should be computed
582
+ result: LinearPotentialFlowResult
583
+ The return of the BEM solver
584
+
585
+ Returns
586
+ -------
587
+ complex-valued array of shape (1,) or (N,) or (nx, ny, nz) or (mesh.nb_faces,) depending of the kind of input
588
+ The value of the pressure at the points
589
+
590
+ Raises
591
+ ------
592
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
593
+ """
594
+ if result.forward_speed != 0:
595
+ pressure = 1j * result.encounter_omega * result.rho * self.compute_potential(points, result)
596
+ nabla_phi = self._compute_potential_gradient(points, result)
597
+ pressure += result.rho * result.forward_speed * nabla_phi[..., 0]
598
+ else:
599
+ pressure = 1j * result.omega * result.rho * self.compute_potential(points, result)
600
+ return pressure
601
+
602
+
603
+ def compute_free_surface_elevation(self, points, result):
604
+ """Compute the value of the free surface elevation at given points for a previously solved potential flow problem.
605
+
606
+ Parameters
607
+ ----------
608
+ points: array of shape (2,) or (N, 2), or 2-ple of arrays returned by meshgrid, or MeshLike object
609
+ Coordinates of the point(s) at which the free surface elevation should be computed
610
+ result: LinearPotentialFlowResult
611
+ The return of the BEM solver
612
+
613
+ Returns
614
+ -------
615
+ complex-valued array of shape (1,) or (N,) or (nx, ny, nz) or (mesh.nb_faces,) depending of the kind of input
616
+ The value of the free surface elevation at the points
617
+
618
+ Raises
619
+ ------
620
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
621
+ """
622
+ points, output_shape = _normalize_free_surface_points(points)
623
+
624
+ if result.forward_speed != 0:
625
+ fs_elevation = -1/result.g * (-1j*result.encounter_omega) * self.compute_potential(points, result)
626
+ nabla_phi = self._compute_potential_gradient(points, result)
627
+ fs_elevation += -1/result.g * result.forward_speed * nabla_phi[..., 0]
628
+ else:
629
+ fs_elevation = -1/result.g * (-1j*result.omega) * self.compute_potential(points, result)
630
+
631
+ return fs_elevation.reshape(output_shape)
632
+
633
+
634
+ ## Legacy
635
+
636
+ def get_potential_on_mesh(self, result, mesh, chunk_size=50):
637
+ """Compute the potential on a mesh for the potential field of a previously solved problem.
638
+ Since the interaction matrix does not need to be computed in full to compute the matrix-vector product,
639
+ only a few lines are evaluated at a time to reduce the memory cost of the operation.
640
+
641
+ The newer method :code:`compute_potential` should be preferred in the future.
642
+
643
+ Parameters
644
+ ----------
645
+ result : LinearPotentialFlowResult
646
+ the return of the BEM solver
647
+ mesh : MeshLike
648
+ a mesh
649
+ chunk_size: int, optional
650
+ Number of lines to compute in the matrix.
651
+ (legacy, should be passed as an engine setting instead).
652
+
653
+ Returns
654
+ -------
655
+ array of shape (mesh.nb_faces,)
656
+ potential on the faces of the mesh
657
+
658
+ Raises
659
+ ------
660
+ Exception: if the :code:`Result` object given as input does not contain the source distribution.
661
+ """
662
+ LOG.info(f"Compute potential on {mesh.name} for {result}.")
663
+
664
+ if result.sources is None:
665
+ raise Exception(f"""The values of the sources of {result} cannot been found.
666
+ 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.
667
+ Please re-run the resolution with the indirect method and keep_details=True.""")
668
+
669
+ gf_params = dict(free_surface=result.free_surface, water_depth=result.water_depth, wavenumber=result.encounter_wavenumber)
670
+ if chunk_size > mesh.nb_faces:
671
+ S = self.engine.build_S_matrix(mesh, result.body.mesh_including_lid, **gf_params)
672
+ phi = S @ result.sources
673
+
674
+ else:
675
+ phi = np.empty((mesh.nb_faces,), dtype=np.complex128)
676
+ for i in range(0, mesh.nb_faces, chunk_size):
677
+ faces_to_extract = list(range(i, min(i+chunk_size, mesh.nb_faces)))
678
+ S = self.engine.build_S_matrix(
679
+ mesh.extract_faces(faces_to_extract),
680
+ result.body.mesh_including_lid,
681
+ **gf_params
682
+ )
683
+ phi[i:i+chunk_size] = S @ result.sources
684
+
685
+ LOG.debug(f"Done computing potential on {mesh.name} for {result}.")
686
+
687
+ return phi
688
+
689
+ def get_free_surface_elevation(self, result, free_surface, keep_details=False):
690
+ """Compute the elevation of the free surface on a mesh for a previously solved problem.
691
+
692
+ The newer method :code:`compute_free_surface_elevation` should be preferred in the future.
693
+
694
+ Parameters
695
+ ----------
696
+ result : LinearPotentialFlowResult
697
+ the return of the solver
698
+ free_surface : FreeSurface
699
+ a meshed free surface
700
+ keep_details : bool, optional
701
+ if True, keep the free surface elevation in the LinearPotentialFlowResult (default:False)
702
+
703
+ Returns
704
+ -------
705
+ array of shape (free_surface.nb_faces,)
706
+ the free surface elevation on each faces of the meshed free surface
707
+
708
+ Raises
709
+ ------
710
+ Exception: if the :code:`Result` object given as input does not contain the source distribution.
711
+ """
712
+ if result.forward_speed != 0.0:
713
+ raise NotImplementedError("For free surface elevation with forward speed, please use the `compute_free_surface_elevation` method.")
714
+
715
+ fs_elevation = 1j*result.omega/result.g * self.get_potential_on_mesh(result, free_surface.mesh)
716
+ if keep_details:
717
+ result.fs_elevation[free_surface] = fs_elevation
718
+ return fs_elevation