capytaine 2.3__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.
- 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.cp310-win_amd64.dll.a → Delhommeau_float32.cp310-win_amd64.dll.a} +0 -0
- capytaine/green_functions/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/{libs/Delhommeau_float64.cp310-win_amd64.dll.a → Delhommeau_float64.cp310-win_amd64.dll.a} +0 -0
- capytaine/green_functions/Delhommeau_float64.cp310-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.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp310-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,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)
|