capytaine 2.3.1__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 (82) hide show
  1. capytaine/__about__.py +7 -2
  2. capytaine/__init__.py +8 -12
  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.cpython-39-darwin.so → Delhommeau_float32.cpython-39-darwin.so} +0 -0
  11. capytaine/green_functions/{libs/Delhommeau_float64.cpython-39-darwin.so → Delhommeau_float64.cpython-39-darwin.so} +0 -0
  12. capytaine/green_functions/abstract_green_function.py +2 -2
  13. capytaine/green_functions/delhommeau.py +31 -16
  14. capytaine/green_functions/hams.py +19 -13
  15. capytaine/io/legacy.py +3 -103
  16. capytaine/io/xarray.py +11 -6
  17. capytaine/meshes/__init__.py +2 -6
  18. capytaine/meshes/abstract_meshes.py +375 -0
  19. capytaine/meshes/clean.py +302 -0
  20. capytaine/meshes/clip.py +347 -0
  21. capytaine/meshes/export.py +89 -0
  22. capytaine/meshes/geometry.py +244 -394
  23. capytaine/meshes/io.py +433 -0
  24. capytaine/meshes/meshes.py +617 -681
  25. capytaine/meshes/predefined/cylinders.py +22 -56
  26. capytaine/meshes/predefined/rectangles.py +26 -85
  27. capytaine/meshes/predefined/spheres.py +4 -11
  28. capytaine/meshes/quality.py +118 -407
  29. capytaine/meshes/surface_integrals.py +48 -29
  30. capytaine/meshes/symmetric_meshes.py +641 -0
  31. capytaine/meshes/visualization.py +353 -0
  32. capytaine/post_pro/free_surfaces.py +1 -4
  33. capytaine/post_pro/kochin.py +10 -10
  34. capytaine/tools/block_circulant_matrices.py +275 -0
  35. capytaine/tools/lists_of_points.py +2 -2
  36. capytaine/tools/memory_monitor.py +45 -0
  37. capytaine/tools/symbolic_multiplication.py +13 -1
  38. capytaine/tools/timer.py +58 -34
  39. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +7 -2
  40. capytaine-3.0.0a1.dist-info/RECORD +65 -0
  41. capytaine/bodies/predefined/__init__.py +0 -6
  42. capytaine/bodies/predefined/cylinders.py +0 -151
  43. capytaine/bodies/predefined/rectangles.py +0 -111
  44. capytaine/bodies/predefined/spheres.py +0 -70
  45. capytaine/green_functions/FinGreen3D/.gitignore +0 -1
  46. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
  47. capytaine/green_functions/FinGreen3D/LICENSE +0 -165
  48. capytaine/green_functions/FinGreen3D/Makefile +0 -16
  49. capytaine/green_functions/FinGreen3D/README.md +0 -24
  50. capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
  51. capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
  52. capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
  53. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
  54. capytaine/green_functions/LiangWuNoblesse/Makefile +0 -16
  55. capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
  56. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
  57. capytaine/green_functions/libs/__init__.py +0 -0
  58. capytaine/io/mesh_loaders.py +0 -1086
  59. capytaine/io/mesh_writers.py +0 -692
  60. capytaine/io/meshio.py +0 -38
  61. capytaine/matrices/__init__.py +0 -16
  62. capytaine/matrices/block.py +0 -592
  63. capytaine/matrices/block_toeplitz.py +0 -325
  64. capytaine/matrices/builders.py +0 -89
  65. capytaine/matrices/linear_solvers.py +0 -232
  66. capytaine/matrices/low_rank.py +0 -395
  67. capytaine/meshes/clipper.py +0 -465
  68. capytaine/meshes/collections.py +0 -342
  69. capytaine/meshes/mesh_like_protocol.py +0 -37
  70. capytaine/meshes/properties.py +0 -276
  71. capytaine/meshes/quadratures.py +0 -80
  72. capytaine/meshes/symmetric.py +0 -462
  73. capytaine/tools/lru_cache.py +0 -49
  74. capytaine/ui/vtk/__init__.py +0 -3
  75. capytaine/ui/vtk/animation.py +0 -329
  76. capytaine/ui/vtk/body_viewer.py +0 -28
  77. capytaine/ui/vtk/helpers.py +0 -82
  78. capytaine/ui/vtk/mesh_viewer.py +0 -461
  79. capytaine-2.3.1.dist-info/RECORD +0 -92
  80. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
  81. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/WHEEL +0 -0
  82. {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,641 @@
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
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import Optional, Union, Dict, Literal
19
+ from functools import cached_property, lru_cache
20
+ from itertools import chain
21
+
22
+ import numpy as np
23
+
24
+ from .abstract_meshes import AbstractMesh
25
+ from .meshes import Mesh
26
+
27
+ LOG = logging.getLogger(__name__)
28
+
29
+
30
+ class ReflectionSymmetricMesh(AbstractMesh):
31
+ """A mesh with reflection symmetry across a plane.
32
+
33
+ This class represents a mesh that has reflection symmetry across either
34
+ the xOz plane (y=0) or yOz plane (x=0). Only half of the mesh is stored,
35
+ and the full mesh can be reconstructed by reflecting across the symmetry plane.
36
+
37
+ Supports nested symmetries: if the half mesh is itself a ReflectionSymmetricMesh,
38
+ this represents a quarter mesh with symmetries across both planes.
39
+
40
+ Attributes
41
+ ----------
42
+ half: AbstractMesh
43
+ The half mesh
44
+ plane: str
45
+ The symmetry plane, either "xOz" or "yOz"
46
+ faces_metadata: Dict[str, np.ndarray], optional
47
+ Some arrays with the same first dimension (should be the number
48
+ of faces of the whole mesh) storing some fields defined on all the
49
+ faces of the mesh.
50
+ name: str, optional
51
+ Name for the mesh
52
+
53
+ Examples
54
+ --------
55
+ >>> # Create a mesh with xOz symmetry (y=0 plane)
56
+ >>> half_mesh = Mesh(vertices=..., faces=...)
57
+ >>> symmetric_mesh = ReflectionSymmetricMesh(half=half_mesh, plane="xOz")
58
+ >>>
59
+ >>> # Create a mesh with both xOz and yOz symmetries (quarter mesh)
60
+ >>> quarter_mesh = Mesh(vertices=..., faces=...)
61
+ >>> sym_xOz = ReflectionSymmetricMesh(half=quarter_mesh, plane="xOz")
62
+ >>> sym_both = ReflectionSymmetricMesh(half=sym_xOz, plane="yOz")
63
+ >>>
64
+ >>> # Get the full merged mesh
65
+ >>> full_mesh = symmetric_mesh.merged()
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ half: AbstractMesh, *,
71
+ plane: str,
72
+ faces_metadata: Optional[Dict[str, np.ndarray]] = None,
73
+ name: Optional[str] = None
74
+ ):
75
+
76
+ if plane not in ["xOz", "yOz"]:
77
+ raise ValueError(f"Plane must be 'xOz' or 'yOz', got '{plane}'")
78
+
79
+ self.half = half
80
+ self.plane = plane
81
+ if self.half.nb_faces > 0:
82
+ self.other_half = self.half.mirrored(plane)
83
+ else:
84
+ self.other_half = half # Degenerate case without any face...
85
+
86
+ self.faces_metadata = {k: np.concatenate([v, v]) for k, v in half.faces_metadata.items()}
87
+ if faces_metadata is not None:
88
+ self.faces_metadata.update(**{k: np.asarray(faces_metadata[k]) for k in faces_metadata})
89
+
90
+ for m in self.faces_metadata:
91
+ assert self.faces_metadata[m].shape[0] == self.nb_faces
92
+
93
+ self.name = str(name) if name is not None else None
94
+
95
+ @cached_property
96
+ def nb_vertices(self) -> int:
97
+ return 2*self.half.nb_vertices
98
+
99
+ @cached_property
100
+ def nb_faces(self) -> int:
101
+ return 2*self.half.nb_faces
102
+
103
+ @cached_property
104
+ def vertices(self) -> np.ndarray:
105
+ return np.concatenate([self.half.vertices, self.other_half.vertices])
106
+
107
+ @cached_property
108
+ def faces(self) -> np.ndarray:
109
+ return np.concatenate([self.half.faces, self.other_half.faces])
110
+
111
+ @cached_property
112
+ def faces_normals(self) -> np.ndarray:
113
+ return np.concatenate([self.half.faces_normals, self.other_half.faces_normals])
114
+
115
+ @cached_property
116
+ def faces_areas(self) -> np.ndarray:
117
+ return np.concatenate([self.half.faces_areas, self.other_half.faces_areas])
118
+
119
+ @cached_property
120
+ def faces_centers(self) -> np.ndarray:
121
+ return np.concatenate([self.half.faces_centers, self.other_half.faces_centers])
122
+
123
+ @cached_property
124
+ def faces_radiuses(self) -> np.ndarray:
125
+ return np.concatenate([self.half.faces_radiuses, self.other_half.faces_radiuses])
126
+
127
+ @cached_property
128
+ def quadrature_points(self) -> np.ndarray:
129
+ return (
130
+ np.concatenate([self.half.quadrature_points[0], self.other_half.quadrature_points[0]]),
131
+ np.concatenate([self.half.quadrature_points[1], self.other_half.quadrature_points[1]]),
132
+ )
133
+
134
+ def with_quadrature(self, quadrature_method):
135
+ return ReflectionSymmetricMesh(
136
+ self.half.with_quadrature(quadrature_method),
137
+ plane=self.plane,
138
+ faces_metadata=self.faces_metadata,
139
+ name=self.name,
140
+ )
141
+
142
+ def __str__(self) -> str:
143
+ return (f"ReflectionSymmetricMesh(half={str(self.half)}"
144
+ + f", plane='{self.plane}'"
145
+ + (f", name=\"{self.name}\")" if self.name is not None else ")"))
146
+
147
+ def __short_str__(self) -> str:
148
+ return self.__str__()
149
+
150
+ def __repr__(self) -> str:
151
+ return self.__str__()
152
+
153
+ def _repr_pretty_(self, p, cycle):
154
+ p.text(self.__str__())
155
+
156
+ def extract_faces(self, faces_id, *, name=None) -> Mesh:
157
+ return self.merged().extract_faces(faces_id, name=name)
158
+
159
+ def translated(self, shift, *, name=None) -> Union[ReflectionSymmetricMesh, Mesh]:
160
+ if ((self.plane == 'xOz' and abs(shift[1]) < 1e-12)
161
+ or(self.plane == 'yOz' and abs(shift[0]) < 1e-12)):
162
+ return ReflectionSymmetricMesh(
163
+ half=self.half.translated(shift),
164
+ plane=self.plane,
165
+ faces_metadata=self.faces_metadata,
166
+ name=name
167
+ )
168
+ else:
169
+ return self.merged().translated(shift, name=name)
170
+
171
+ def rotated_with_matrix(self, R, *, name=None) -> Mesh:
172
+ return self.merged().rotated_with_matrix(R, name=name)
173
+
174
+ def mirrored(self, plane: Literal['xOz', 'yOz'], *, name=None) -> ReflectionSymmetricMesh:
175
+ return ReflectionSymmetricMesh(
176
+ half=self.half.mirrored(plane),
177
+ plane=self.plane,
178
+ faces_metadata=self.faces_metadata,
179
+ name=name
180
+ )
181
+
182
+ def join_meshes(self, *meshes, return_masks=False, name=None) -> Union[ReflectionSymmetricMesh, Mesh]:
183
+ if (all(isinstance(m, ReflectionSymmetricMesh) for m in meshes) and
184
+ all(m.plane == self.plane for m in meshes)):
185
+ if return_masks:
186
+ joined_halves, half_masks = self.half.join_meshes(
187
+ *[m.half for m in meshes],
188
+ return_masks=True
189
+ )
190
+ masks = [np.concatenate([half_mask, half_mask]) for half_mask in half_masks]
191
+ else:
192
+ joined_halves = self.half.join_meshes(
193
+ *[m.half for m in meshes],
194
+ return_masks=False
195
+ )
196
+
197
+ faces_metadata = {k: np.concatenate(
198
+ [m.faces_metadata[k][:m.nb_faces//2] for m in chain([self], meshes)]
199
+ + [m.faces_metadata[k][m.nb_faces//2:] for m in chain([self], meshes)],
200
+ axis=0
201
+ )
202
+ for k in AbstractMesh._common_metadata_keys(*meshes)}
203
+
204
+ joined_mesh = ReflectionSymmetricMesh(
205
+ half=joined_halves,
206
+ plane=self.plane,
207
+ faces_metadata=faces_metadata,
208
+ name=name,
209
+ )
210
+
211
+ if return_masks:
212
+ return joined_mesh, masks
213
+ else:
214
+ return joined_mesh
215
+
216
+ else:
217
+ return Mesh.join_meshes(
218
+ self.merged(),
219
+ *[m.merged() for m in meshes],
220
+ return_masks=return_masks,
221
+ name=name
222
+ )
223
+
224
+ def generate_lid(self, z=0.0, faces_max_radius=None, name=None):
225
+ return ReflectionSymmetricMesh(
226
+ self.half.generate_lid(z=z, faces_max_radius=faces_max_radius),
227
+ plane=self.plane,
228
+ name=name
229
+ )
230
+
231
+ def extract_lid(self, z=0.0):
232
+ half_hull, half_lid = self.half.extract_lid(z=z)
233
+ return (
234
+ ReflectionSymmetricMesh(half_hull, plane=self.plane),
235
+ ReflectionSymmetricMesh(half_lid, plane=self.plane),
236
+ )
237
+
238
+ def with_normal_vector_going_down(self, **kwargs) -> ReflectionSymmetricMesh:
239
+ return ReflectionSymmetricMesh(
240
+ half=self.half.with_normal_vector_going_down(),
241
+ plane=self.plane,
242
+ faces_metadata=self.faces_metadata,
243
+ name=self.name)
244
+
245
+ def copy(self, *, faces_metadata=None, name=None) -> ReflectionSymmetricMesh:
246
+ if faces_metadata is None:
247
+ faces_metadata = self.faces_metadata.copy()
248
+ if name is None:
249
+ name = self.name
250
+ return ReflectionSymmetricMesh(
251
+ half=self.half.copy(),
252
+ plane=self.plane,
253
+ faces_metadata=faces_metadata,
254
+ name=self.name)
255
+
256
+ @lru_cache
257
+ def merged(self, name=None) -> Mesh:
258
+ return Mesh.join_meshes(
259
+ self.half.merged(),
260
+ self.other_half.merged()
261
+ ).with_metadata(
262
+ **self.faces_metadata
263
+ )
264
+
265
+ def clipped(self, *, origin, normal, name=None) -> Union[ReflectionSymmetricMesh, Mesh]:
266
+ if ((self.plane == 'xOz' and abs(normal[0]) < 1e-12)
267
+ or(self.plane == 'yOz' and abs(normal[1]) < 1e-12)):
268
+ clipped_half, indices = (
269
+ self.half
270
+ .with_metadata(index=np.arange(self.half.nb_faces))
271
+ .clipped(origin=origin, normal=normal)
272
+ .pop_metadata("index")
273
+ )
274
+ all_indices = np.concatenate([indices, indices + self.half.nb_faces])
275
+ metadata = {k: self.faces_metadata[k][all_indices] for k in self.faces_metadata.keys()}
276
+ return ReflectionSymmetricMesh(
277
+ half=clipped_half,
278
+ plane=self.plane,
279
+ faces_metadata=metadata,
280
+ name=name)
281
+ else:
282
+ LOG.warning("Dropping mesh reflection symmetry with respect to "
283
+ f"{self.plane} when clipping with respect to plane "
284
+ f"with origin {origin} and normal {normal}")
285
+ return self.merged().clipped(origin=origin, normal=normal, name=name)
286
+
287
+ def show(self, *, backend=None, ghost_meshes=None, **kwargs):
288
+ if ghost_meshes is None:
289
+ ghost_meshes = []
290
+ ghost_meshes = ghost_meshes + [self.other_half.merged()]
291
+ return self.half.show(backend=backend, ghost_meshes=ghost_meshes, **kwargs)
292
+
293
+ def export(self, format, **kwargs):
294
+ LOG.warning(f"Losing symmetric structure when exporting {self} to {format}")
295
+ return self.merged().export(format, **kwargs)
296
+
297
+
298
+ # For some backward compatibility:
299
+ yOz_Plane = "yOz"
300
+ xOz_Plane = "xOz"
301
+
302
+
303
+ class RotationSymmetricMesh(AbstractMesh):
304
+ """A mesh with rotation symmetry around the Oz axis.
305
+
306
+ This class represents a mesh that has n-fold rotational symmetry about
307
+ the z-axis. Only a wedge (1/n of the full mesh) is stored, and the full
308
+ mesh can be reconstructed by rotating the wedge n times.
309
+
310
+ Supports nested symmetries: the wedge mesh can be a ReflectionSymmetricMesh
311
+ for dihedral symmetry.
312
+
313
+ Attributes
314
+ ----------
315
+ wedge: AbstractMesh
316
+ The wedge mesh (1/n of the full mesh)
317
+ n: int
318
+ The rotation order (number of rotations to complete full circle)
319
+ axis: either 'z+' or 'z-'
320
+ Only the z-axis is supported, but two possible orientations can be used.
321
+ Both are equivalent, except for the ordering of the sub-meshes.
322
+ faces_metadata: Dict[str, np.ndarray], optional
323
+ Some arrays with the same first dimension (should be the number
324
+ of faces of the whole mesh) storing some fields defined on all the
325
+ faces of the mesh.
326
+ name: str, optional
327
+ Name for the mesh
328
+
329
+ Examples
330
+ --------
331
+ >>> # Create a mesh with 3-fold rotation symmetry about z-axis
332
+ >>> wedge_mesh = Mesh(vertices=..., faces=...)
333
+ >>> symmetric_mesh = RotationSymmetricMesh(wedge=wedge_mesh, n=3)
334
+ >>>
335
+ >>> # Get the full merged mesh
336
+ >>> full_mesh = symmetric_mesh.merged()
337
+ """
338
+
339
+ def __init__(
340
+ self,
341
+ wedge: AbstractMesh,
342
+ n: int, *,
343
+ axis: Literal['z+', 'z-'] = 'z+',
344
+ faces_metadata: Optional[Dict[str, np.ndarray]] = None,
345
+ name: Optional[str] = None
346
+ ):
347
+
348
+ if isinstance(wedge, ReflectionSymmetricMesh) and n > 4:
349
+ LOG.warning("RotationSymmetricMesh with n>4 and containing ReflectionSymmetricMesh are not fully supported at the moment. "
350
+ "You might prefer to define instead a ReflectionSymmetricMesh of a RotationSymmetricMesh.")
351
+
352
+ if n < 2:
353
+ raise ValueError(f"Rotation order must be >= 2, got: {n}")
354
+
355
+ self.wedge = wedge
356
+ self.n = n
357
+ self.axis = axis
358
+ if self.axis == 'z+':
359
+ self.all_wedges = [self.wedge] + [self.wedge.rotated_z(2*i*np.pi/n) for i in range(1, n)]
360
+ elif self.axis == 'z-':
361
+ self.all_wedges = [self.wedge] + [self.wedge.rotated_z(-2*i*np.pi/n) for i in range(1, n)]
362
+ else:
363
+ raise ValueError(f"Unsupported axis for RotationSymmetricMesh: {axis}")
364
+
365
+ self.faces_metadata = {k: np.concatenate([v]*n) for k, v in wedge.faces_metadata.items()}
366
+ if faces_metadata is not None:
367
+ self.faces_metadata.update(**{k: np.asarray(faces_metadata[k]) for k in faces_metadata})
368
+
369
+ for m in self.faces_metadata:
370
+ assert self.faces_metadata[m].shape[0] == self.nb_faces
371
+
372
+ self.name = str(name) if name is not None else None
373
+
374
+ @classmethod
375
+ def from_profile_points(cls, points: np.ndarray, n: int, *, faces_metadata=None, name=None):
376
+ """Return the mesh defined by the set of `points` repeated `n` times around the z-axis.
377
+
378
+ Points will be sorted by increasing z-coordinate before making a mesh,
379
+ in order to ensure that the normal vector are outwards.
380
+
381
+ Parameters
382
+ ---------
383
+ points: array of shape (..., 3)
384
+ A list of points in 3D.
385
+ n: int
386
+ The rotation order (number of rotations to complete full circle)
387
+ faces_metadata: Dict[str, np.ndarray], optional
388
+ Some arrays with the same first dimension (should be the number
389
+ of faces of the whole mesh) storing some fields defined on all the
390
+ faces of the mesh.
391
+ name: str, optional
392
+ Name for the mesh
393
+
394
+ Example
395
+ -------
396
+ >>> meridian_points = np.array([(np.sqrt(1-z**2), 0.0, z) for z in np.linspace(-1.0, 1.0, 10)])
397
+ >>> sphere = RotationSymmetricMesh.from_profile_points(meridian_points, n=10)
398
+ """
399
+ c, s = np.cos(2*np.pi/n), np.sin(2*np.pi/n)
400
+ rotation_matrix = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
401
+
402
+ points = np.asarray(sorted(list(points), key=lambda p: p[2])) # Sort by increasing z
403
+ vertices = np.concatenate([points, points @ rotation_matrix.T])
404
+ faces = np.array([(i, i+len(points), i+len(points)+1, i+1) for i in range(len(points)-1)])
405
+ wedge = Mesh(vertices=vertices, faces=faces)
406
+
407
+ return RotationSymmetricMesh(
408
+ wedge=wedge,
409
+ n=n,
410
+ faces_metadata=faces_metadata,
411
+ name=name
412
+ )
413
+
414
+ @cached_property
415
+ def nb_vertices(self) -> int:
416
+ return self.n * self.wedge.nb_vertices
417
+
418
+ @cached_property
419
+ def nb_faces(self) -> int:
420
+ return self.n * self.wedge.nb_faces
421
+
422
+ @cached_property
423
+ def vertices(self) -> np.ndarray:
424
+ return np.concatenate([w.vertices for w in self.all_wedges])
425
+
426
+ @cached_property
427
+ def faces(self) -> np.ndarray:
428
+ return np.concatenate([w.faces for w in self.all_wedges])
429
+
430
+ @cached_property
431
+ def faces_normals(self) -> np.ndarray:
432
+ return np.concatenate([w.faces_normals for w in self.all_wedges])
433
+
434
+ @cached_property
435
+ def faces_areas(self) -> np.ndarray:
436
+ return np.concatenate([w.faces_areas for w in self.all_wedges])
437
+
438
+ @cached_property
439
+ def faces_centers(self) -> np.ndarray:
440
+ return np.concatenate([w.faces_centers for w in self.all_wedges])
441
+
442
+ @cached_property
443
+ def faces_radiuses(self) -> np.ndarray:
444
+ return np.concatenate([w.faces_radiuses for w in self.all_wedges])
445
+
446
+ @cached_property
447
+ def quadrature_points(self) -> np.ndarray:
448
+ return (
449
+ np.concatenate([w.quadrature_points[0] for w in self.all_wedges]),
450
+ np.concatenate([w.quadrature_points[1] for w in self.all_wedges]),
451
+ )
452
+
453
+ def with_quadrature(self, quadrature_method):
454
+ return RotationSymmetricMesh(
455
+ self.wedge.with_quadrature(quadrature_method),
456
+ n=self.n,
457
+ axis=self.axis,
458
+ faces_metadata=self.faces_metadata,
459
+ name=self.name,
460
+ )
461
+
462
+ def __str__(self) -> str:
463
+ return (f"RotationSymmetricMesh(wedge={str(self.wedge)}"
464
+ + f", n={self.n}"
465
+ + (f", name=\"{self.name}\")" if self.name is not None else ")"))
466
+
467
+ def __short_str__(self) -> str:
468
+ return self.__str__()
469
+
470
+ def __repr__(self) -> str:
471
+ return self.__str__()
472
+
473
+ def _repr_pretty_(self, p, cycle):
474
+ p.text(self.__str__())
475
+
476
+ def extract_faces(self, faces_id, *, name=None) -> Mesh:
477
+ return self.merged().extract_faces(faces_id, name=name)
478
+
479
+ def translated(self, shift, *, name=None) -> Union[RotationSymmetricMesh, Mesh]:
480
+ if (abs(shift[0]) < 1e-12 and abs(shift[1] < 1e-12)):
481
+ # Vertical translation
482
+ return RotationSymmetricMesh(
483
+ self.wedge.translated_z(shift[2]),
484
+ n=self.n,
485
+ axis=self.axis,
486
+ faces_metadata=self.faces_metadata,
487
+ name=name)
488
+ else:
489
+ return self.merged().translated(shift, name=name)
490
+
491
+ def rotated_with_matrix(self, R, *, name=None) -> Union[RotationSymmetricMesh, Mesh]:
492
+ if (np.allclose(R[:, 2], [0.0, 0.0, 1.0])
493
+ and np.allclose(R[2, :], [0.0, 0.0, 1.0])):
494
+ # Rotation around the z-axis: we keep the symmetry
495
+ return RotationSymmetricMesh(
496
+ self.wedge.rotated_with_matrix(R),
497
+ n=self.n,
498
+ axis=self.axis,
499
+ faces_metadata=self.faces_metadata,
500
+ name=name,
501
+ )
502
+ else:
503
+ return self.merged().rotated_with_matrix(R, name=name)
504
+
505
+ @property
506
+ def _opposite_axis(self):
507
+ if self.axis == 'z+':
508
+ return 'z-'
509
+ else:
510
+ return 'z+'
511
+
512
+
513
+ def mirrored(self, plane: Literal['xOz', 'yOz'], *, name=None) -> RotationSymmetricMesh:
514
+ return RotationSymmetricMesh(
515
+ wedge=self.wedge.mirrored(plane),
516
+ n=self.n,
517
+ axis=self._opposite_axis,
518
+ faces_metadata=self.faces_metadata,
519
+ name=name
520
+ )
521
+
522
+ def _metadata_of_wedge(self, k, i):
523
+ return self.faces_metadata[k][i*self.wedge.nb_faces:(i+1)*self.wedge.nb_faces]
524
+
525
+ def join_meshes(self, *meshes, return_masks=False, name=None) -> Union[RotationSymmetricMesh, Mesh]:
526
+ if (all(isinstance(m, RotationSymmetricMesh) for m in meshes) and
527
+ all(m.n == self.n for m in meshes)):
528
+ if return_masks:
529
+ joined_wegdes, wedges_masks = self.wedge.join_meshes(
530
+ *[m.wedge for m in meshes],
531
+ return_masks=True
532
+ )
533
+ masks = [np.concatenate([w_mesh]*self.n) for w_mesh in wedges_masks]
534
+ else:
535
+ joined_wegdes = self.wedge.join_meshes(
536
+ *[m.wedge for m in meshes],
537
+ return_masks=False
538
+ )
539
+
540
+ faces_metadata = {k: np.concatenate(
541
+ [m._metadata_of_wedge(k, i) for i in range(self.n) for m in chain([self], meshes)] ,
542
+ axis=0
543
+ )
544
+ for k in AbstractMesh._common_metadata_keys(*meshes)}
545
+
546
+ joined_mesh = RotationSymmetricMesh(
547
+ wedge=joined_wegdes,
548
+ n=self.n,
549
+ axis=self.axis,
550
+ faces_metadata=faces_metadata,
551
+ name=name,
552
+ )
553
+
554
+ if return_masks:
555
+ return joined_mesh, masks
556
+ else:
557
+ return joined_mesh
558
+
559
+ else:
560
+ return Mesh.join_meshes(
561
+ self.merged(),
562
+ *[m.merged() for m in meshes],
563
+ return_masks=return_masks,
564
+ name=name
565
+ )
566
+
567
+ def generate_lid(self, z=0.0, faces_max_radius=None, name=None):
568
+ return RotationSymmetricMesh(
569
+ self.wedge.generate_lid(z=z, faces_max_radius=faces_max_radius),
570
+ axis=self.axis,
571
+ n=self.n,
572
+ name=name
573
+ )
574
+
575
+ def extract_lid(self, z=0.0):
576
+ wedge_hull, wedge_lid = self.wedge.extract_lid(z=z)
577
+ return (
578
+ RotationSymmetricMesh(wedge_hull, axis=self.axis, n=self.n),
579
+ RotationSymmetricMesh(wedge_lid, axis=self.axis, n=self.n),
580
+ )
581
+
582
+ def with_normal_vector_going_down(self, **kwargs) -> RotationSymmetricMesh:
583
+ return RotationSymmetricMesh(
584
+ wedge=self.wedge.with_normal_vector_going_down(),
585
+ n=self.n,
586
+ axis=self.axis,
587
+ faces_metadata=self.faces_metadata,
588
+ name=self.name)
589
+
590
+ def copy(self, *, faces_metadata=None, name=None) -> RotationSymmetricMesh:
591
+ if faces_metadata is None:
592
+ faces_metadata = self.faces_metadata.copy()
593
+ if name is None:
594
+ name = self.name
595
+ return RotationSymmetricMesh(
596
+ wedge=self.wedge.copy(),
597
+ n=self.n,
598
+ axis=self.axis,
599
+ faces_metadata=faces_metadata,
600
+ name=self.name)
601
+
602
+ @lru_cache
603
+ def merged(self, name=None) -> Mesh:
604
+ return Mesh.join_meshes(
605
+ *[w.merged() for w in self.all_wedges]
606
+ ).with_metadata(
607
+ **self.faces_metadata
608
+ )
609
+
610
+ def clipped(self, *, origin, normal, name=None) -> Union[RotationSymmetricMesh, Mesh]:
611
+ if (abs(normal[0]) < 1e-12 and abs(normal[1]) < 1e-12):
612
+ # Horizontal plane
613
+ clipped_wedge, indices = (
614
+ self.wedge
615
+ .with_metadata(index=np.arange(self.wedge.nb_faces))
616
+ .clipped(origin=origin, normal=normal)
617
+ .pop_metadata("index")
618
+ )
619
+ all_indices = np.concatenate([indices + i*self.wedge.nb_faces for i in range(self.n)])
620
+ metadata = {k: self.faces_metadata[k][all_indices] for k in self.faces_metadata.keys()}
621
+ return RotationSymmetricMesh(
622
+ wedge=clipped_wedge,
623
+ n=self.n,
624
+ axis=self.axis,
625
+ faces_metadata=metadata,
626
+ name=name)
627
+ else:
628
+ LOG.warning("Dropping mesh rotation symmetry with respect to "
629
+ f"z-axis when clipping with respect to plane "
630
+ f"with origin {origin} and normal {normal}")
631
+ return self.merged().clipped(origin=origin, normal=normal, name=name)
632
+
633
+ def show(self, *, backend=None, ghost_meshes=None, **kwargs):
634
+ if ghost_meshes is None:
635
+ ghost_meshes = []
636
+ ghost_meshes = ghost_meshes + [w.merged() for w in self.all_wedges[1:]]
637
+ return self.wedge.show(backend=backend, ghost_meshes=ghost_meshes, **kwargs)
638
+
639
+ def export(self, format, **kwargs):
640
+ LOG.warning(f"Losing symmetric structure when exporting {self} to {format}")
641
+ return self.merged().export(format, **kwargs)