capytaine 2.3__cp312-cp312-win_amd64.whl → 3.0.0a1__cp312-cp312-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.
- capytaine/__about__.py +7 -2
- capytaine/__init__.py +11 -15
- capytaine/bem/engines.py +234 -354
- capytaine/bem/problems_and_results.py +30 -21
- capytaine/bem/solver.py +205 -81
- capytaine/bodies/bodies.py +279 -862
- capytaine/bodies/dofs.py +136 -9
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/{libs/Delhommeau_float32.cp312-win_amd64.dll.a → Delhommeau_float32.cp312-win_amd64.dll.a} +0 -0
- capytaine/green_functions/Delhommeau_float32.cp312-win_amd64.pyd +0 -0
- capytaine/green_functions/{libs/Delhommeau_float64.cp312-win_amd64.dll.a → Delhommeau_float64.cp312-win_amd64.dll.a} +0 -0
- capytaine/green_functions/Delhommeau_float64.cp312-win_amd64.pyd +0 -0
- capytaine/green_functions/abstract_green_function.py +2 -2
- capytaine/green_functions/delhommeau.py +50 -31
- capytaine/green_functions/hams.py +19 -13
- capytaine/io/legacy.py +3 -103
- capytaine/io/xarray.py +15 -10
- capytaine/meshes/__init__.py +2 -6
- 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 +244 -394
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +621 -676
- capytaine/meshes/predefined/cylinders.py +22 -56
- capytaine/meshes/predefined/rectangles.py +26 -85
- capytaine/meshes/predefined/spheres.py +4 -11
- capytaine/meshes/quality.py +118 -407
- capytaine/meshes/surface_integrals.py +48 -29
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/free_surfaces.py +1 -4
- capytaine/post_pro/kochin.py +10 -10
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/lists_of_points.py +2 -2
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/symbolic_multiplication.py +31 -5
- capytaine/tools/timer.py +68 -42
- capytaine-3.0.0a1.dist-info/DELVEWHEEL +2 -0
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +8 -14
- capytaine-3.0.0a1.dist-info/RECORD +70 -0
- capytaine/bodies/predefined/__init__.py +0 -6
- capytaine/bodies/predefined/cylinders.py +0 -151
- capytaine/bodies/predefined/rectangles.py +0 -111
- capytaine/bodies/predefined/spheres.py +0 -70
- capytaine/green_functions/FinGreen3D/.gitignore +0 -1
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
- capytaine/green_functions/FinGreen3D/LICENSE +0 -165
- capytaine/green_functions/FinGreen3D/Makefile +0 -16
- capytaine/green_functions/FinGreen3D/README.md +0 -24
- capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
- capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
- capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
- capytaine/green_functions/LiangWuNoblesse/Makefile +0 -18
- capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
- capytaine/green_functions/libs/Delhommeau_float32.cp312-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp312-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/mesh_loaders.py +0 -1086
- capytaine/io/mesh_writers.py +0 -692
- capytaine/io/meshio.py +0 -38
- capytaine/matrices/__init__.py +0 -16
- capytaine/matrices/block.py +0 -592
- capytaine/matrices/block_toeplitz.py +0 -325
- capytaine/matrices/builders.py +0 -89
- capytaine/matrices/linear_solvers.py +0 -232
- capytaine/matrices/low_rank.py +0 -395
- capytaine/meshes/clipper.py +0 -465
- capytaine/meshes/collections.py +0 -334
- capytaine/meshes/mesh_like_protocol.py +0 -37
- capytaine/meshes/properties.py +0 -276
- capytaine/meshes/quadratures.py +0 -80
- capytaine/meshes/symmetric.py +0 -392
- capytaine/tools/lru_cache.py +0 -49
- capytaine/ui/vtk/__init__.py +0 -3
- capytaine/ui/vtk/animation.py +0 -329
- capytaine/ui/vtk/body_viewer.py +0 -28
- capytaine/ui/vtk/helpers.py +0 -82
- capytaine/ui/vtk/mesh_viewer.py +0 -461
- capytaine-2.3.dist-info/DELVEWHEEL +0 -2
- capytaine-2.3.dist-info/RECORD +0 -97
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/WHEEL +0 -0
- {capytaine-2.3.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()
|
capytaine/post_pro/kochin.py
CHANGED
|
@@ -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 = (
|
|
40
|
-
omega_bar = (result.body.
|
|
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.
|
|
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.
|
|
45
|
+
cih = np.exp(k*result.body.mesh_including_lid.faces_centers[:, 2])
|
|
46
46
|
|
|
47
|
-
# cih.shape = (
|
|
48
|
-
# omega_bar.T.shape = (nb_theta,
|
|
49
|
-
# result.body.mesh.faces_areas.shape = (
|
|
50
|
-
zs = cih * np.exp(-1j * k * omega_bar.T) * result.body.
|
|
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,
|
|
53
|
-
# result.sources.shape = (
|
|
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.
|
|
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,
|
|
14
|
+
if isinstance(points, AbstractMesh):
|
|
15
15
|
if keep_mesh:
|
|
16
16
|
return points, (points.nb_faces,)
|
|
17
17
|
else:
|