capytaine 2.3__cp39-cp39-macosx_14_0_arm64.whl → 3.0.0a1__cp39-cp39-macosx_14_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) 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 +7 -2
  5. capytaine/__init__.py +8 -12
  6. capytaine/bem/engines.py +234 -354
  7. capytaine/bem/problems_and_results.py +30 -21
  8. capytaine/bem/solver.py +205 -81
  9. capytaine/bodies/bodies.py +279 -862
  10. capytaine/bodies/dofs.py +136 -9
  11. capytaine/bodies/hydrostatics.py +540 -0
  12. capytaine/bodies/multibodies.py +216 -0
  13. capytaine/green_functions/{libs/Delhommeau_float32.cpython-39-darwin.so → Delhommeau_float32.cpython-39-darwin.so} +0 -0
  14. capytaine/green_functions/{libs/Delhommeau_float64.cpython-39-darwin.so → Delhommeau_float64.cpython-39-darwin.so} +0 -0
  15. capytaine/green_functions/abstract_green_function.py +2 -2
  16. capytaine/green_functions/delhommeau.py +50 -31
  17. capytaine/green_functions/hams.py +19 -13
  18. capytaine/io/legacy.py +3 -103
  19. capytaine/io/xarray.py +15 -10
  20. capytaine/meshes/__init__.py +2 -6
  21. capytaine/meshes/abstract_meshes.py +375 -0
  22. capytaine/meshes/clean.py +302 -0
  23. capytaine/meshes/clip.py +347 -0
  24. capytaine/meshes/export.py +89 -0
  25. capytaine/meshes/geometry.py +244 -394
  26. capytaine/meshes/io.py +433 -0
  27. capytaine/meshes/meshes.py +621 -676
  28. capytaine/meshes/predefined/cylinders.py +22 -56
  29. capytaine/meshes/predefined/rectangles.py +26 -85
  30. capytaine/meshes/predefined/spheres.py +4 -11
  31. capytaine/meshes/quality.py +118 -407
  32. capytaine/meshes/surface_integrals.py +48 -29
  33. capytaine/meshes/symmetric_meshes.py +641 -0
  34. capytaine/meshes/visualization.py +353 -0
  35. capytaine/post_pro/free_surfaces.py +1 -4
  36. capytaine/post_pro/kochin.py +10 -10
  37. capytaine/tools/block_circulant_matrices.py +275 -0
  38. capytaine/tools/lists_of_points.py +2 -2
  39. capytaine/tools/memory_monitor.py +45 -0
  40. capytaine/tools/symbolic_multiplication.py +31 -5
  41. capytaine/tools/timer.py +68 -42
  42. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +8 -14
  43. capytaine-3.0.0a1.dist-info/RECORD +65 -0
  44. capytaine-3.0.0a1.dist-info/WHEEL +6 -0
  45. capytaine/bodies/predefined/__init__.py +0 -6
  46. capytaine/bodies/predefined/cylinders.py +0 -151
  47. capytaine/bodies/predefined/rectangles.py +0 -111
  48. capytaine/bodies/predefined/spheres.py +0 -70
  49. capytaine/green_functions/FinGreen3D/.gitignore +0 -1
  50. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
  51. capytaine/green_functions/FinGreen3D/LICENSE +0 -165
  52. capytaine/green_functions/FinGreen3D/Makefile +0 -16
  53. capytaine/green_functions/FinGreen3D/README.md +0 -24
  54. capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
  55. capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
  56. capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
  57. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
  58. capytaine/green_functions/LiangWuNoblesse/Makefile +0 -18
  59. capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
  60. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
  61. capytaine/green_functions/libs/__init__.py +0 -0
  62. capytaine/io/mesh_loaders.py +0 -1086
  63. capytaine/io/mesh_writers.py +0 -692
  64. capytaine/io/meshio.py +0 -38
  65. capytaine/matrices/__init__.py +0 -16
  66. capytaine/matrices/block.py +0 -592
  67. capytaine/matrices/block_toeplitz.py +0 -325
  68. capytaine/matrices/builders.py +0 -89
  69. capytaine/matrices/linear_solvers.py +0 -232
  70. capytaine/matrices/low_rank.py +0 -395
  71. capytaine/meshes/clipper.py +0 -465
  72. capytaine/meshes/collections.py +0 -334
  73. capytaine/meshes/mesh_like_protocol.py +0 -37
  74. capytaine/meshes/properties.py +0 -276
  75. capytaine/meshes/quadratures.py +0 -80
  76. capytaine/meshes/symmetric.py +0 -392
  77. capytaine/tools/lru_cache.py +0 -49
  78. capytaine/ui/vtk/__init__.py +0 -3
  79. capytaine/ui/vtk/animation.py +0 -329
  80. capytaine/ui/vtk/body_viewer.py +0 -28
  81. capytaine/ui/vtk/helpers.py +0 -82
  82. capytaine/ui/vtk/mesh_viewer.py +0 -461
  83. capytaine-2.3.dist-info/RECORD +0 -92
  84. capytaine-2.3.dist-info/WHEEL +0 -4
  85. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
  86. {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
capytaine/bem/engines.py CHANGED
@@ -1,23 +1,23 @@
1
1
  """Definition of the methods to build influence matrices, using possibly some sparse structures."""
2
2
  # Copyright (C) 2017-2019 Matthieu Ancellin
3
- # See LICENSE file at <https://github.com/mancellin/capytaine>
3
+ # See LICENSE file at <https://github.com/capytaine/capytaine>
4
4
 
5
5
  import logging
6
6
  from abc import ABC, abstractmethod
7
+ from typing import Tuple, Union, Optional, Callable
7
8
 
8
9
  import numpy as np
9
- from scipy.linalg import lu_factor
10
- from scipy.sparse import coo_matrix
11
- from scipy.sparse import linalg as ssl
10
+ import scipy.sparse.linalg as ssl
12
11
 
13
- from capytaine.meshes.collections import CollectionOfMeshes
14
- from capytaine.meshes.symmetric import ReflectionSymmetricMesh, TranslationalSymmetricMesh, AxialSymmetricMesh
12
+ from capytaine.meshes.symmetric_meshes import ReflectionSymmetricMesh, RotationSymmetricMesh
15
13
 
16
- from capytaine.matrices import linear_solvers
17
- from capytaine.matrices.block import BlockMatrix
18
- from capytaine.matrices.low_rank import LowRankMatrix, NoConvergenceOfACA
19
- from capytaine.matrices.block_toeplitz import BlockSymmetricToeplitzMatrix, BlockToeplitzMatrix, BlockCirculantMatrix
20
- from capytaine.tools.lru_cache import lru_cache_with_strict_maxsize
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
21
 
22
22
  LOG = logging.getLogger(__name__)
23
23
 
@@ -30,60 +30,93 @@ class MatrixEngine(ABC):
30
30
  """Abstract method to build a matrix."""
31
31
 
32
32
  @abstractmethod
33
- def build_matrices(self, mesh1, mesh2, free_surface, water_depth, wavenumber, green_function, adjoint_double_layer):
33
+ def build_matrices(self, mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer):
34
34
  pass
35
35
 
36
- def build_S_matrix(self, *args, **kwargs):
37
- """Similar to :code:`build_matrices`, but returning only :math:`S`"""
38
- S, _ = self.build_matrices(*args, **kwargs) # Could be optimized...
39
- return S
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
40
43
 
41
44
 
42
45
  ##################
43
46
  # BASIC ENGINE #
44
47
  ##################
45
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
+
46
78
  class BasicMatrixEngine(MatrixEngine):
47
79
  """
48
- Simple engine that assemble a full matrix (except for one reflection symmetry).
49
- Basically only calls :code:`green_function.evaluate`.
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.
50
86
 
51
87
  Parameters
52
88
  ----------
89
+ green_function: AbstractGreenFunction
90
+ the low level implementation used to compute the coefficients of the matrices.
53
91
  linear_solver: str or function, optional
54
92
  Setting of the numerical solver for linear problems Ax = b.
55
93
  It can be set with the name of a preexisting solver
56
- (available: "direct" and "gmres", the former is the default choice)
94
+ (available: "lu_decomposition", "lu_decompositon_with_overwrite" and "gmres", the former is the default choice)
57
95
  or by passing directly a solver function.
58
- matrix_cache_size: int, optional
59
- number of matrices to keep in cache
60
96
  """
61
97
 
62
- available_linear_solvers = {'direct': linear_solvers.solve_directly,
63
- 'lu_decomposition': linear_solvers.LUSolverWithCache().solve,
64
- 'gmres': linear_solvers.solve_gmres,
65
- }
98
+ green_function: AbstractGreenFunction
99
+ _linear_solver: Union[str, Callable]
100
+ last_computed_matrices: Optional[Tuple[MatrixLike, LUDecomposedMatrixOrNot]]
66
101
 
67
- def __init__(self, *, linear_solver='lu_decomposition', matrix_cache_size=1):
102
+ def __init__(self, *, green_function=None, linear_solver='lu_decomposition'):
68
103
 
69
- if linear_solver in self.available_linear_solvers:
70
- self.linear_solver = self.available_linear_solvers[linear_solver]
71
- else:
72
- self.linear_solver = linear_solver
104
+ self.green_function = Delhommeau() if green_function is None else green_function
73
105
 
74
- if matrix_cache_size > 0:
75
- self.build_matrices = lru_cache_with_strict_maxsize(maxsize=matrix_cache_size)(self.build_matrices)
106
+ self._linear_solver = linear_solver
107
+
108
+ self.last_computed_inputs = None
109
+ self.last_computed_matrices = None
76
110
 
77
111
  self.exportable_settings = {
78
112
  'engine': 'BasicMatrixEngine',
79
- 'matrix_cache_size': matrix_cache_size,
80
113
  'linear_solver': str(linear_solver),
114
+ **self.green_function.exportable_settings,
81
115
  }
82
116
 
83
117
  def __str__(self):
84
- params = f"linear_solver=\'{self.exportable_settings['linear_solver']}\'"
85
- params += f", matrix_cache_size={self.exportable_settings['matrix_cache_size']}" if self.exportable_settings['matrix_cache_size'] != 1 else ""
86
- return f"BasicMatrixEngine({params})"
118
+ params= [f"green_function={self.green_function}", f"linear_solver={repr(self._linear_solver)}"]
119
+ return f"BasicMatrixEngine({', '.join(params)})"
87
120
 
88
121
  def __repr__(self):
89
122
  return self.__str__()
@@ -91,14 +124,83 @@ class BasicMatrixEngine(MatrixEngine):
91
124
  def _repr_pretty_(self, p, cycle):
92
125
  p.text(self.__str__())
93
126
 
94
- def build_matrices(self, mesh1, mesh2, free_surface, water_depth, wavenumber, green_function, adjoint_double_layer=True):
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):
95
197
  r"""Build the influence matrices between mesh1 and mesh2.
96
198
 
97
199
  Parameters
98
200
  ----------
99
- mesh1: Mesh or CollectionOfMeshes
201
+ mesh1: MeshLike or list of points
100
202
  mesh of the receiving body (where the potential is measured)
101
- mesh2: Mesh or CollectionOfMeshes
203
+ mesh2: MeshLike
102
204
  mesh of the source body (over which the source distribution is integrated)
103
205
  free_surface: float
104
206
  position of the free surface (default: :math:`z = 0`)
@@ -106,336 +208,114 @@ class BasicMatrixEngine(MatrixEngine):
106
208
  position of the sea bottom (default: :math:`z = -\infty`)
107
209
  wavenumber: float
108
210
  wavenumber (default: 1.0)
109
- green_function: AbstractGreenFunction
110
- object with an "evaluate" method that computes the Green function.
111
211
  adjoint_double_layer: bool, optional
112
212
  compute double layer for direct method (F) or adjoint double layer for indirect method (T) matrices (default: True)
113
213
 
114
214
  Returns
115
215
  -------
116
- tuple of matrix-like
216
+ tuple of matrix-like (Numpy arrays or BlockCirculantMatrix)
117
217
  the matrices :math:`S` and :math:`K`
118
218
  """
219
+ return self._build_and_cache_matrices_with_symmetries(
220
+ mesh1, mesh2, **gf_params
221
+ )
119
222
 
120
- if (isinstance(mesh1, ReflectionSymmetricMesh)
121
- and isinstance(mesh2, ReflectionSymmetricMesh)
122
- and mesh1.plane == mesh2.plane):
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
123
225
 
124
- S_a, V_a = self.build_matrices(
125
- mesh1[0], mesh2[0], free_surface, water_depth, wavenumber,
126
- green_function, adjoint_double_layer=adjoint_double_layer)
127
- S_b, V_b = self.build_matrices(
128
- mesh1[0], mesh2[1], free_surface, water_depth, wavenumber,
129
- green_function, adjoint_double_layer=adjoint_double_layer)
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
130
232
 
131
- return BlockSymmetricToeplitzMatrix([[S_a, S_b]]), BlockSymmetricToeplitzMatrix([[V_a, V_b]])
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)
132
265
 
133
266
  else:
134
- return green_function.evaluate(
135
- mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer=adjoint_double_layer
267
+ raise NotImplementedError(
268
+ f"Unknown `linear_solver` in BasicMatrixEngine: {self._linear_solver}"
136
269
  )
137
270
 
138
- ###################################
139
- # HIERARCHIAL TOEPLITZ MATRICES #
140
- ###################################
141
-
142
- class HierarchicalToeplitzMatrixEngine(MatrixEngine):
143
- """An experimental matrix engine that build a hierarchical matrix with
144
- some block-Toeplitz structure.
145
-
146
- Parameters
147
- ----------
148
- ACA_distance: float, optional
149
- Above this distance, the ACA is used to approximate the matrix with a low-rank block.
150
- ACA_tol: float, optional
151
- The tolerance of the ACA when building a low-rank matrix.
152
- matrix_cache_size: int, optional
153
- number of matrices to keep in cache
154
- """
155
-
156
- def __init__(self, *, ACA_distance=8.0, ACA_tol=1e-2, matrix_cache_size=1):
157
-
158
- if matrix_cache_size > 0:
159
- self.build_matrices = lru_cache_with_strict_maxsize(maxsize=matrix_cache_size)(self.build_matrices)
160
-
161
- self.ACA_distance = ACA_distance
162
- self.ACA_tol = ACA_tol
163
-
164
- self.linear_solver = linear_solvers.solve_gmres
165
-
166
- self.exportable_settings = {
167
- 'engine': 'HierarchicalToeplitzMatrixEngine',
168
- 'ACA_distance': ACA_distance,
169
- 'ACA_tol': ACA_tol,
170
- 'matrix_cache_size': matrix_cache_size,
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
+ },
171
305
  }
172
306
 
173
- def __str__(self):
174
- params = f"ACA_distance={self.ACA_distance}"
175
- params += f", ACA_tol={self.ACA_tol}"
176
- params += f", matrix_cache_size={self.exportable_settings['matrix_cache_size']}" if self.exportable_settings['matrix_cache_size'] != 1 else ""
177
- return f"HierarchicalToeplitzMatrixEngine({params})"
178
-
179
- def _repr_pretty_(self, p, cycle):
180
- p.text(self.__str__())
181
-
182
-
183
- def build_matrices(self,
184
- mesh1, mesh2, free_surface, water_depth, wavenumber, green_function,
185
- adjoint_double_layer=True):
186
-
187
- return self._build_matrices(
188
- mesh1, mesh2, free_surface, water_depth, wavenumber, green_function,
189
- adjoint_double_layer, _rec_depth=1)
190
-
191
-
192
- def _build_matrices(self,
193
- mesh1, mesh2, free_surface, water_depth, wavenumber, green_function,
194
- adjoint_double_layer, _rec_depth=1):
195
- """Recursively builds a hierarchical matrix between mesh1 and mesh2.
196
-
197
- Same arguments as :func:`BasicMatrixEngine.build_matrices`.
198
-
199
- :code:`_rec_depth` keeps track of the recursion depth only for pretty log printing.
200
- """
201
-
202
- if logging.getLogger().isEnabledFor(logging.DEBUG):
203
- log_entry = (
204
- "\t" * (_rec_depth+1) +
205
- "Build the S and K influence matrices between {mesh1} and {mesh2}"
206
- .format(mesh1=mesh1.name, mesh2=(mesh2.name if mesh2 is not mesh1 else 'itself'))
207
- )
208
- else:
209
- log_entry = "" # will not be used
210
-
211
- # Distance between the meshes (for ACA).
212
- distance = np.linalg.norm(mesh1.center_of_mass_of_nodes - mesh2.center_of_mass_of_nodes)
213
-
214
- # I) SPARSE COMPUTATION
215
- # I-i) BLOCK TOEPLITZ MATRIX
216
-
217
- if (isinstance(mesh1, ReflectionSymmetricMesh)
218
- and isinstance(mesh2, ReflectionSymmetricMesh)
219
- and mesh1.plane == mesh2.plane):
220
-
221
- LOG.debug(log_entry + " using mirror symmetry.")
222
-
223
- S_a, V_a = self._build_matrices(
224
- mesh1[0], mesh2[0], free_surface, water_depth, wavenumber, green_function,
225
- adjoint_double_layer=adjoint_double_layer, _rec_depth=_rec_depth+1)
226
- S_b, V_b = self._build_matrices(
227
- mesh1[0], mesh2[1], free_surface, water_depth, wavenumber, green_function,
228
- adjoint_double_layer=adjoint_double_layer, _rec_depth=_rec_depth+1)
229
-
230
- return BlockSymmetricToeplitzMatrix([[S_a, S_b]]), BlockSymmetricToeplitzMatrix([[V_a, V_b]])
231
-
232
- elif (isinstance(mesh1, TranslationalSymmetricMesh)
233
- and isinstance(mesh2, TranslationalSymmetricMesh)
234
- and np.allclose(mesh1.translation, mesh2.translation)
235
- and mesh1.nb_submeshes == mesh2.nb_submeshes):
236
-
237
- LOG.debug(log_entry + " using translational symmetry.")
238
-
239
- S_list, V_list = [], []
240
- for submesh in mesh2:
241
- S, V = self._build_matrices(
242
- mesh1[0], submesh, free_surface, water_depth, wavenumber, green_function,
243
- adjoint_double_layer=adjoint_double_layer, _rec_depth=_rec_depth+1)
244
- S_list.append(S)
245
- V_list.append(V)
246
- for submesh in mesh1[1:][::-1]:
247
- S, V = self._build_matrices(
248
- submesh, mesh2[0], free_surface, water_depth, wavenumber, green_function,
249
- adjoint_double_layer=adjoint_double_layer, _rec_depth=_rec_depth+1)
250
- S_list.append(S)
251
- V_list.append(V)
252
-
253
- return BlockToeplitzMatrix([S_list]), BlockToeplitzMatrix([V_list])
254
-
255
- elif (isinstance(mesh1, AxialSymmetricMesh)
256
- and isinstance(mesh2, AxialSymmetricMesh)
257
- and mesh1.axis == mesh2.axis
258
- and mesh1.nb_submeshes == mesh2.nb_submeshes):
259
-
260
- LOG.debug(log_entry + " using rotation symmetry.")
261
-
262
- S_line, V_line = [], []
263
- for submesh in mesh2[:mesh2.nb_submeshes]:
264
- S, V = self._build_matrices(
265
- mesh1[0], submesh, free_surface, water_depth, wavenumber, green_function,
266
- adjoint_double_layer=adjoint_double_layer, _rec_depth=_rec_depth+1)
267
- S_line.append(S)
268
- V_line.append(V)
269
-
270
- return BlockCirculantMatrix([S_line]), BlockCirculantMatrix([V_line])
271
-
272
- # I-ii) LOW-RANK MATRIX WITH ACA
273
-
274
- elif distance > self.ACA_distance*mesh1.diameter_of_nodes or distance > self.ACA_distance*mesh2.diameter_of_nodes:
275
-
276
- LOG.debug(log_entry + " using ACA.")
277
-
278
- def get_row_func(i):
279
- s, v = green_function.evaluate(
280
- mesh1.extract_one_face(i), mesh2,
281
- free_surface, water_depth, wavenumber,
282
- adjoint_double_layer=adjoint_double_layer
283
- )
284
- return s.flatten(), v.flatten()
285
-
286
- def get_col_func(j):
287
- s, v = green_function.evaluate(
288
- mesh1, mesh2.extract_one_face(j),
289
- free_surface, water_depth, wavenumber,
290
- adjoint_double_layer=adjoint_double_layer
291
- )
292
- return s.flatten(), v.flatten()
293
-
294
- try:
295
- return LowRankMatrix.from_rows_and_cols_functions_with_multi_ACA(
296
- get_row_func, get_col_func, mesh1.nb_faces, mesh2.nb_faces,
297
- nb_matrices=2, id_main=1, # Approximate V and get an approximation of S at the same time
298
- tol=self.ACA_tol, dtype=np.complex128)
299
- except NoConvergenceOfACA:
300
- pass # Continue with non sparse computation
301
-
302
- # II) NON-SPARSE COMPUTATIONS
303
- # II-i) BLOCK MATRIX
304
-
305
- if (isinstance(mesh1, CollectionOfMeshes)
306
- and isinstance(mesh2, CollectionOfMeshes)):
307
-
308
- LOG.debug(log_entry + " using block matrix structure.")
309
-
310
- S_matrix, V_matrix = [], []
311
- for submesh1 in mesh1:
312
- S_line, V_line = [], []
313
- for submesh2 in mesh2:
314
- S, V = self._build_matrices(
315
- submesh1, submesh2, free_surface, water_depth, wavenumber, green_function,
316
- adjoint_double_layer=adjoint_double_layer, _rec_depth=_rec_depth+1)
317
-
318
- S_line.append(S)
319
- V_line.append(V)
320
- S_matrix.append(S_line)
321
- V_matrix.append(V_line)
322
-
323
- return BlockMatrix(S_matrix), BlockMatrix(V_matrix)
324
-
325
- # II-ii) PLAIN NUMPY ARRAY
326
-
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
327
317
  else:
328
- LOG.debug(log_entry)
329
-
330
- S, V = green_function.evaluate(
331
- mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer=adjoint_double_layer
332
- )
333
- return S, V
318
+ symmetry_factor = 1.0
334
319
 
335
- class HierarchicalPrecondMatrixEngine(HierarchicalToeplitzMatrixEngine):
336
- """An experimental matrix engine that build a hierarchical matrix with
337
- some block-Toeplitz structure.
338
-
339
- Parameters
340
- ----------
341
- ACA_distance: float, optional
342
- Above this distance, the ACA is used to approximate the matrix with a low-rank block.
343
- ACA_tol: float, optional
344
- The tolerance of the ACA when building a low-rank matrix.
345
- matrix_cache_size: int, optional
346
- number of matrices to keep in cache
347
- """
348
-
349
- def __init__(self, *, ACA_distance=8.0, ACA_tol=1e-2, matrix_cache_size=1):
350
- super().__init__(ACA_distance=ACA_distance, ACA_tol=ACA_tol, matrix_cache_size=matrix_cache_size)
351
- self.linear_solver = linear_solvers.solve_precond_gmres
352
-
353
- def build_matrices(self,
354
- mesh1, mesh2, free_surface, water_depth, wavenumber,
355
- green_function, adjoint_double_layer=True):
356
- """Recursively builds a hierarchical matrix between mesh1 and mesh2,
357
- and precomputes some of the quantities needed for the preconditioner.
358
-
359
- Same arguments as :func:`BasicMatrixEngine.build_matrices`, except for rec_depth
360
- """
361
- # Build the matrices using the method of the parent class
362
- S, K = super().build_matrices(mesh1, mesh2, free_surface, water_depth,
363
- wavenumber, green_function,
364
- adjoint_double_layer=adjoint_double_layer)
365
-
366
- path_to_leaf = mesh1.path_to_leaf()
367
-
368
- n = len(path_to_leaf)
369
- N = K.shape[0]
370
-
371
- # Navigate to the diagonal blocks and compute their LU decompositions
372
- DLU = []
373
- diag_shapes = []
374
- for leaf in range(n):
375
- # Navigate to the block containing the one we need
376
- # (one layer above in the dendrogram)
377
- #upper_block = self.access_block_by_path(K, path_to_leaf[leaf][:-1])
378
- upper_block = K.access_block_by_path(path_to_leaf[leaf][:-1])
379
- # find the local index in the full path
380
- ind = path_to_leaf[leaf][-1]
381
- # compute the LU decomposition and add to the list
382
- DLU.append(lu_factor(upper_block.all_blocks[ind, ind]))
383
- diag_shapes.append(upper_block.all_blocks[ind, ind].shape[0])
384
-
385
- # Build the restriction and precompute its multiplication by K
386
- R = np.zeros((n, N), dtype=complex)
387
- RA = np.zeros((n, N), dtype=complex)
388
- for ii in range(n):
389
- row_slice = slice(sum(diag_shapes[:ii]), sum(diag_shapes[:ii+1]))
390
- R[ii, row_slice] = 1
391
- # Compute the multiplication using only the relevant slices of K
392
- # The slices are found by navigating the tree
393
- #RA[ii, :] = self.slice_rmatvec(R[ii, :], ii)
394
- Aloc = K
395
- v = R[ii, :]
396
- va = np.zeros(N, dtype=complex)
397
- free = [0, N]
398
-
399
- for lvl, jj in enumerate(path_to_leaf[ii]):
400
-
401
- Nrows = Aloc.all_blocks[jj, jj].shape[0]
402
-
403
- if jj==0:
404
- v = v[:Nrows]
405
- w = v @ Aloc.all_blocks[0,1]
406
- va[free[1]-len(w) : free[1]] = w
407
- free[1] = free[1] - len(w)
408
- else:
409
- v = v[-Nrows:]
410
- w = v @ Aloc.all_blocks[1, 0]
411
- va[free[0] : free[0]+len(w)] = w
412
- free[0] = free[0] + len(w)
413
-
414
- Aloc = Aloc.all_blocks[jj, jj]
415
-
416
- if lvl == len(path_to_leaf[ii])-1:
417
- w = v@Aloc
418
- va[free[0] : free[1]] = w
419
- free[0] = free[0] + len(w)
420
-
421
- RA[ii, :] = va
422
-
423
- Ac = RA @ R.T
424
- AcLU = lu_factor(Ac)
425
-
426
- # Now navigate again to the diagonal blocks and set them to zero
427
- for leaf in range(n):
428
- upper_block = K.access_block_by_path(path_to_leaf[leaf][:-1])
429
- ind = path_to_leaf[leaf][-1]
430
- # turn the diagonal block into a zero sparse matrix
431
- upper_block.all_blocks[ind, ind] = coo_matrix(upper_block.all_blocks[ind, ind].shape)
432
-
433
- def PinvA_mv(v):
434
- v = v + 1j*np.zeros(N)
435
- return v - linear_solvers._block_Jacobi_coarse_corr(
436
- K, np.zeros(N, dtype=complex), v,
437
- R, RA, AcLU, DLU, diag_shapes, n)
438
-
439
- PinvA = ssl.LinearOperator((N, N), matvec=PinvA_mv)
440
-
441
- return S, (K, R, RA, AcLU, DLU, diag_shapes, n, PinvA)
320
+ memory_peak = symmetry_factor * nb_faces**2 * nb_matrices * nb_bytes/1e9
321
+ return memory_peak