capytaine 2.3.1__cp310-cp310-win_amd64.whl → 3.0.0a1__cp310-cp310-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. capytaine/__about__.py +7 -2
  2. capytaine/__init__.py +11 -15
  3. capytaine/bem/engines.py +234 -354
  4. capytaine/bem/problems_and_results.py +14 -13
  5. capytaine/bem/solver.py +204 -80
  6. capytaine/bodies/bodies.py +278 -869
  7. capytaine/bodies/dofs.py +136 -9
  8. capytaine/bodies/hydrostatics.py +540 -0
  9. capytaine/bodies/multibodies.py +216 -0
  10. capytaine/green_functions/{libs/Delhommeau_float32.cp310-win_amd64.dll.a → Delhommeau_float32.cp310-win_amd64.dll.a} +0 -0
  11. capytaine/green_functions/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
  12. capytaine/green_functions/{libs/Delhommeau_float64.cp310-win_amd64.dll.a → Delhommeau_float64.cp310-win_amd64.dll.a} +0 -0
  13. capytaine/green_functions/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
  14. capytaine/green_functions/abstract_green_function.py +2 -2
  15. capytaine/green_functions/delhommeau.py +31 -16
  16. capytaine/green_functions/hams.py +19 -13
  17. capytaine/io/legacy.py +3 -103
  18. capytaine/io/xarray.py +11 -6
  19. capytaine/meshes/__init__.py +2 -6
  20. capytaine/meshes/abstract_meshes.py +375 -0
  21. capytaine/meshes/clean.py +302 -0
  22. capytaine/meshes/clip.py +347 -0
  23. capytaine/meshes/export.py +89 -0
  24. capytaine/meshes/geometry.py +244 -394
  25. capytaine/meshes/io.py +433 -0
  26. capytaine/meshes/meshes.py +617 -681
  27. capytaine/meshes/predefined/cylinders.py +22 -56
  28. capytaine/meshes/predefined/rectangles.py +26 -85
  29. capytaine/meshes/predefined/spheres.py +4 -11
  30. capytaine/meshes/quality.py +118 -407
  31. capytaine/meshes/surface_integrals.py +48 -29
  32. capytaine/meshes/symmetric_meshes.py +641 -0
  33. capytaine/meshes/visualization.py +353 -0
  34. capytaine/post_pro/free_surfaces.py +1 -4
  35. capytaine/post_pro/kochin.py +10 -10
  36. capytaine/tools/block_circulant_matrices.py +275 -0
  37. capytaine/tools/lists_of_points.py +2 -2
  38. capytaine/tools/memory_monitor.py +45 -0
  39. capytaine/tools/symbolic_multiplication.py +13 -1
  40. capytaine/tools/timer.py +58 -34
  41. capytaine-3.0.0a1.dist-info/DELVEWHEEL +2 -0
  42. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +7 -2
  43. capytaine-3.0.0a1.dist-info/RECORD +70 -0
  44. capytaine/bodies/predefined/__init__.py +0 -6
  45. capytaine/bodies/predefined/cylinders.py +0 -151
  46. capytaine/bodies/predefined/rectangles.py +0 -111
  47. capytaine/bodies/predefined/spheres.py +0 -70
  48. capytaine/green_functions/FinGreen3D/.gitignore +0 -1
  49. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
  50. capytaine/green_functions/FinGreen3D/LICENSE +0 -165
  51. capytaine/green_functions/FinGreen3D/Makefile +0 -16
  52. capytaine/green_functions/FinGreen3D/README.md +0 -24
  53. capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
  54. capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
  55. capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
  56. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
  57. capytaine/green_functions/LiangWuNoblesse/Makefile +0 -16
  58. capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
  59. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
  60. capytaine/green_functions/libs/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
  61. capytaine/green_functions/libs/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
  62. capytaine/green_functions/libs/__init__.py +0 -0
  63. capytaine/io/mesh_loaders.py +0 -1086
  64. capytaine/io/mesh_writers.py +0 -692
  65. capytaine/io/meshio.py +0 -38
  66. capytaine/matrices/__init__.py +0 -16
  67. capytaine/matrices/block.py +0 -592
  68. capytaine/matrices/block_toeplitz.py +0 -325
  69. capytaine/matrices/builders.py +0 -89
  70. capytaine/matrices/linear_solvers.py +0 -232
  71. capytaine/matrices/low_rank.py +0 -395
  72. capytaine/meshes/clipper.py +0 -465
  73. capytaine/meshes/collections.py +0 -342
  74. capytaine/meshes/mesh_like_protocol.py +0 -37
  75. capytaine/meshes/properties.py +0 -276
  76. capytaine/meshes/quadratures.py +0 -80
  77. capytaine/meshes/symmetric.py +0 -462
  78. capytaine/tools/lru_cache.py +0 -49
  79. capytaine/ui/vtk/__init__.py +0 -3
  80. capytaine/ui/vtk/animation.py +0 -329
  81. capytaine/ui/vtk/body_viewer.py +0 -28
  82. capytaine/ui/vtk/helpers.py +0 -82
  83. capytaine/ui/vtk/mesh_viewer.py +0 -461
  84. capytaine-2.3.1.dist-info/DELVEWHEEL +0 -2
  85. capytaine-2.3.1.dist-info/RECORD +0 -97
  86. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
  87. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/WHEEL +0 -0
  88. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,353 @@
1
+ # Copyright 2025 Mews Labs
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import importlib
16
+ from typing import Optional, List
17
+
18
+ import numpy as np
19
+
20
+ from capytaine import __version__
21
+ from capytaine.tools.optional_imports import import_optional_dependency
22
+
23
+
24
+ def show_3d(mesh, *, backend=None, **kwargs):
25
+ """Dispatch the 3D viewing to one of the available backends below."""
26
+ backends_functions = {
27
+ "pyvista": show_pyvista,
28
+ "matplotlib": show_matplotlib,
29
+ }
30
+ if backend is not None:
31
+ if backend in backends_functions:
32
+ return backends_functions[backend](mesh, **kwargs)
33
+ else:
34
+ raise NotImplementedError(f"Backend '{backend}' is not implemented.")
35
+ else:
36
+ for backend in backends_functions:
37
+ try:
38
+ return backends_functions[backend](mesh, **kwargs)
39
+ except (NotImplementedError, ImportError):
40
+ pass
41
+ raise NotImplementedError(f"No compatible backend found to show the mesh {mesh}"
42
+ "Consider installing `matplotlib` or `pyvista`.")
43
+
44
+
45
+ def show_pyvista(
46
+ mesh,
47
+ *,
48
+ ghost_meshes=None,
49
+ plotter=None,
50
+ normal_vectors=False,
51
+ display_free_surface=True,
52
+ water_depth=np.inf,
53
+ color_field=None,
54
+ cbar_label="",
55
+ **kwargs
56
+ ) -> Optional["pv.Plotter"]: # noqa: F821
57
+ """
58
+ Visualize the mesh using PyVista.
59
+
60
+ PyVista default keyboards controls: https://docs.pyvista.org/api/plotting/plotting
61
+
62
+ Parameters
63
+ ----------
64
+ mesh : Mesh
65
+ The mesh object to visualize.
66
+ ghost_meshes: List[Mesh], optional
67
+ Additional meshes, shown in transparency
68
+ plotter: pv.Plotter, optional
69
+ If provided, use this PyVista plotter and return it at the end.
70
+ Otherwise a new one is created and the 3D view is displayed at the end.
71
+ normal_vectors: bool, optional
72
+ If True, display normal vector (default: True)
73
+ display_free_surface: bool, optional
74
+ If True, display free surface and if `water_depth` is finite display the sea bottom.
75
+ (default: True)
76
+ water_depth: float, optional
77
+ Where to display the sea bottom if `display_free_surface` is True
78
+ color_field: array of shape (nb_faces, ), optional
79
+ Scalar field to be plot on the mesh.
80
+ cmap: matplotlib colormap, optional
81
+ Colormap to use for scalar field plotting.
82
+ cbar_label: string, optional
83
+ Label for colorbar show color field scale
84
+ kwargs : additional optional arguments
85
+ Additional arguments passed to PyVista's add_mesh methods for customization (e.g. mesh color).
86
+ """
87
+ pv = import_optional_dependency("pyvista")
88
+
89
+ all_meshes_in_scene: List[Mesh] = [mesh] if ghost_meshes is None else [mesh, *ghost_meshes]
90
+ pv_meshes = [m.export_to_pyvista() for m in all_meshes_in_scene]
91
+
92
+ if color_field is not None and isinstance(color_field, np.ndarray):
93
+ acc_faces = 0
94
+ # Split the content of color_fields into the meshes in the scene
95
+ for m in pv_meshes:
96
+ m.cell_data["color_field"] = color_field[acc_faces:acc_faces+m.n_cells]
97
+ acc_faces = acc_faces + m.n_cells
98
+
99
+ if plotter is None:
100
+ default_plotter = True
101
+ plotter = pv.Plotter()
102
+ else:
103
+ default_plotter = False
104
+
105
+ if color_field is not None:
106
+ kwargs.setdefault("scalars", "color_field")
107
+ kwargs.setdefault("scalar_bar_args", {"title": cbar_label})
108
+ plotter.add_mesh(pv_meshes[0], name="hull", show_edges=True, **kwargs)
109
+
110
+ for i_ghost, g_mesh in enumerate(pv_meshes[1:]):
111
+ plotter.add_mesh(
112
+ g_mesh,
113
+ name=f"symmetric_hull_{i_ghost}",
114
+ opacity=0.4,
115
+ show_edges=False,
116
+ **kwargs
117
+ )
118
+
119
+ # NORMALS
120
+ def show_normals():
121
+ mini = mesh.vertices.min()
122
+ maxi = mesh.vertices.max()
123
+ plotter.add_arrows(
124
+ mesh.faces_centers,
125
+ mesh.faces_normals,
126
+ name="normals",
127
+ mag=0.04*(maxi-mini),
128
+ show_scalar_bar=False
129
+ )
130
+
131
+ def toggle_normals():
132
+ nonlocal normal_vectors
133
+ if normal_vectors:
134
+ normal_vectors = False
135
+ plotter.remove_actor('normals')
136
+ else:
137
+ normal_vectors = True
138
+ show_normals()
139
+
140
+ if normal_vectors:
141
+ show_normals()
142
+ plotter.add_key_event("n", lambda : toggle_normals())
143
+
144
+ scene_min = np.min([m.vertices[:, :].min(axis=0) for m in all_meshes_in_scene], axis=0)
145
+ scene_max = np.max([m.vertices[:, :].max(axis=0) for m in all_meshes_in_scene], axis=0)
146
+
147
+ # FREE SURFACE
148
+ def show_free_surface():
149
+ center = (scene_min[:2] + scene_max[:2]) / 2
150
+ diam = 1.1*(scene_max[:2] - scene_min[:2])
151
+ plane = pv.Plane(center=(*center, 0), direction=(0, 0, 1), i_size=diam[0], j_size=diam[1])
152
+ plotter.add_mesh(plane, color="blue", opacity=0.5, name="display_free_surface")
153
+ if water_depth != np.inf:
154
+ plane = pv.Plane(center=(*center, -water_depth), direction=(0, 0, 1), i_size=diam[0], j_size=diam[1])
155
+ plotter.add_mesh(plane, color="brown", opacity=0.5, name="display_sea_bottom")
156
+
157
+
158
+ def toggle_free_surface():
159
+ nonlocal display_free_surface
160
+ if display_free_surface:
161
+ display_free_surface = False
162
+ plotter.remove_actor('display_free_surface')
163
+ if water_depth != np.inf:
164
+ plotter.remove_actor('display_sea_bottom')
165
+ else:
166
+ display_free_surface = True
167
+ show_free_surface()
168
+
169
+ if display_free_surface:
170
+ show_free_surface()
171
+
172
+ plotter.add_key_event("h", lambda : toggle_free_surface())
173
+
174
+ # BOUNDS
175
+ def show_bounds():
176
+ plotter.show_bounds(grid='back', location='outer', n_xlabels=2, n_ylabels=2, n_zlabels=2)
177
+
178
+ bounds = True
179
+ show_bounds()
180
+ def toggle_bounds():
181
+ nonlocal bounds
182
+ if bounds:
183
+ plotter.remove_bounds_axes()
184
+ bounds = False
185
+ else:
186
+ show_bounds()
187
+ plotter.update()
188
+ bounds = True
189
+
190
+
191
+ plotter.add_key_event("b", lambda: toggle_bounds())
192
+
193
+ plotter.add_key_event("T", lambda : plotter.view_xy())
194
+ plotter.add_key_event("B", lambda : plotter.view_xy(negative=True))
195
+ plotter.add_key_event("S", lambda : plotter.view_xz())
196
+ plotter.add_key_event("P", lambda : plotter.view_xz(negative=True))
197
+ plotter.add_key_event("F", lambda : plotter.view_yz())
198
+ plotter.add_key_event("R", lambda : plotter.view_yz(negative=True))
199
+
200
+ view_clipping = {'x': 0, 'y': 0} # 0 = no clipping, +1 clipping one side, -1 clipping other side
201
+ def clipped_mesh():
202
+ nonlocal view_clipping
203
+ clipped_pv_mesh = pv_meshes[0]
204
+ for dir in ['x', 'y']:
205
+ if view_clipping[dir] == 1:
206
+ clipped_pv_mesh = clipped_pv_mesh.clip(dir)
207
+ elif view_clipping[dir] == -1:
208
+ clipped_pv_mesh = clipped_pv_mesh.clip("-" + dir)
209
+ return clipped_pv_mesh
210
+
211
+ def toggle_view_clipping(dir):
212
+ nonlocal view_clipping
213
+ if view_clipping[dir] == 0:
214
+ view_clipping[dir] = +1
215
+ elif view_clipping[dir] == +1:
216
+ view_clipping[dir] = -1
217
+ else:
218
+ view_clipping[dir] = 0
219
+ plotter.add_mesh(clipped_mesh(), name="hull", show_edges=True, **kwargs)
220
+
221
+ plotter.add_key_event("X", lambda : toggle_view_clipping("x"))
222
+ plotter.add_key_event("Y", lambda : toggle_view_clipping("y"))
223
+
224
+ plotter.add_text(
225
+ f"Capytaine version {__version__}\n\n"
226
+ """Keyboard controls:
227
+ b: toggle scale and bounding box
228
+ h: toggle free surface (and sea bottom if water depth was given)
229
+ n: toggle normal vectors
230
+ T,B,P,S,F,R: view [T]op, [B]ottom, [P]ort, [S]tarboard, [F]ront, [R]ear
231
+ X, Y: toggle displaying clipped mesh in x or y direction
232
+ q: exit
233
+ """,
234
+ position="upper_left",
235
+ font_size=10
236
+ )
237
+ plotter.show_axes() # xyz in bottom left corner
238
+
239
+ if default_plotter:
240
+ plotter.show()
241
+ else:
242
+ return plotter
243
+
244
+
245
+ def show_matplotlib(
246
+ mesh,
247
+ *,
248
+ ghost_meshes=None,
249
+ ax=None,
250
+ bounding_box=None,
251
+ normal_vectors=False,
252
+ scale_normal_vector=None,
253
+ color_field=None,
254
+ cmap=None,
255
+ cbar_label=None,
256
+ **kwargs
257
+ ):
258
+ """
259
+ Visualize the mesh using Matplotlib.
260
+
261
+ Parameters
262
+ ----------
263
+ mesh : Mesh
264
+ The mesh object to visualize.
265
+ ghost_meshes: List[Mesh], optional
266
+ Additional meshes. In the matplotlib viewer, they are just merged with the main mesh.
267
+ ax: matplotlib axis
268
+ The 3d axis in which to plot the mesh. If not provided, create a new one.
269
+ bounding_box: tuple[tuple[int]], optional
270
+ Min and max coordinates values to display in each three dimensions.
271
+ normal_vectors: bool, optional
272
+ If True, display normal vector.
273
+ scale_normal_vector: array of shape (nb_faces, ), optional
274
+ Scale separately each of the normal vectors.
275
+ color_field: array of shape (nb_faces, ), optional
276
+ Scalar field to be plot on the mesh (optional).
277
+ cmap: matplotlib colormap, optional
278
+ Colormap to use for scalar field plotting.
279
+ cbar_label: string, optional
280
+ Label for colorbar show color field scale
281
+
282
+ Other parameters are passed to Poly3DCollection.
283
+ """
284
+ matplotlib = import_optional_dependency("matplotlib")
285
+ plt = importlib.import_module("matplotlib.pyplot")
286
+ cm = importlib.import_module("matplotlib.cm")
287
+
288
+ mpl_toolkits = import_optional_dependency("mpl_toolkits", package_name="matplotlib")
289
+ Poly3DCollection = mpl_toolkits.mplot3d.art3d.Poly3DCollection
290
+
291
+ default_axis = ax is None
292
+ if default_axis:
293
+ fig = plt.figure(layout="constrained")
294
+ ax = fig.add_subplot(111, projection="3d")
295
+ ax.set_box_aspect([1, 1, 1]) # Equal aspect ratio
296
+
297
+ all_meshes_in_scene: List[Mesh] = [mesh] if ghost_meshes is None else [mesh, *ghost_meshes]
298
+
299
+ faces = []
300
+ for m in all_meshes_in_scene:
301
+ for face in m.faces:
302
+ vertices = [m.vertices[int(index_vertex), :] for index_vertex in face]
303
+ faces.append(vertices)
304
+
305
+ if color_field is None:
306
+ if 'facecolors' not in kwargs:
307
+ kwargs['facecolors'] = "yellow"
308
+ else:
309
+ if cmap is None:
310
+ cmap = matplotlib.colormaps['coolwarm']
311
+ m = cm.ScalarMappable(cmap=cmap)
312
+ m.set_array([min(color_field), max(color_field)])
313
+ m.set_clim(vmin=min(color_field), vmax=max(color_field))
314
+ colors = m.to_rgba(color_field)
315
+ kwargs['facecolors'] = colors
316
+ kwargs.setdefault("edgecolor", "k")
317
+
318
+ ax.add_collection3d(Poly3DCollection(faces, **kwargs))
319
+
320
+ if color_field is not None:
321
+ cbar = plt.colorbar(m, ax=ax)
322
+ if cbar_label is not None:
323
+ cbar.set_label(cbar_label)
324
+
325
+ # Plot normal vectors.
326
+ if normal_vectors:
327
+ if scale_normal_vector is not None:
328
+ vectors = (scale_normal_vector * mesh.faces_normals.T).T
329
+ else:
330
+ vectors = mesh.faces_normals
331
+ ax.quiver(*zip(*mesh.faces_centers), *zip(*vectors), length=0.2)
332
+
333
+ ax.set_xlabel("x")
334
+ ax.set_ylabel("y")
335
+ ax.set_zlabel("z")
336
+
337
+ if bounding_box is None:
338
+ # auto cube around mesh
339
+ mini = mesh.vertices.min(axis=0)
340
+ maxi = mesh.vertices.max(axis=0)
341
+ center = (mini + maxi) / 2
342
+ radius = (maxi - mini).max() / 2
343
+ ax.set_xlim(center[0] - radius, center[0] + radius)
344
+ ax.set_ylim(center[1] - radius, center[1] + radius)
345
+ ax.set_zlim(center[2] - radius, center[2] + radius)
346
+ else:
347
+ (xmin, xmax), (ymin, ymax), (zmin, zmax) = bounding_box
348
+ ax.set_xlim(xmin, xmax)
349
+ ax.set_ylim(ymin, ymax)
350
+ ax.set_zlim(zmin, zmax)
351
+
352
+ if default_axis:
353
+ plt.show()
@@ -48,10 +48,7 @@ class FreeSurface():
48
48
  self.y_range = y_range
49
49
  self.ny = ny
50
50
 
51
- if name is None:
52
- self.name = f"free_surface_{next(Mesh._ids)}"
53
- else:
54
- self.name = name
51
+ self.name = name
55
52
 
56
53
  self.mesh = self._generate_mesh()
57
54
 
@@ -36,19 +36,19 @@ def compute_kochin(result, theta, ref_point=(0.0, 0.0)):
36
36
  k = result.wavenumber
37
37
  h = result.water_depth
38
38
 
39
- # omega_bar.shape = (nb_faces, 2) @ (2, nb_theta)
40
- omega_bar = (result.body.mesh.faces_centers[:, 0:2] - ref_point) @ (np.cos(theta), np.sin(theta))
39
+ # omega_bar.shape = (nb_faces_including_lid, 2) @ (2, nb_theta)
40
+ omega_bar = (result.body.mesh_including_lid.faces_centers[:, 0:2] - ref_point) @ (np.cos(theta), np.sin(theta))
41
41
 
42
42
  if 0 <= k*h < 20:
43
- cih = np.cosh(k*(result.body.mesh.faces_centers[:, 2]+h))/np.cosh(k*h)
43
+ cih = np.cosh(k*(result.body.mesh_including_lid.faces_centers[:, 2]+h))/np.cosh(k*h)
44
44
  else:
45
- cih = np.exp(k*result.body.mesh.faces_centers[:, 2])
45
+ cih = np.exp(k*result.body.mesh_including_lid.faces_centers[:, 2])
46
46
 
47
- # cih.shape = (nb_faces,)
48
- # omega_bar.T.shape = (nb_theta, nb_faces)
49
- # result.body.mesh.faces_areas.shape = (nb_faces,)
50
- zs = cih * np.exp(-1j * k * omega_bar.T) * result.body.mesh.faces_areas
47
+ # cih.shape = (nb_faces_including_lid,)
48
+ # omega_bar.T.shape = (nb_theta, nb_faces_including_lid)
49
+ # result.body.mesh.faces_areas.shape = (nb_faces_including_lid,)
50
+ zs = cih * np.exp(-1j * k * omega_bar.T) * result.body.mesh_including_lid.faces_areas
51
51
 
52
- # zs.shape = (nb_theta, nb_faces)
53
- # result.sources.shape = (nb_faces,)
52
+ # zs.shape = (nb_theta, nb_faces_including_lid)
53
+ # result.sources.shape = (nb_faces_including_lid,)
54
54
  return zs @ result.sources/(4*np.pi)
@@ -0,0 +1,275 @@
1
+ """Implementation of block circulant matrices to be used for optimizing resolution with symmetries."""
2
+ # Copyright (C) 2025 Capytaine developers
3
+ # See LICENSE file at <https://github.com/capytaine/capytaine>
4
+
5
+ import logging
6
+ import numpy as np
7
+ from typing import List, Union, Sequence
8
+ from numpy.typing import NDArray, ArrayLike
9
+ import scipy.linalg as sl
10
+
11
+ LOG = logging.getLogger(__name__)
12
+
13
+
14
+ def circular_permutation(l: List, i: int) -> List:
15
+ return l[-i:] + l[:-i]
16
+
17
+
18
+ def leading_dimensions_at_the_end(a):
19
+ """Transform an array of shape (n, m, ...) into (..., n, m).
20
+ Invert of `leading_dimensions_at_the_end`"""
21
+ return np.moveaxis(a, [0, 1], [-2, -1])
22
+
23
+
24
+ def ending_dimensions_at_the_beginning(a):
25
+ """Transform an array of shape (..., n, m) into (n, m, ...).
26
+ Invert of `leading_dimensions_at_the_end`"""
27
+ return np.moveaxis(a, [-2, -1], [0, 1])
28
+
29
+
30
+ class BlockCirculantMatrix:
31
+ """Data-sparse representation of a block matrix of the following form
32
+
33
+ ( a d c b )
34
+ ( b a d c )
35
+ ( c b a d )
36
+ ( d c b a )
37
+
38
+ where a, b, c and d are matrices of the same shape.
39
+
40
+ Parameters
41
+ ----------
42
+ blocks: Sequence of matrix-like, can be also a ndarray of shape (nb_blocks, n, n, ...)
43
+ The **first column** of blocks [a, b, c, d, ...]
44
+ Each block should have the same shape.
45
+ """
46
+ def __init__(self, blocks: Sequence[ArrayLike]):
47
+ self.blocks = blocks
48
+ self.nb_blocks = len(blocks)
49
+ assert all(self.blocks[0].shape == b.shape for b in self.blocks[1:])
50
+ assert all(self.blocks[0].dtype == b.dtype for b in self.blocks[1:])
51
+ self.shape = (
52
+ self.nb_blocks*self.blocks[0].shape[0],
53
+ self.nb_blocks*self.blocks[0].shape[1],
54
+ *self.blocks[0].shape[2:]
55
+ )
56
+ self.ndim = len(self.shape)
57
+ self.dtype = self.blocks[0].dtype
58
+
59
+ def __array__(self, dtype=None, copy=True):
60
+ if not copy:
61
+ raise NotImplementedError
62
+ if dtype is None:
63
+ dtype = self.dtype
64
+ full_blocks = [np.asarray(b) for b in self.blocks] # Transform all blocks to numpy arrays
65
+ first_row = [full_blocks[0], *(full_blocks[1:][::-1])]
66
+ if self.ndim >= 3:
67
+ first_row = [leading_dimensions_at_the_end(b) for b in first_row]
68
+ # Need to permute_dims to conform to `block` usage when the array is more than 2D
69
+ full_matrix = np.block([[b for b in circular_permutation(first_row, i)]
70
+ for i in range(self.nb_blocks)]).astype(dtype)
71
+ if self.ndim >= 3:
72
+ full_matrix = ending_dimensions_at_the_beginning(full_matrix)
73
+ return full_matrix
74
+
75
+ def __add__(self, other):
76
+ if isinstance(other, BlockCirculantMatrix) and self.shape == other.shape:
77
+ return BlockCirculantMatrix([a + b for (a, b) in zip(self.blocks, other.blocks)])
78
+ else:
79
+ return NotImplemented
80
+
81
+ def __sub__(self, other):
82
+ if isinstance(other, BlockCirculantMatrix) and self.shape == other.shape:
83
+ return BlockCirculantMatrix([a - b for (a, b) in zip(self.blocks, other.blocks)])
84
+ else:
85
+ return NotImplemented
86
+
87
+ def __matmul__(self, other):
88
+ if self.nb_blocks == 2 and isinstance(other, np.ndarray) and other.ndim == 1:
89
+ a, b = self.blocks
90
+ x1, x2 = other[:len(other)//2], other[len(other)//2:]
91
+ y = np.concatenate([a @ x1 + b @ x2, b @ x1 + a @ x2], axis=0)
92
+ return y
93
+ elif self.nb_blocks == 3 and isinstance(other, np.ndarray) and other.ndim == 1:
94
+ a, b, c = self.blocks
95
+ n = len(other)
96
+ x1, x2, x3 = other[:n//3], other[n//3:2*n//3], other[2*n//3:]
97
+ y = np.concatenate([
98
+ a @ x1 + c @ x2 + b @ x3,
99
+ b @ x1 + a @ x2 + c @ x3,
100
+ c @ x1 + b @ x2 + a @ x3,
101
+ ], axis=0)
102
+ return y
103
+ elif isinstance(other, np.ndarray) and other.ndim == 1:
104
+ self.blocks
105
+ y = np.zeros(other.shape, dtype=np.result_type(self.dtype, other.dtype))
106
+ blocks_indices = list(range(self.nb_blocks))
107
+ for i, x_i in enumerate(np.split(other, self.nb_blocks)):
108
+ y += np.concatenate([self.blocks[j] @ x_i for j in circular_permutation(blocks_indices, i)])
109
+ return y
110
+ else:
111
+ return NotImplemented
112
+
113
+ def matvec(self, other):
114
+ return self.__matmul__(other)
115
+
116
+ def block_diagonalize(self) -> "BlockDiagonalMatrix":
117
+ if self.ndim == 2 and self.nb_blocks == 2:
118
+ a, b = self.blocks
119
+ return BlockDiagonalMatrix([a + b, a - b])
120
+ elif self.ndim == 2 and self.nb_blocks == 3:
121
+ a, b, c = self.blocks
122
+ return BlockDiagonalMatrix([
123
+ a + b + c,
124
+ a + np.exp(-2j*np.pi/3, dtype=self.dtype) * b + np.exp(2j*np.pi/3, dtype=self.dtype) * c,
125
+ a + np.exp(2j*np.pi/3, dtype=self.dtype) * b + np.exp(-2j*np.pi/3, dtype=self.dtype) * c,
126
+ ])
127
+ elif self.ndim == 2 and self.nb_blocks == 4:
128
+ a, b, c, d = self.blocks
129
+ return BlockDiagonalMatrix([
130
+ a + b + c + d,
131
+ a - 1j*b - c + 1j*d,
132
+ a - b + c - d,
133
+ a + 1j*b - c - 1j*d,
134
+ ])
135
+ elif self.ndim == 2 and all(isinstance(b, np.ndarray) for b in self.blocks):
136
+ return BlockDiagonalMatrix(np.fft.fft(np.asarray(self.blocks), axis=0))
137
+ else:
138
+ raise NotImplementedError()
139
+
140
+ def solve(self, b: np.ndarray) -> np.ndarray:
141
+ LOG.debug("Called solve on %s of shape %s",
142
+ self.__class__.__name__, self.shape)
143
+ n = self.nb_blocks
144
+ b_fft = np.fft.fft(b.reshape((n, b.shape[0]//n)), axis=0).reshape(b.shape)
145
+ res_fft = self.block_diagonalize().solve(b_fft)
146
+ res = np.fft.ifft(res_fft.reshape((n, b.shape[0]//n)), axis=0).reshape(b.shape)
147
+ LOG.debug("Done")
148
+ return res
149
+
150
+
151
+ class BlockDiagonalMatrix:
152
+ """Data-sparse representation of a block matrix of the following form
153
+
154
+ ( a 0 0 0 )
155
+ ( 0 b 0 0 )
156
+ ( 0 0 c 0 )
157
+ ( 0 0 0 d )
158
+
159
+ where a, b, c and d are matrices of the same shape.
160
+
161
+ Parameters
162
+ ----------
163
+ blocks: iterable of matrix-like
164
+ The blocks [a, b, c, d, ...]
165
+ """
166
+ def __init__(self, blocks: Sequence[ArrayLike]):
167
+ self.blocks = blocks
168
+ self.nb_blocks = len(blocks)
169
+ assert all(blocks[0].shape == b.shape for b in blocks[1:])
170
+ self.shape = (
171
+ sum(bl.shape[0] for bl in blocks),
172
+ sum(bl.shape[1] for bl in blocks)
173
+ )
174
+ assert all(blocks[0].dtype == b.dtype for b in blocks[1:])
175
+ self.dtype = blocks[0].dtype
176
+
177
+ def __array__(self, dtype=None, copy=True):
178
+ if not copy:
179
+ raise NotImplementedError
180
+ if dtype is None:
181
+ dtype = self.dtype
182
+ full_blocks = [np.asarray(b) for b in self.blocks] # Transform all blocks to numpy arrays
183
+ if self.ndim >= 3:
184
+ full_blocks = [leading_dimensions_at_the_end(b) for b in full_blocks]
185
+ full_matrix = np.block([
186
+ [full_blocks[i] if i == j else np.zeros(full_blocks[i].shape)
187
+ for j in range(self.nb_blocks)]
188
+ for i in range(self.nb_blocks)])
189
+ if self.ndim >= 3:
190
+ full_matrix = ending_dimensions_at_the_beginning(full_matrix)
191
+ return full_matrix
192
+
193
+ def solve(self, b: np.ndarray) -> np.ndarray:
194
+ LOG.debug("Called solve on %s of shape %s",
195
+ self.__class__.__name__, self.shape)
196
+ n = self.nb_blocks
197
+ rhs = np.split(b, n)
198
+ res = [np.linalg.solve(Ai, bi) if isinstance(Ai, np.ndarray) else Ai.solve(bi)
199
+ for (Ai, bi) in zip(self.blocks, rhs)]
200
+ LOG.debug("Done")
201
+ return np.hstack(res)
202
+
203
+
204
+ MatrixLike = Union[np.ndarray, BlockDiagonalMatrix, BlockCirculantMatrix]
205
+
206
+
207
+ def lu_decompose(A: MatrixLike, *, overwrite_a : bool = False):
208
+ if isinstance(A, np.ndarray):
209
+ return LUDecomposedMatrix(A, overwrite_a=overwrite_a)
210
+ elif isinstance(A, BlockDiagonalMatrix):
211
+ return LUDecomposedBlockDiagonalMatrix(A, overwrite_a=overwrite_a)
212
+ elif isinstance(A, BlockCirculantMatrix):
213
+ return LUDecomposedBlockCirculantMatrix(A, overwrite_a=overwrite_a)
214
+ else:
215
+ raise NotImplementedError()
216
+
217
+
218
+ class LUDecomposedMatrix:
219
+ def __init__(self, A: NDArray, *, overwrite_a : bool = False):
220
+ LOG.debug("LU decomp of %s of shape %s",
221
+ A.__class__.__name__, A.shape)
222
+ self._lu_decomp = sl.lu_factor(A, overwrite_a=overwrite_a)
223
+ self.shape = A.shape
224
+ self.dtype = A.dtype
225
+
226
+ def solve(self, b: np.ndarray) -> np.ndarray:
227
+ LOG.debug("Called solve on %s of shape %s",
228
+ self.__class__.__name__, self.shape)
229
+ return sl.lu_solve(self._lu_decomp, b)
230
+
231
+
232
+ class LUDecomposedBlockDiagonalMatrix:
233
+ """LU decomposition of a BlockDiagonalMatrix,
234
+ stored as the LU decomposition of each block."""
235
+ def __init__(self, bdm: BlockDiagonalMatrix, *, overwrite_a : bool = False):
236
+ LOG.debug("LU decomp of %s of shape %s",
237
+ bdm.__class__.__name__, bdm.shape)
238
+ self._lu_decomp = [lu_decompose(bl, overwrite_a=overwrite_a) for bl in bdm.blocks]
239
+ self.shape = bdm.shape
240
+ self.nb_blocks = bdm.nb_blocks
241
+ self.dtype = bdm.dtype
242
+
243
+ def solve(self, b: np.ndarray) -> np.ndarray:
244
+ LOG.debug("Called solve on %s of shape %s",
245
+ self.__class__.__name__, self.shape)
246
+ rhs = np.split(b, self.nb_blocks)
247
+ res = [Ai.solve(bi) for (Ai, bi) in zip(self._lu_decomp, rhs)]
248
+ return np.hstack(res)
249
+
250
+
251
+ class LUDecomposedBlockCirculantMatrix:
252
+ def __init__(self, bcm: BlockCirculantMatrix, *, overwrite_a : bool = False):
253
+ LOG.debug("LU decomp of %s of shape %s",
254
+ bcm.__class__.__name__, bcm.shape)
255
+ self._lu_decomp = lu_decompose(bcm.block_diagonalize(), overwrite_a=overwrite_a)
256
+ self.shape = bcm.shape
257
+ self.nb_blocks = bcm.nb_blocks
258
+ self.dtype = bcm.dtype
259
+
260
+ def solve(self, b: np.ndarray) -> np.ndarray:
261
+ LOG.debug("Called solve on %s of shape %s",
262
+ self.__class__.__name__, self.shape)
263
+ n = self.nb_blocks
264
+ b_fft = np.fft.fft(b.reshape((n, b.shape[0]//n)), axis=0).reshape(b.shape)
265
+ res_fft = self._lu_decomp.solve(b_fft)
266
+ res = np.fft.ifft(res_fft.reshape((n, b.shape[0]//n)), axis=0).reshape(b.shape)
267
+ return res
268
+
269
+
270
+ LUDecomposedMatrixLike = Union[LUDecomposedMatrix, LUDecomposedBlockDiagonalMatrix, LUDecomposedBlockCirculantMatrix]
271
+
272
+
273
+ def has_been_lu_decomposed(A):
274
+ # Python 3.8 does not support isinstance(A, LUDecomposedMatrixLike)
275
+ return isinstance(A, (LUDecomposedMatrix, LUDecomposedBlockDiagonalMatrix, LUDecomposedBlockCirculantMatrix))
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  from capytaine.bodies import FloatingBody
3
3
  from capytaine.post_pro.free_surfaces import FreeSurface
4
- from capytaine.meshes.mesh_like_protocol import MeshLike
4
+ from capytaine.meshes.abstract_meshes import AbstractMesh
5
5
 
6
6
 
7
7
  def _normalize_points(points, keep_mesh=False):
@@ -11,7 +11,7 @@ def _normalize_points(points, keep_mesh=False):
11
11
  else:
12
12
  return points.mesh.faces_centers, (points.mesh.nb_faces,)
13
13
 
14
- if isinstance(points, MeshLike):
14
+ if isinstance(points, AbstractMesh):
15
15
  if keep_mesh:
16
16
  return points, (points.nb_faces,)
17
17
  else: