capytaine 3.0.0a1__cp38-cp38-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.
- capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
- capytaine/.dylibs/libgfortran.5.dylib +0 -0
- capytaine/.dylibs/libquadmath.0.dylib +0 -0
- capytaine/__about__.py +21 -0
- capytaine/__init__.py +32 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +321 -0
- capytaine/bem/problems_and_results.py +601 -0
- capytaine/bem/solver.py +718 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +630 -0
- capytaine/bodies/dofs.py +146 -0
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/Delhommeau_float32.cpython-38-darwin.so +0 -0
- capytaine/green_functions/Delhommeau_float64.cpython-38-darwin.so +0 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +522 -0
- capytaine/green_functions/hams.py +210 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +228 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +673 -0
- capytaine/meshes/__init__.py +2 -0
- capytaine/meshes/abstract_meshes.py +375 -0
- capytaine/meshes/clean.py +302 -0
- capytaine/meshes/clip.py +347 -0
- capytaine/meshes/export.py +89 -0
- capytaine/meshes/geometry.py +259 -0
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +826 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +280 -0
- capytaine/meshes/predefined/rectangles.py +202 -0
- capytaine/meshes/predefined/spheres.py +55 -0
- capytaine/meshes/quality.py +159 -0
- capytaine/meshes/surface_integrals.py +82 -0
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +85 -0
- capytaine/post_pro/impedance.py +92 -0
- capytaine/post_pro/kochin.py +54 -0
- capytaine/post_pro/rao.py +60 -0
- capytaine/tools/__init__.py +0 -0
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/cache_on_disk.py +26 -0
- capytaine/tools/deprecation_handling.py +18 -0
- capytaine/tools/lists_of_points.py +52 -0
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +161 -0
- capytaine/tools/timer.py +90 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine-3.0.0a1.dist-info/LICENSE +674 -0
- capytaine-3.0.0a1.dist-info/METADATA +755 -0
- capytaine-3.0.0a1.dist-info/RECORD +65 -0
- capytaine-3.0.0a1.dist-info/WHEEL +4 -0
- capytaine-3.0.0a1.dist-info/entry_points.txt +3 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
capytaine/__about__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
__all__ = ["__title__", "__description__", "__version__", "__author__", "__uri__", "__license__"]
|
|
4
|
+
|
|
5
|
+
__title__ = "capytaine"
|
|
6
|
+
__description__ = """Python BEM solver for linear potential flow, based on Nemoh"""
|
|
7
|
+
|
|
8
|
+
__author__ = "Matthieu Ancellin"
|
|
9
|
+
__uri__ = "https://github.com/capytaine/capytaine"
|
|
10
|
+
__license__ = "GPL-3.0"
|
|
11
|
+
|
|
12
|
+
__version__ = "3.0.0a1"
|
|
13
|
+
|
|
14
|
+
__build_info__ = {
|
|
15
|
+
"compiler": "gcc 13.4.0",
|
|
16
|
+
"build_time": "2026-02-02T13:05:10Z"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
print(__version__)
|
capytaine/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright (C) 2017-2025 Matthieu Ancellin
|
|
2
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
3
|
+
|
|
4
|
+
from .__about__ import (
|
|
5
|
+
__title__, __description__, __version__, __author__, __uri__, __license__, __build_info__
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from capytaine.meshes.meshes import Mesh
|
|
9
|
+
from capytaine.meshes.io import load_mesh
|
|
10
|
+
from capytaine.meshes.symmetric_meshes import ReflectionSymmetricMesh, RotationSymmetricMesh
|
|
11
|
+
|
|
12
|
+
from capytaine.meshes.predefined.cylinders import mesh_disk, mesh_horizontal_cylinder, mesh_vertical_cylinder
|
|
13
|
+
from capytaine.meshes.predefined.spheres import mesh_sphere
|
|
14
|
+
from capytaine.meshes.predefined.rectangles import mesh_rectangle, mesh_parallelepiped
|
|
15
|
+
|
|
16
|
+
from capytaine.bodies.bodies import FloatingBody
|
|
17
|
+
from capytaine.bodies.multibodies import Multibody
|
|
18
|
+
from capytaine.bodies.dofs import rigid_body_dofs
|
|
19
|
+
|
|
20
|
+
from capytaine.bem.problems_and_results import RadiationProblem, DiffractionProblem
|
|
21
|
+
from capytaine.bem.solver import BEMSolver
|
|
22
|
+
from capytaine.bem.engines import BasicMatrixEngine
|
|
23
|
+
from capytaine.green_functions.delhommeau import Delhommeau, XieDelhommeau
|
|
24
|
+
from capytaine.green_functions.hams import LiangWuNoblesseGF, FinGreen3D, HAMS_GF
|
|
25
|
+
|
|
26
|
+
from capytaine.post_pro.free_surfaces import FreeSurface
|
|
27
|
+
|
|
28
|
+
from capytaine.io.xarray import assemble_dataframe, assemble_dataset, assemble_matrices, export_dataset
|
|
29
|
+
|
|
30
|
+
from capytaine.ui.rich import set_logging
|
|
31
|
+
|
|
32
|
+
set_logging(level="WARNING")
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Computing the potential and velocity of Airy wave."""
|
|
2
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/mancellin/capytaine>
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from capytaine.tools.lists_of_points import _normalize_points, _normalize_free_surface_points
|
|
7
|
+
|
|
8
|
+
def airy_waves_potential(points, pb):
|
|
9
|
+
"""Compute the potential for Airy waves at a given point (or array of points).
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
points: array of shape (3) or (N x 3)
|
|
14
|
+
coordinates of the points in which to evaluate the potential.
|
|
15
|
+
pb: DiffractionProblem
|
|
16
|
+
problem with the environmental conditions (g, rho, ...) of interest
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
array of shape (1) or (N x 1)
|
|
21
|
+
The potential
|
|
22
|
+
"""
|
|
23
|
+
points, output_shape = _normalize_points(points)
|
|
24
|
+
|
|
25
|
+
if float(pb.wavenumber) == 0.0 or float(pb.wavenumber) == np.inf:
|
|
26
|
+
return np.nan * np.ones(output_shape)
|
|
27
|
+
|
|
28
|
+
x, y, z = points.T
|
|
29
|
+
k = pb.wavenumber
|
|
30
|
+
h = pb.water_depth
|
|
31
|
+
beta = pb.encounter_wave_direction
|
|
32
|
+
wbar = x * np.cos(beta) + y * np.sin(beta)
|
|
33
|
+
|
|
34
|
+
if 0 <= k*h < 20:
|
|
35
|
+
cih = np.cosh(k*(z+h))/np.cosh(k*h)
|
|
36
|
+
# sih = np.sinh(k*(z+h))/np.cosh(k*h)
|
|
37
|
+
else:
|
|
38
|
+
cih = np.exp(k*z)
|
|
39
|
+
# sih = np.exp(k*z)
|
|
40
|
+
|
|
41
|
+
phi = -1j*pb.g/pb.omega * cih * np.exp(1j * k * wbar)
|
|
42
|
+
return phi.reshape(output_shape)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def airy_waves_velocity(points, pb):
|
|
46
|
+
"""Compute the fluid velocity for Airy waves at a given point (or array of points).
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
points: array of shape (3) or (N x 3)
|
|
51
|
+
coordinates of the points in which to evaluate the potential.
|
|
52
|
+
pb: DiffractionProblem
|
|
53
|
+
problem with the environmental conditions (g, rho, ...) of interest
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
array of shape (3) or (N x 3)
|
|
58
|
+
the velocity vectors
|
|
59
|
+
"""
|
|
60
|
+
points, output_shape = _normalize_points(points)
|
|
61
|
+
|
|
62
|
+
if float(pb.wavenumber) == 0.0 or float(pb.wavenumber) == np.inf:
|
|
63
|
+
return np.nan * np.ones((*output_shape, 3))
|
|
64
|
+
|
|
65
|
+
x, y, z = points.T
|
|
66
|
+
k = pb.wavenumber
|
|
67
|
+
h = pb.water_depth
|
|
68
|
+
beta = pb.encounter_wave_direction
|
|
69
|
+
|
|
70
|
+
wbar = x * np.cos(beta) + y * np.sin(beta)
|
|
71
|
+
|
|
72
|
+
if 0 <= k*h < 20:
|
|
73
|
+
cih = np.cosh(k*(z+h))/np.cosh(k*h)
|
|
74
|
+
sih = np.sinh(k*(z+h))/np.cosh(k*h)
|
|
75
|
+
else:
|
|
76
|
+
cih = np.exp(k*z)
|
|
77
|
+
sih = np.exp(k*z)
|
|
78
|
+
|
|
79
|
+
v = pb.g*k/pb.omega * \
|
|
80
|
+
np.exp(1j * k * wbar) * \
|
|
81
|
+
np.array([np.cos(pb.wave_direction) * cih, np.sin(pb.wave_direction) * cih, -1j * sih])
|
|
82
|
+
|
|
83
|
+
return v.T.reshape((*output_shape, 3))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def airy_waves_pressure(points, pb):
|
|
87
|
+
return 1j * float(pb.omega) * pb.rho * airy_waves_potential(points, pb)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def froude_krylov_force(pb):
|
|
91
|
+
return pb.body.integrate_pressure(airy_waves_pressure(pb.body.mesh.faces_centers, pb))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def airy_waves_free_surface_elevation(points, pb):
|
|
95
|
+
"""Compute the free surface elevation at points of the undisturbed Airy waves
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
points: array of shape (3) or (N × 3) or (2) or (N × 2)
|
|
100
|
+
coordinates of the points in which to evaluate the potential.
|
|
101
|
+
If only two coordinates are passed, the last one is filled with zeros.
|
|
102
|
+
pb: DiffractionProblem
|
|
103
|
+
problem with the environmental conditions (g, rho, ...) of interest
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
complex-valued array of shape (1,) or (N,)
|
|
108
|
+
the free surface elevations
|
|
109
|
+
"""
|
|
110
|
+
points, output_shape = _normalize_free_surface_points(points)
|
|
111
|
+
return 1j * pb.omega / pb.g * airy_waves_potential(points, pb).reshape(output_shape)
|
capytaine/bem/engines.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Definition of the methods to build influence matrices, using possibly some sparse structures."""
|
|
2
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Tuple, Union, Optional, Callable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import scipy.sparse.linalg as ssl
|
|
11
|
+
|
|
12
|
+
from capytaine.meshes.symmetric_meshes import ReflectionSymmetricMesh, RotationSymmetricMesh
|
|
13
|
+
|
|
14
|
+
from capytaine.green_functions.abstract_green_function import AbstractGreenFunction
|
|
15
|
+
from capytaine.green_functions.delhommeau import Delhommeau
|
|
16
|
+
|
|
17
|
+
from capytaine.tools.block_circulant_matrices import (
|
|
18
|
+
BlockCirculantMatrix, lu_decompose, has_been_lu_decomposed,
|
|
19
|
+
MatrixLike, LUDecomposedMatrixLike
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
LOG = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
####################
|
|
26
|
+
# ABSTRACT CLASS #
|
|
27
|
+
####################
|
|
28
|
+
|
|
29
|
+
class MatrixEngine(ABC):
|
|
30
|
+
"""Abstract method to build a matrix."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def build_matrices(self, mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def build_S_matrix(self, mesh1, mesh2, free_surface, water_depth, wavenumber):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def build_fullK_matrix(self, mesh1, mesh2, free_surface, water_depth, wavenumber):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
##################
|
|
46
|
+
# BASIC ENGINE #
|
|
47
|
+
##################
|
|
48
|
+
|
|
49
|
+
class Counter:
|
|
50
|
+
def __init__(self):
|
|
51
|
+
self.nb_iter = 0
|
|
52
|
+
|
|
53
|
+
def __call__(self, *args, **kwargs):
|
|
54
|
+
self.nb_iter += 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def solve_gmres(A, b):
|
|
58
|
+
LOG.debug(f"Solve with GMRES for {A}.")
|
|
59
|
+
|
|
60
|
+
if LOG.isEnabledFor(logging.INFO):
|
|
61
|
+
counter = Counter()
|
|
62
|
+
x, info = ssl.gmres(A, b, atol=1e-6, callback=counter)
|
|
63
|
+
LOG.info(f"End of GMRES after {counter.nb_iter} iterations.")
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
x, info = ssl.gmres(A, b, atol=1e-6)
|
|
67
|
+
|
|
68
|
+
if info > 0:
|
|
69
|
+
raise RuntimeError(f"No convergence of the GMRES after {info} iterations.\n"
|
|
70
|
+
"This can be due to overlapping panels or irregular frequencies.")
|
|
71
|
+
|
|
72
|
+
return x
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
LUDecomposedMatrixOrNot = Union[MatrixLike, LUDecomposedMatrixLike]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BasicMatrixEngine(MatrixEngine):
|
|
79
|
+
"""
|
|
80
|
+
Default matrix engine.
|
|
81
|
+
|
|
82
|
+
Features:
|
|
83
|
+
- Caching of the last computed matrices.
|
|
84
|
+
- Supports plane symmetries and nested plane symmetries.
|
|
85
|
+
- Linear solver can be customized. Default is `lu_decomposition` with caching of the LU decomposition.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
green_function: AbstractGreenFunction
|
|
90
|
+
the low level implementation used to compute the coefficients of the matrices.
|
|
91
|
+
linear_solver: str or function, optional
|
|
92
|
+
Setting of the numerical solver for linear problems Ax = b.
|
|
93
|
+
It can be set with the name of a preexisting solver
|
|
94
|
+
(available: "lu_decomposition", "lu_decompositon_with_overwrite" and "gmres", the former is the default choice)
|
|
95
|
+
or by passing directly a solver function.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
green_function: AbstractGreenFunction
|
|
99
|
+
_linear_solver: Union[str, Callable]
|
|
100
|
+
last_computed_matrices: Optional[Tuple[MatrixLike, LUDecomposedMatrixOrNot]]
|
|
101
|
+
|
|
102
|
+
def __init__(self, *, green_function=None, linear_solver='lu_decomposition'):
|
|
103
|
+
|
|
104
|
+
self.green_function = Delhommeau() if green_function is None else green_function
|
|
105
|
+
|
|
106
|
+
self._linear_solver = linear_solver
|
|
107
|
+
|
|
108
|
+
self.last_computed_inputs = None
|
|
109
|
+
self.last_computed_matrices = None
|
|
110
|
+
|
|
111
|
+
self.exportable_settings = {
|
|
112
|
+
'engine': 'BasicMatrixEngine',
|
|
113
|
+
'linear_solver': str(linear_solver),
|
|
114
|
+
**self.green_function.exportable_settings,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def __str__(self):
|
|
118
|
+
params= [f"green_function={self.green_function}", f"linear_solver={repr(self._linear_solver)}"]
|
|
119
|
+
return f"BasicMatrixEngine({', '.join(params)})"
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return self.__str__()
|
|
123
|
+
|
|
124
|
+
def _repr_pretty_(self, p, cycle):
|
|
125
|
+
p.text(self.__str__())
|
|
126
|
+
|
|
127
|
+
def build_S_matrix(self, mesh1, mesh2, **gf_params) -> np.ndarray:
|
|
128
|
+
"""Similar to :code:`build_matrices`, but returning only :math:`S`"""
|
|
129
|
+
# Calls directly evaluate instead of build_matrices because the caching
|
|
130
|
+
# mechanism of build_matrices is not compatible with giving mesh1 as a
|
|
131
|
+
# list of points, but we need that for post-processing
|
|
132
|
+
S, _ = self.green_function.evaluate(mesh1, mesh2, **gf_params)
|
|
133
|
+
return S
|
|
134
|
+
|
|
135
|
+
def build_fullK_matrix(self, mesh1, mesh2, **gf_params) -> np.ndarray:
|
|
136
|
+
"""Similar to :code:`build_matrices`, but returning only full :math:`K`
|
|
137
|
+
(that is the three components of the gradient, not just the normal one)"""
|
|
138
|
+
# TODO: could use symmetries. In particular for forward, we compute the
|
|
139
|
+
# full velocity on the same mesh so symmetries could be used.
|
|
140
|
+
gf_params.setdefault("diagonal_term_in_double_layer", True)
|
|
141
|
+
gf_params.setdefault("adjoint_double_layer", True)
|
|
142
|
+
gf_params.setdefault("early_dot_product", False)
|
|
143
|
+
_, fullK = self.green_function.evaluate(mesh1, mesh2, **gf_params)
|
|
144
|
+
return fullK
|
|
145
|
+
|
|
146
|
+
def _build_matrices_with_symmetries(self, mesh1, mesh2, *, diagonal_term_in_double_layer=True, **gf_params) -> Tuple[MatrixLike, MatrixLike]:
|
|
147
|
+
if (isinstance(mesh1, ReflectionSymmetricMesh)
|
|
148
|
+
and isinstance(mesh2, ReflectionSymmetricMesh)
|
|
149
|
+
and mesh1.plane == mesh2.plane):
|
|
150
|
+
|
|
151
|
+
S_a, K_a = self._build_matrices_with_symmetries(mesh1.half, mesh2.half,
|
|
152
|
+
diagonal_term_in_double_layer=diagonal_term_in_double_layer, **gf_params)
|
|
153
|
+
S_b, K_b = self._build_matrices_with_symmetries(mesh1.other_half, mesh2.half,
|
|
154
|
+
diagonal_term_in_double_layer=False, **gf_params)
|
|
155
|
+
|
|
156
|
+
return BlockCirculantMatrix([S_a, S_b]), BlockCirculantMatrix([K_a, K_b])
|
|
157
|
+
|
|
158
|
+
elif (isinstance(mesh1, RotationSymmetricMesh)
|
|
159
|
+
and isinstance(mesh2, RotationSymmetricMesh)
|
|
160
|
+
and mesh1.n == mesh2.n):
|
|
161
|
+
|
|
162
|
+
S_cols, K_cols = self.green_function.evaluate(
|
|
163
|
+
mesh1.merged(), mesh2.wedge,
|
|
164
|
+
diagonal_term_in_double_layer=diagonal_term_in_double_layer,
|
|
165
|
+
**gf_params,
|
|
166
|
+
)
|
|
167
|
+
# Building the first column of blocks, that is the interactions of all of mesh1 with the reference wedge of mesh2.
|
|
168
|
+
|
|
169
|
+
n_blocks = mesh1.n # == mesh2.n
|
|
170
|
+
block_shape = (mesh2.wedge.nb_faces, mesh2.wedge.nb_faces)
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
BlockCirculantMatrix(S_cols.reshape((n_blocks, *block_shape))),
|
|
174
|
+
BlockCirculantMatrix(K_cols.reshape((n_blocks, *block_shape))),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
gf_params.setdefault("early_dot_product", True)
|
|
179
|
+
return self.green_function.evaluate(mesh1, mesh2, diagonal_term_in_double_layer=diagonal_term_in_double_layer, **gf_params)
|
|
180
|
+
|
|
181
|
+
def _build_and_cache_matrices_with_symmetries(
|
|
182
|
+
self, mesh1, mesh2, **gf_params
|
|
183
|
+
) -> Tuple[MatrixLike, LUDecomposedMatrixOrNot]:
|
|
184
|
+
if (mesh1, mesh2, gf_params) == self.last_computed_inputs:
|
|
185
|
+
LOG.debug("%s: reading cache.", self.__class__.__name__)
|
|
186
|
+
return self.last_computed_matrices
|
|
187
|
+
else:
|
|
188
|
+
LOG.debug("%s: computing new matrices.", self.__class__.__name__)
|
|
189
|
+
self.last_computed_matrices = None # Unlink former cached values, so the memory can be freed to compute new matrices.
|
|
190
|
+
S, K = self._build_matrices_with_symmetries(mesh1, mesh2, **gf_params)
|
|
191
|
+
self.last_computed_inputs = (mesh1, mesh2, gf_params)
|
|
192
|
+
self.last_computed_matrices = (S, K)
|
|
193
|
+
return self.last_computed_matrices
|
|
194
|
+
|
|
195
|
+
# Main interface for compliance with AbstractGreenFunction interface
|
|
196
|
+
def build_matrices(self, mesh1, mesh2, **gf_params):
|
|
197
|
+
r"""Build the influence matrices between mesh1 and mesh2.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
mesh1: MeshLike or list of points
|
|
202
|
+
mesh of the receiving body (where the potential is measured)
|
|
203
|
+
mesh2: MeshLike
|
|
204
|
+
mesh of the source body (over which the source distribution is integrated)
|
|
205
|
+
free_surface: float
|
|
206
|
+
position of the free surface (default: :math:`z = 0`)
|
|
207
|
+
water_depth: float
|
|
208
|
+
position of the sea bottom (default: :math:`z = -\infty`)
|
|
209
|
+
wavenumber: float
|
|
210
|
+
wavenumber (default: 1.0)
|
|
211
|
+
adjoint_double_layer: bool, optional
|
|
212
|
+
compute double layer for direct method (F) or adjoint double layer for indirect method (T) matrices (default: True)
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
tuple of matrix-like (Numpy arrays or BlockCirculantMatrix)
|
|
217
|
+
the matrices :math:`S` and :math:`K`
|
|
218
|
+
"""
|
|
219
|
+
return self._build_and_cache_matrices_with_symmetries(
|
|
220
|
+
mesh1, mesh2, **gf_params
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def linear_solver(self, A: LUDecomposedMatrixOrNot, b: np.ndarray) -> np.ndarray:
|
|
224
|
+
"""Solve a linear system with left-hand side A and right-hand-side b
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
A: matrix-like
|
|
229
|
+
Expected to be the second output of `build_matrices`
|
|
230
|
+
b: np.ndarray
|
|
231
|
+
Vector of the correct length
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
x: np.ndarray
|
|
236
|
+
Vector such that A@x = b
|
|
237
|
+
"""
|
|
238
|
+
if not isinstance(self._linear_solver, str):
|
|
239
|
+
# If not a string, it is expected to be a custom function that can
|
|
240
|
+
# be called to solve the system
|
|
241
|
+
x = self._linear_solver(A, b)
|
|
242
|
+
|
|
243
|
+
if not x.shape == b.shape:
|
|
244
|
+
raise ValueError(f"Error in linear solver of {self}: the shape of the output ({x.shape}) "
|
|
245
|
+
f"does not match the expected shape ({b.shape})")
|
|
246
|
+
|
|
247
|
+
return x
|
|
248
|
+
|
|
249
|
+
elif self._linear_solver in ("lu_decomposition", "lu_decomposition_with_overwrite") :
|
|
250
|
+
overwrite_a = (self._linear_solver == "lu_decomposition_with_overwrite")
|
|
251
|
+
if not has_been_lu_decomposed(A):
|
|
252
|
+
luA = lu_decompose(A, overwrite_a=overwrite_a)
|
|
253
|
+
if A is self.last_computed_matrices[1]:
|
|
254
|
+
# In normal operation of Capytaine, `A` is always the $D$
|
|
255
|
+
# or $K$ matrix stored in the cache of the solver.
|
|
256
|
+
# Here we replace the matrix by its LU decomposition in the
|
|
257
|
+
# cache to avoid doing the decomposition again.
|
|
258
|
+
self.last_computed_matrices = (self.last_computed_matrices[0], luA)
|
|
259
|
+
else:
|
|
260
|
+
luA: LUDecomposedMatrixLike = A
|
|
261
|
+
return luA.solve(b)
|
|
262
|
+
|
|
263
|
+
elif self._linear_solver == "gmres":
|
|
264
|
+
return solve_gmres(A, b)
|
|
265
|
+
|
|
266
|
+
else:
|
|
267
|
+
raise NotImplementedError(
|
|
268
|
+
f"Unknown `linear_solver` in BasicMatrixEngine: {self._linear_solver}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def compute_ram_estimation(self, problem):
|
|
272
|
+
nb_faces = problem.body.mesh.nb_faces
|
|
273
|
+
nb_matrices = 2
|
|
274
|
+
nb_bytes = 16
|
|
275
|
+
|
|
276
|
+
if self._linear_solver == "lu_decomposition":
|
|
277
|
+
nb_matrices += 1
|
|
278
|
+
|
|
279
|
+
if self.green_function.floating_point_precision == "float32":
|
|
280
|
+
nb_bytes = 8
|
|
281
|
+
|
|
282
|
+
# In theory a simple symmetry is a gain of factor 1/2
|
|
283
|
+
# and a nested symmetry is a gain of factor 1/4.
|
|
284
|
+
# For the solvers that use LU decomposition the gain is a bit less.
|
|
285
|
+
solver_factors = {
|
|
286
|
+
# Formula to compute the factor of gain:
|
|
287
|
+
# (2 matrices * theoretical symmetry factor + LU decomposition + intermediate_step) / nb matrices without symmetry
|
|
288
|
+
"lu_decomposition": {
|
|
289
|
+
"simple": 2 / 3, # (2 * 1/2 + 1/2 + 1/2) / 3
|
|
290
|
+
"nested": 5 / 12, # (2 * 1/4 + 1/4 + 1/2) / 3
|
|
291
|
+
"rotation": 4 / 3,
|
|
292
|
+
},
|
|
293
|
+
# Formula to compute the factor of gain:
|
|
294
|
+
# (2 matrices * theoretical symmetry factor + intermediate step) / nb matrices without symmetry
|
|
295
|
+
"lu_decomposition_with_overwrite": {
|
|
296
|
+
"simple": 3 / 4, # (2 * 1/2 + 1/2) / 2
|
|
297
|
+
"nested": 1 / 2, # (2 * 1/4 + 1/2) / 2
|
|
298
|
+
"rotation": 3 / 2,
|
|
299
|
+
},
|
|
300
|
+
"gmres": {
|
|
301
|
+
"simple": 1 / 2,
|
|
302
|
+
"nested": 1 / 4,
|
|
303
|
+
"rotation": 1,
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if isinstance(problem.body.mesh, ReflectionSymmetricMesh):
|
|
308
|
+
if isinstance(problem.body.mesh.half, ReflectionSymmetricMesh):
|
|
309
|
+
# Should not go deeper than that, there is currently only two
|
|
310
|
+
# symmetries available
|
|
311
|
+
symmetry_type = "nested"
|
|
312
|
+
else:
|
|
313
|
+
symmetry_type = "simple"
|
|
314
|
+
symmetry_factor = solver_factors[self._linear_solver][symmetry_type]
|
|
315
|
+
elif isinstance(problem.body.mesh, RotationSymmetricMesh):
|
|
316
|
+
symmetry_factor = solver_factors[self._linear_solver]["rotation"] / problem.body.mesh.n
|
|
317
|
+
else:
|
|
318
|
+
symmetry_factor = 1.0
|
|
319
|
+
|
|
320
|
+
memory_peak = symmetry_factor * nb_faces**2 * nb_matrices * nb_bytes/1e9
|
|
321
|
+
return memory_peak
|