capytaine 2.3.1__cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_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 (93) hide show
  1. capytaine/__about__.py +16 -0
  2. capytaine/__init__.py +36 -0
  3. capytaine/bem/__init__.py +0 -0
  4. capytaine/bem/airy_waves.py +111 -0
  5. capytaine/bem/engines.py +441 -0
  6. capytaine/bem/problems_and_results.py +600 -0
  7. capytaine/bem/solver.py +594 -0
  8. capytaine/bodies/__init__.py +4 -0
  9. capytaine/bodies/bodies.py +1221 -0
  10. capytaine/bodies/dofs.py +19 -0
  11. capytaine/bodies/predefined/__init__.py +6 -0
  12. capytaine/bodies/predefined/cylinders.py +151 -0
  13. capytaine/bodies/predefined/rectangles.py +111 -0
  14. capytaine/bodies/predefined/spheres.py +70 -0
  15. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  16. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  17. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  18. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  19. capytaine/green_functions/FinGreen3D/README.md +24 -0
  20. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  21. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  22. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  23. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  24. capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
  25. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  26. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  27. capytaine/green_functions/__init__.py +2 -0
  28. capytaine/green_functions/abstract_green_function.py +64 -0
  29. capytaine/green_functions/delhommeau.py +507 -0
  30. capytaine/green_functions/hams.py +204 -0
  31. capytaine/green_functions/libs/Delhommeau_float32.cpython-312-x86_64-linux-gnu.so +0 -0
  32. capytaine/green_functions/libs/Delhommeau_float64.cpython-312-x86_64-linux-gnu.so +0 -0
  33. capytaine/green_functions/libs/__init__.py +0 -0
  34. capytaine/io/__init__.py +0 -0
  35. capytaine/io/bemio.py +153 -0
  36. capytaine/io/legacy.py +328 -0
  37. capytaine/io/mesh_loaders.py +1086 -0
  38. capytaine/io/mesh_writers.py +692 -0
  39. capytaine/io/meshio.py +38 -0
  40. capytaine/io/wamit.py +479 -0
  41. capytaine/io/xarray.py +668 -0
  42. capytaine/matrices/__init__.py +16 -0
  43. capytaine/matrices/block.py +592 -0
  44. capytaine/matrices/block_toeplitz.py +325 -0
  45. capytaine/matrices/builders.py +89 -0
  46. capytaine/matrices/linear_solvers.py +232 -0
  47. capytaine/matrices/low_rank.py +395 -0
  48. capytaine/meshes/__init__.py +6 -0
  49. capytaine/meshes/clipper.py +465 -0
  50. capytaine/meshes/collections.py +342 -0
  51. capytaine/meshes/geometry.py +409 -0
  52. capytaine/meshes/mesh_like_protocol.py +37 -0
  53. capytaine/meshes/meshes.py +890 -0
  54. capytaine/meshes/predefined/__init__.py +6 -0
  55. capytaine/meshes/predefined/cylinders.py +314 -0
  56. capytaine/meshes/predefined/rectangles.py +261 -0
  57. capytaine/meshes/predefined/spheres.py +62 -0
  58. capytaine/meshes/properties.py +276 -0
  59. capytaine/meshes/quadratures.py +80 -0
  60. capytaine/meshes/quality.py +448 -0
  61. capytaine/meshes/surface_integrals.py +63 -0
  62. capytaine/meshes/symmetric.py +462 -0
  63. capytaine/post_pro/__init__.py +6 -0
  64. capytaine/post_pro/free_surfaces.py +88 -0
  65. capytaine/post_pro/impedance.py +92 -0
  66. capytaine/post_pro/kochin.py +54 -0
  67. capytaine/post_pro/rao.py +60 -0
  68. capytaine/tools/__init__.py +0 -0
  69. capytaine/tools/cache_on_disk.py +26 -0
  70. capytaine/tools/deprecation_handling.py +18 -0
  71. capytaine/tools/lists_of_points.py +52 -0
  72. capytaine/tools/lru_cache.py +49 -0
  73. capytaine/tools/optional_imports.py +27 -0
  74. capytaine/tools/prony_decomposition.py +150 -0
  75. capytaine/tools/symbolic_multiplication.py +149 -0
  76. capytaine/tools/timer.py +66 -0
  77. capytaine/ui/__init__.py +0 -0
  78. capytaine/ui/cli.py +28 -0
  79. capytaine/ui/rich.py +5 -0
  80. capytaine/ui/vtk/__init__.py +3 -0
  81. capytaine/ui/vtk/animation.py +329 -0
  82. capytaine/ui/vtk/body_viewer.py +28 -0
  83. capytaine/ui/vtk/helpers.py +82 -0
  84. capytaine/ui/vtk/mesh_viewer.py +461 -0
  85. capytaine-2.3.1.dist-info/LICENSE +674 -0
  86. capytaine-2.3.1.dist-info/METADATA +750 -0
  87. capytaine-2.3.1.dist-info/RECORD +93 -0
  88. capytaine-2.3.1.dist-info/WHEEL +6 -0
  89. capytaine-2.3.1.dist-info/entry_points.txt +3 -0
  90. capytaine.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
  91. capytaine.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
  92. capytaine.libs/libmvec-2-583a17db.28.so +0 -0
  93. capytaine.libs/libquadmath-2284e583.so.0.0.0 +0 -0
@@ -0,0 +1,594 @@
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(green_functions=..., engine=...).solve(problem)
9
+
10
+ """
11
+
12
+ import os
13
+ import logging
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ from datetime import datetime
19
+
20
+ from rich.progress import track
21
+
22
+ from capytaine.bem.problems_and_results import LinearPotentialFlowProblem, DiffractionProblem
23
+ from capytaine.green_functions.delhommeau import Delhommeau
24
+ from capytaine.bem.engines import BasicMatrixEngine
25
+ from capytaine.io.xarray import problems_from_dataset, assemble_dataset, kochin_data_array
26
+ from capytaine.tools.optional_imports import silently_import_optional_dependency
27
+ from capytaine.tools.lists_of_points import _normalize_points, _normalize_free_surface_points
28
+ from capytaine.tools.symbolic_multiplication import supporting_symbolic_multiplication
29
+ from capytaine.tools.timer import Timer
30
+
31
+ LOG = logging.getLogger(__name__)
32
+
33
+ class BEMSolver:
34
+ """
35
+ Solver for linear potential flow problems.
36
+
37
+ Parameters
38
+ ----------
39
+ green_function: AbstractGreenFunction, optional
40
+ Object handling the computation of the Green function.
41
+ (default: :class:`~capytaine.green_function.delhommeau.Delhommeau`)
42
+ engine: MatrixEngine, optional
43
+ Object handling the building of matrices and the resolution of linear systems with these matrices.
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"
49
+
50
+ Attributes
51
+ ----------
52
+ timer: dict[str, Timer]
53
+ Storing the time spent on each subtasks of the resolution
54
+ exportable_settings : dict
55
+ Settings of the solver that can be saved to reinit the same solver later.
56
+ """
57
+
58
+ def __init__(self, *, green_function=None, engine=None, method="indirect"):
59
+ self.green_function = Delhommeau() if green_function is None else green_function
60
+ self.engine = BasicMatrixEngine() if engine is None else engine
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
+
70
+ try:
71
+ self.exportable_settings = {
72
+ **self.green_function.exportable_settings,
73
+ **self.engine.exportable_settings,
74
+ "method": self.method,
75
+ }
76
+ except AttributeError:
77
+ self.exportable_settings = {}
78
+
79
+ def __str__(self):
80
+ return f"BEMSolver(engine={self.engine}, green_function={self.green_function})"
81
+
82
+ def __repr__(self):
83
+ return self.__str__()
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
+
94
+ def _repr_pretty_(self, p, cycle):
95
+ p.text(self.__str__())
96
+
97
+ @classmethod
98
+ def from_exported_settings(settings):
99
+ raise NotImplementedError
100
+
101
+ def solve(self, problem, method=None, keep_details=True, _check_wavelength=True):
102
+ """Solve the linear potential flow problem.
103
+
104
+ Parameters
105
+ ----------
106
+ problem: LinearPotentialFlowProblem
107
+ the problem to be solved
108
+ method: string, optional
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.
112
+ keep_details: bool, optional
113
+ if True, store the sources and the potential on the floating body in the output object
114
+ (default: True)
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.
118
+
119
+ Returns
120
+ -------
121
+ LinearPotentialFlowResult
122
+ an object storing the problem data and its results
123
+ """
124
+ LOG.info("Solve %s.", problem)
125
+
126
+ if _check_wavelength:
127
+ self._check_wavelength_and_mesh_resolution([problem])
128
+ self._check_wavelength_and_irregular_frequencies([problem])
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
+
140
+ if problem.forward_speed != 0.0:
141
+ omega, wavenumber = problem.encounter_omega, problem.encounter_wavenumber
142
+ else:
143
+ omega, wavenumber = problem.omega, problem.wavenumber
144
+
145
+ linear_solver = supporting_symbolic_multiplication(self.engine.linear_solver)
146
+ method = method if method is not None else self.method
147
+ if (method == 'direct'):
148
+ if problem.forward_speed != 0.0:
149
+ raise NotImplementedError("Direct solver is not able to solve problems with forward speed.")
150
+
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})")
163
+ pressure = 1j * omega * problem.rho * potential
164
+ sources = None
165
+ else:
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
+ )
172
+
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})")
178
+ potential = S @ sources
179
+ pressure = 1j * omega * problem.rho * potential
180
+ if problem.forward_speed != 0.0:
181
+ result = problem.make_results_container(sources=sources)
182
+ # Temporary result object to compute the ∇Φ term
183
+ nabla_phi = self._compute_potential_gradient(problem.body.mesh_including_lid, result)
184
+ pressure += problem.rho * problem.forward_speed * nabla_phi[:, 0]
185
+
186
+ pressure_on_hull = pressure[problem.body.hull_mask] # Discards pressure on lid if any
187
+ forces = problem.body.integrate_pressure(pressure_on_hull)
188
+
189
+ if not keep_details:
190
+ result = problem.make_results_container(forces)
191
+ else:
192
+ result = problem.make_results_container(forces, sources, potential, pressure)
193
+
194
+ LOG.debug("Done!")
195
+
196
+ return result
197
+
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):
209
+ """Solve several problems.
210
+ Optional keyword arguments are passed to `BEMSolver.solve`.
211
+
212
+ Parameters
213
+ ----------
214
+ problems: list of LinearPotentialFlowProblem
215
+ several problems to be solved
216
+ method: string, optional
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.
220
+ n_jobs: int, optional (default: 1)
221
+ the number of jobs to run in parallel using the optional dependency `joblib`
222
+ By defaults: do not use joblib and solve sequentially.
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.
231
+
232
+ Returns
233
+ -------
234
+ list of LinearPotentialFlowResult
235
+ the solved problems
236
+ """
237
+ if _check_wavelength:
238
+ self._check_wavelength_and_mesh_resolution(problems)
239
+ self._check_wavelength_and_irregular_frequencies(problems)
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
+
253
+ if n_jobs == 1: # force sequential resolution
254
+ problems = sorted(problems)
255
+ if progress_bar:
256
+ problems = track(problems, total=len(problems), description="Solving BEM problems")
257
+ results = [self._solve_and_catch_errors(pb, method=method, _check_wavelength=False, **kwargs) for pb in problems]
258
+ else:
259
+ joblib = silently_import_optional_dependency("joblib")
260
+ if joblib is None:
261
+ raise ImportError(f"Setting the `n_jobs` argument to {n_jobs} requires the missing optional dependency 'joblib'.")
262
+ groups_of_problems = LinearPotentialFlowProblem._group_for_parallel_resolution(problems)
263
+ parallel = joblib.Parallel(return_as="generator", n_jobs=n_jobs)
264
+ groups_of_results = parallel(joblib.delayed(self.solve_all)(grp, method=method, n_jobs=1, progress_bar=False, _check_wavelength=False, **kwargs) for grp in groups_of_problems)
265
+ if progress_bar:
266
+ groups_of_results = track(groups_of_results,
267
+ total=len(groups_of_problems),
268
+ description=f"Solving BEM problems with {n_jobs} threads:")
269
+ results = [res for grp in groups_of_results for res in grp] # flatten the nested list
270
+ LOG.info("Solver timer summary:\n%s", self.timer_summary())
271
+ return results
272
+
273
+ @staticmethod
274
+ def _check_wavelength_and_mesh_resolution(problems):
275
+ """Display a warning if some of the problems have a mesh resolution
276
+ that might not be sufficient for the given wavelength."""
277
+ LOG.debug("Check wavelength with mesh resolution.")
278
+ risky_problems = [pb for pb in problems
279
+ if 0.0 < pb.wavelength < pb.body.minimal_computable_wavelength]
280
+ nb_risky_problems = len(risky_problems)
281
+ if nb_risky_problems == 1:
282
+ pb = risky_problems[0]
283
+ freq_type = risky_problems[0].provided_freq_type
284
+ freq = pb.__getattribute__(freq_type)
285
+ LOG.warning(f"Mesh resolution for {pb}:\n"
286
+ f"The resolution of the mesh of the body {pb.body.__short_str__()} might "
287
+ f"be insufficient for {freq_type}={freq}.\n"
288
+ "This warning appears because the largest panel of this mesh "
289
+ f"has radius {pb.body.mesh.faces_radiuses.max():.3f} > wavelength/8."
290
+ )
291
+ elif nb_risky_problems > 1:
292
+ freq_type = risky_problems[0].provided_freq_type
293
+ freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
294
+ LOG.warning(f"Mesh resolution for {nb_risky_problems} problems:\n"
295
+ "The resolution of the mesh might be insufficient "
296
+ f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
297
+ "This warning appears when the largest panel of this mesh "
298
+ "has radius > wavelength/8."
299
+ )
300
+
301
+ @staticmethod
302
+ def _check_wavelength_and_irregular_frequencies(problems):
303
+ """Display a warning if some of the problems might encounter irregular frequencies."""
304
+ LOG.debug("Check wavelength with estimated irregular frequency.")
305
+ risky_problems = [pb for pb in problems
306
+ if pb.free_surface != np.inf and
307
+ pb.body.first_irregular_frequency_estimate(g=pb.g) < pb.omega < np.inf]
308
+ nb_risky_problems = len(risky_problems)
309
+ if nb_risky_problems >= 1:
310
+ if any(pb.body.lid_mesh is None for pb in problems):
311
+ recommendation = "Setting a lid for the floating body is recommended."
312
+ else:
313
+ recommendation = "The lid might need to be closer to the free surface."
314
+ if nb_risky_problems == 1:
315
+ pb = risky_problems[0]
316
+ freq_type = risky_problems[0].provided_freq_type
317
+ freq = pb.__getattribute__(freq_type)
318
+ LOG.warning(f"Irregular frequencies for {pb}:\n"
319
+ f"The body {pb.body.__short_str__()} might display irregular frequencies "
320
+ f"for {freq_type}={freq}.\n"
321
+ + recommendation
322
+ )
323
+ elif nb_risky_problems > 1:
324
+ freq_type = risky_problems[0].provided_freq_type
325
+ freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
326
+ LOG.warning(f"Irregular frequencies for {nb_risky_problems} problems:\n"
327
+ "Irregular frequencies might be encountered "
328
+ f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
329
+ + recommendation
330
+ )
331
+
332
+ def fill_dataset(self, dataset, bodies, *, method=None, n_jobs=1, _check_wavelength=True, progress_bar=None, **kwargs):
333
+ """Solve a set of problems defined by the coordinates of an xarray dataset.
334
+
335
+ Parameters
336
+ ----------
337
+ dataset : xarray Dataset
338
+ dataset containing the problems parameters: frequency, radiating_dof, water_depth, ...
339
+ bodies : FloatingBody or list of FloatingBody
340
+ The body or bodies involved in the problems
341
+ They should all have different names.
342
+ method: string, optional
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.
346
+ n_jobs: int, optional (default: 1)
347
+ the number of jobs to run in parallel using the optional dependency `joblib`
348
+ By defaults: do not use joblib and solve sequentially.
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.
357
+
358
+ Returns
359
+ -------
360
+ xarray Dataset
361
+ """
362
+ attrs = {'start_of_computation': datetime.now().isoformat(),
363
+ **self.exportable_settings}
364
+ if method is not None: # Overrides the method in self.exportable_settings
365
+ attrs["method"] = method
366
+ problems = problems_from_dataset(dataset, bodies)
367
+ if 'theta' in dataset.coords:
368
+ results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
369
+ kochin = kochin_data_array(results, dataset.coords['theta'])
370
+ dataset = assemble_dataset(results, attrs=attrs, **kwargs)
371
+ dataset.update(kochin)
372
+ else:
373
+ results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength, progress_bar=progress_bar)
374
+ dataset = assemble_dataset(results, attrs=attrs, **kwargs)
375
+ return dataset
376
+
377
+
378
+ def compute_potential(self, points, result):
379
+ """Compute the value of the potential at given points for a previously solved potential flow problem.
380
+
381
+ Parameters
382
+ ----------
383
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
384
+ Coordinates of the point(s) at which the potential should be computed
385
+ result: LinearPotentialFlowResult
386
+ The return of the BEM solver
387
+
388
+ Returns
389
+ -------
390
+ complex-valued array of shape (1,) or (N,) or (nx, ny, nz) or (mesh.nb_faces,) depending of the kind of input
391
+ The value of the potential at the points
392
+
393
+ Raises
394
+ ------
395
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
396
+ """
397
+ points, output_shape = _normalize_points(points, keep_mesh=True)
398
+ if result.sources is None:
399
+ raise Exception(f"""The values of the sources of {result} cannot been found.
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.
401
+ Please re-run the resolution with the indirect method and keep_details=True.""")
402
+
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)
405
+ potential = S @ result.sources # Sum the contributions of all panels in the mesh
406
+ return potential.reshape(output_shape)
407
+
408
+ def _compute_potential_gradient(self, points, result):
409
+ points, output_shape = _normalize_points(points, keep_mesh=True)
410
+
411
+ if result.sources is None:
412
+ raise Exception(f"""The values of the sources of {result} cannot been found.
413
+ They probably have not been stored by the solver because the option keep_details=True have not been set.
414
+ Please re-run the resolution with this option.""")
415
+
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,
418
+ early_dot_product=False)
419
+ velocities = np.einsum('ijk,j->ik', gradG, result.sources) # Sum the contributions of all panels in the mesh
420
+ return velocities.reshape((*output_shape, 3))
421
+
422
+ def compute_velocity(self, points, result):
423
+ """Compute the value of the velocity vector at given points for a previously solved potential flow problem.
424
+
425
+ Parameters
426
+ ----------
427
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
428
+ Coordinates of the point(s) at which the velocity should be computed
429
+ result: LinearPotentialFlowResult
430
+ The return of the BEM solver
431
+
432
+ Returns
433
+ -------
434
+ 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
435
+ The value of the velocity at the points
436
+
437
+ Raises
438
+ ------
439
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
440
+ """
441
+ nabla_phi = self._compute_potential_gradient(points, result)
442
+ if result.forward_speed != 0.0:
443
+ nabla_phi[..., 0] -= result.forward_speed
444
+ return nabla_phi
445
+
446
+ def compute_pressure(self, points, result):
447
+ """Compute the value of the pressure at given points for a previously solved potential flow problem.
448
+
449
+ Parameters
450
+ ----------
451
+ points: array of shape (3,) or (N, 3), or 3-ple of arrays returned by meshgrid, or MeshLike object
452
+ Coordinates of the point(s) at which the pressure should be computed
453
+ result: LinearPotentialFlowResult
454
+ The return of the BEM solver
455
+
456
+ Returns
457
+ -------
458
+ complex-valued array of shape (1,) or (N,) or (nx, ny, nz) or (mesh.nb_faces,) depending of the kind of input
459
+ The value of the pressure at the points
460
+
461
+ Raises
462
+ ------
463
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
464
+ """
465
+ if result.forward_speed != 0:
466
+ pressure = 1j * result.encounter_omega * result.rho * self.compute_potential(points, result)
467
+ nabla_phi = self._compute_potential_gradient(points, result)
468
+ pressure += result.rho * result.forward_speed * nabla_phi[..., 0]
469
+ else:
470
+ pressure = 1j * result.omega * result.rho * self.compute_potential(points, result)
471
+ return pressure
472
+
473
+
474
+ def compute_free_surface_elevation(self, points, result):
475
+ """Compute the value of the free surface elevation at given points for a previously solved potential flow problem.
476
+
477
+ Parameters
478
+ ----------
479
+ points: array of shape (2,) or (N, 2), or 2-ple of arrays returned by meshgrid, or MeshLike object
480
+ Coordinates of the point(s) at which the free surface elevation should be computed
481
+ result: LinearPotentialFlowResult
482
+ The return of the BEM solver
483
+
484
+ Returns
485
+ -------
486
+ complex-valued array of shape (1,) or (N,) or (nx, ny, nz) or (mesh.nb_faces,) depending of the kind of input
487
+ The value of the free surface elevation at the points
488
+
489
+ Raises
490
+ ------
491
+ Exception: if the :code:`LinearPotentialFlowResult` object given as input does not contain the source distribution.
492
+ """
493
+ points, output_shape = _normalize_free_surface_points(points, keep_mesh=True)
494
+
495
+ if result.forward_speed != 0:
496
+ fs_elevation = -1/result.g * (-1j*result.encounter_omega) * self.compute_potential(points, result)
497
+ nabla_phi = self._compute_potential_gradient(points, result)
498
+ fs_elevation += -1/result.g * result.forward_speed * nabla_phi[..., 0]
499
+ else:
500
+ fs_elevation = -1/result.g * (-1j*result.omega) * self.compute_potential(points, result)
501
+
502
+ return fs_elevation.reshape(output_shape)
503
+
504
+
505
+ ## Legacy
506
+
507
+ def get_potential_on_mesh(self, result, mesh, chunk_size=50):
508
+ """Compute the potential on a mesh for the potential field of a previously solved problem.
509
+ Since the interaction matrix does not need to be computed in full to compute the matrix-vector product,
510
+ only a few lines are evaluated at a time to reduce the memory cost of the operation.
511
+
512
+ The newer method :code:`compute_potential` should be preferred in the future.
513
+
514
+ Parameters
515
+ ----------
516
+ result : LinearPotentialFlowResult
517
+ the return of the BEM solver
518
+ mesh : MeshLike
519
+ a mesh
520
+ chunk_size: int, optional
521
+ Number of lines to compute in the matrix.
522
+ (legacy, should be passed as an engine setting instead).
523
+
524
+ Returns
525
+ -------
526
+ array of shape (mesh.nb_faces,)
527
+ potential on the faces of the mesh
528
+
529
+ Raises
530
+ ------
531
+ Exception: if the :code:`Result` object given as input does not contain the source distribution.
532
+ """
533
+ LOG.info(f"Compute potential on {mesh.name} for {result}.")
534
+
535
+ if result.sources is None:
536
+ raise Exception(f"""The values of the sources of {result} cannot been found.
537
+ 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.
538
+ Please re-run the resolution with the indirect method and keep_details=True.""")
539
+
540
+ if chunk_size > mesh.nb_faces:
541
+ S = self.engine.build_S_matrix(
542
+ mesh,
543
+ result.body.mesh_including_lid,
544
+ result.free_surface, result.water_depth, result.wavenumber,
545
+ self.green_function
546
+ )
547
+ phi = S @ result.sources
548
+
549
+ else:
550
+ phi = np.empty((mesh.nb_faces,), dtype=np.complex128)
551
+ for i in range(0, mesh.nb_faces, chunk_size):
552
+ faces_to_extract = list(range(i, min(i+chunk_size, mesh.nb_faces)))
553
+ S = self.engine.build_S_matrix(
554
+ mesh.extract_faces(faces_to_extract),
555
+ result.body.mesh_including_lid,
556
+ result.free_surface, result.water_depth, result.wavenumber,
557
+ self.green_function
558
+ )
559
+ phi[i:i+chunk_size] = S @ result.sources
560
+
561
+ LOG.debug(f"Done computing potential on {mesh.name} for {result}.")
562
+
563
+ return phi
564
+
565
+ def get_free_surface_elevation(self, result, free_surface, keep_details=False):
566
+ """Compute the elevation of the free surface on a mesh for a previously solved problem.
567
+
568
+ The newer method :code:`compute_free_surface_elevation` should be preferred in the future.
569
+
570
+ Parameters
571
+ ----------
572
+ result : LinearPotentialFlowResult
573
+ the return of the solver
574
+ free_surface : FreeSurface
575
+ a meshed free surface
576
+ keep_details : bool, optional
577
+ if True, keep the free surface elevation in the LinearPotentialFlowResult (default:False)
578
+
579
+ Returns
580
+ -------
581
+ array of shape (free_surface.nb_faces,)
582
+ the free surface elevation on each faces of the meshed free surface
583
+
584
+ Raises
585
+ ------
586
+ Exception: if the :code:`Result` object given as input does not contain the source distribution.
587
+ """
588
+ if result.forward_speed != 0.0:
589
+ raise NotImplementedError("For free surface elevation with forward speed, please use the `compute_free_surface_elevation` method.")
590
+
591
+ fs_elevation = 1j*result.omega/result.g * self.get_potential_on_mesh(result, free_surface.mesh)
592
+ if keep_details:
593
+ result.fs_elevation[free_surface] = fs_elevation
594
+ return fs_elevation
@@ -0,0 +1,4 @@
1
+ # Copyright (C) 2017-2019 Matthieu Ancellin
2
+ # See LICENSE file at <https://github.com/mancellin/capytaine>
3
+
4
+ from capytaine.bodies.bodies import FloatingBody