capytaine 2.3.1__cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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 +16 -0
- capytaine/__init__.py +36 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +441 -0
- capytaine/bem/problems_and_results.py +600 -0
- capytaine/bem/solver.py +594 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +1221 -0
- capytaine/bodies/dofs.py +19 -0
- capytaine/bodies/predefined/__init__.py +6 -0
- capytaine/bodies/predefined/cylinders.py +151 -0
- capytaine/bodies/predefined/rectangles.py +111 -0
- capytaine/bodies/predefined/spheres.py +70 -0
- capytaine/green_functions/FinGreen3D/.gitignore +1 -0
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
- capytaine/green_functions/FinGreen3D/LICENSE +165 -0
- capytaine/green_functions/FinGreen3D/Makefile +16 -0
- capytaine/green_functions/FinGreen3D/README.md +24 -0
- capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
- capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
- capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
- capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
- capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +507 -0
- capytaine/green_functions/hams.py +204 -0
- capytaine/green_functions/libs/Delhommeau_float32.cpython-311-x86_64-linux-gnu.so +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cpython-311-x86_64-linux-gnu.so +0 -0
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +328 -0
- capytaine/io/mesh_loaders.py +1086 -0
- capytaine/io/mesh_writers.py +692 -0
- capytaine/io/meshio.py +38 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +668 -0
- capytaine/matrices/__init__.py +16 -0
- capytaine/matrices/block.py +592 -0
- capytaine/matrices/block_toeplitz.py +325 -0
- capytaine/matrices/builders.py +89 -0
- capytaine/matrices/linear_solvers.py +232 -0
- capytaine/matrices/low_rank.py +395 -0
- capytaine/meshes/__init__.py +6 -0
- capytaine/meshes/clipper.py +465 -0
- capytaine/meshes/collections.py +342 -0
- capytaine/meshes/geometry.py +409 -0
- capytaine/meshes/mesh_like_protocol.py +37 -0
- capytaine/meshes/meshes.py +890 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +314 -0
- capytaine/meshes/predefined/rectangles.py +261 -0
- capytaine/meshes/predefined/spheres.py +62 -0
- capytaine/meshes/properties.py +276 -0
- capytaine/meshes/quadratures.py +80 -0
- capytaine/meshes/quality.py +448 -0
- capytaine/meshes/surface_integrals.py +63 -0
- capytaine/meshes/symmetric.py +462 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +88 -0
- capytaine/post_pro/impedance.py +92 -0
- capytaine/post_pro/kochin.py +54 -0
- capytaine/post_pro/rao.py +60 -0
- capytaine/tools/__init__.py +0 -0
- capytaine/tools/cache_on_disk.py +26 -0
- capytaine/tools/deprecation_handling.py +18 -0
- capytaine/tools/lists_of_points.py +52 -0
- capytaine/tools/lru_cache.py +49 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +149 -0
- capytaine/tools/timer.py +66 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine/ui/vtk/__init__.py +3 -0
- capytaine/ui/vtk/animation.py +329 -0
- capytaine/ui/vtk/body_viewer.py +28 -0
- capytaine/ui/vtk/helpers.py +82 -0
- capytaine/ui/vtk/mesh_viewer.py +461 -0
- capytaine-2.3.1.dist-info/LICENSE +674 -0
- capytaine-2.3.1.dist-info/METADATA +750 -0
- capytaine-2.3.1.dist-info/RECORD +93 -0
- capytaine-2.3.1.dist-info/WHEEL +6 -0
- capytaine-2.3.1.dist-info/entry_points.txt +3 -0
- capytaine.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
- capytaine.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
- capytaine.libs/libmvec-2-583a17db.28.so +0 -0
- capytaine.libs/libquadmath-2284e583.so.0.0.0 +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Copyright (C) 2017-2022 Matthieu Ancellin
|
|
2
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
3
|
+
|
|
4
|
+
from capytaine.meshes.predefined.cylinders import mesh_disk, mesh_horizontal_cylinder, mesh_vertical_cylinder
|
|
5
|
+
from capytaine.meshes.predefined.spheres import mesh_sphere
|
|
6
|
+
from capytaine.meshes.predefined.rectangles import mesh_rectangle, mesh_parallelepiped
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Generate meshes of cylinders and disks"""
|
|
2
|
+
# Copyright (C) 2017-2022 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from itertools import product
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy import pi, cos, sin
|
|
10
|
+
|
|
11
|
+
from capytaine.meshes.geometry import xOz_Plane, yOz_Plane, Oz_axis
|
|
12
|
+
from capytaine.meshes.meshes import Mesh
|
|
13
|
+
from capytaine.meshes.collections import CollectionOfMeshes
|
|
14
|
+
from capytaine.meshes.symmetric import TranslationalSymmetricMesh, AxialSymmetricMesh, ReflectionSymmetricMesh
|
|
15
|
+
|
|
16
|
+
LOG = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def mesh_disk(*, radius=1.0, center=(0, 0, 0), normal=(0, 0, 1),
|
|
20
|
+
resolution=(3, 6), faces_max_radius=None,
|
|
21
|
+
reflection_symmetry=False, axial_symmetry=False, name=None, _theta_max=2*pi):
|
|
22
|
+
"""(One-sided) disk.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
radius : float, optional
|
|
27
|
+
radius of the disk
|
|
28
|
+
center : 3-ple or array of shape (3,), optional
|
|
29
|
+
position of the geometric center of the disk
|
|
30
|
+
normal: 3-ple of floats, optional
|
|
31
|
+
normal vector, default: along x axis
|
|
32
|
+
resolution : 2-ple of int, optional
|
|
33
|
+
number of panels along a radius and around the disk
|
|
34
|
+
faces_max_radius : float, optional
|
|
35
|
+
maximal radius of a panel. (Default: no maximal radius.)
|
|
36
|
+
If the provided resolution is too coarse, the number of panels is
|
|
37
|
+
changed to fit the constraint on the maximal radius.
|
|
38
|
+
axial_symmetry : bool, optional
|
|
39
|
+
if True, returns an AxialSymmetricMesh
|
|
40
|
+
reflection_symmetry : bool, optional
|
|
41
|
+
if True, returns a ReflectionSymmetricMesh
|
|
42
|
+
name : str, optional
|
|
43
|
+
a string naming the mesh
|
|
44
|
+
_theta_max: float, optional
|
|
45
|
+
internal parameter, to return an arc circle instead of a full circle
|
|
46
|
+
"""
|
|
47
|
+
assert radius > 0, "Radius of the disk mesh should be given as a positive value."
|
|
48
|
+
|
|
49
|
+
assert len(resolution) == 2, "Resolution of a disk should be given as a couple of values."
|
|
50
|
+
assert all([h > 0 for h in resolution]), "Resolution of the disk mesh should be given as positive values."
|
|
51
|
+
assert all([i == int(i) for i in resolution]), "Resolution of a disk should be given as integer values."
|
|
52
|
+
|
|
53
|
+
assert len(center) == 3, "Position of the center of a disk should be given a 3-ple of values."
|
|
54
|
+
|
|
55
|
+
nr, ntheta = resolution
|
|
56
|
+
if faces_max_radius is not None:
|
|
57
|
+
# The biggest cell is on the side of the disk.
|
|
58
|
+
# Assuming it is a rectangle of sides radius/nr and perimeter/ntheta
|
|
59
|
+
estimated_max_radius = np.hypot(radius/nr, 2*pi*radius/ntheta)/2
|
|
60
|
+
if estimated_max_radius > faces_max_radius:
|
|
61
|
+
nr = int(np.ceil(radius / (np.sqrt(2) * faces_max_radius)))
|
|
62
|
+
ntheta = int(np.ceil(2*pi*radius / (np.sqrt(2) * faces_max_radius)))
|
|
63
|
+
|
|
64
|
+
if name is None:
|
|
65
|
+
name = f"disk_{next(Mesh._ids)}"
|
|
66
|
+
|
|
67
|
+
if reflection_symmetry and axial_symmetry:
|
|
68
|
+
raise NotImplementedError("Disks with both symmetries have not been implemented.")
|
|
69
|
+
|
|
70
|
+
LOG.debug(f"New disk of radius {radius} and resolution {resolution}, named {name}.")
|
|
71
|
+
|
|
72
|
+
if reflection_symmetry:
|
|
73
|
+
if ntheta % 2 == 1:
|
|
74
|
+
raise ValueError("To use the reflection symmetry of the mesh, "
|
|
75
|
+
"it should have an even number of panels in this direction.")
|
|
76
|
+
|
|
77
|
+
half_mesh = mesh_disk(radius=radius, _theta_max=_theta_max/2, resolution=(nr, ntheta//2),
|
|
78
|
+
center=(0, 0, 0), normal=(0, 0, 1),
|
|
79
|
+
reflection_symmetry=False, axial_symmetry=False, name=f"half_of_{name}")
|
|
80
|
+
mesh = ReflectionSymmetricMesh(half_mesh, plane=xOz_Plane, name=name)
|
|
81
|
+
|
|
82
|
+
elif axial_symmetry:
|
|
83
|
+
mesh_slice = mesh_disk(radius=radius, _theta_max=_theta_max/ntheta, resolution=(nr, 1),
|
|
84
|
+
center=(0, 0, 0), normal=(0, 0, 1),
|
|
85
|
+
reflection_symmetry=False, axial_symmetry=False, name=f"slice_of_{name}")
|
|
86
|
+
mesh = AxialSymmetricMesh(mesh_slice, axis=Oz_axis, nb_repetitions=ntheta - 1, name=name)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
theta_range = np.linspace(0, _theta_max, ntheta+1)
|
|
90
|
+
r_range = np.linspace(0.0, radius, nr+1)
|
|
91
|
+
nodes = np.array([(0, r*sin(t), -r*cos(t)) for (r, t) in product(r_range, theta_range)])
|
|
92
|
+
panels = np.array([(j+i*(ntheta+1), j+1+i*(ntheta+1), j+1+(i+1)*(ntheta+1), j+(i+1)*(ntheta+1))
|
|
93
|
+
for (i, j) in product(range(0, nr), range(0, ntheta))])
|
|
94
|
+
|
|
95
|
+
mesh = Mesh(nodes, panels, name=name)
|
|
96
|
+
|
|
97
|
+
mesh.heal_mesh()
|
|
98
|
+
mesh.translate(center)
|
|
99
|
+
mesh.rotate_around_center_to_align_vectors(center, mesh.faces_normals[0], normal)
|
|
100
|
+
mesh.geometric_center = np.asarray(center, dtype=float)
|
|
101
|
+
return mesh
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def mesh_vertical_cylinder(*, length=10.0, radius=1.0, center=(0, 0, 0),
|
|
105
|
+
resolution=(2, 8, 10), faces_max_radius=None,
|
|
106
|
+
axial_symmetry=False, reflection_symmetry=False, name=None, _theta_max=2*pi):
|
|
107
|
+
"""Vertical cylinder.
|
|
108
|
+
|
|
109
|
+
Total number of panels = (2*resolution[0] + resolution[2])*resolution[1]
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
length : float, optional
|
|
114
|
+
length of the cylinder
|
|
115
|
+
radius : float, optional
|
|
116
|
+
radius of the cylinder
|
|
117
|
+
center : 3-ple or array of shape (3,), optional
|
|
118
|
+
position of the geometric center of the cylinder
|
|
119
|
+
resolution : 3-ple of int, optional
|
|
120
|
+
(number of panel along a radius at the end, number of panels around a slice, number of slices)
|
|
121
|
+
Mnemonic: same ordering as the cylindrical coordinates (nr, ntheta, nz)
|
|
122
|
+
faces_max_radius : float, optional
|
|
123
|
+
maximal radius of a panel. (Default: no maximal radius.)
|
|
124
|
+
If the provided resolution is too coarse, the number of panels is
|
|
125
|
+
changed to fit the constraint on the maximal radius.
|
|
126
|
+
axial_symmetry : bool, optional
|
|
127
|
+
if True, returns an AxialSymmetricMesh
|
|
128
|
+
reflection_symmetry : bool, optional
|
|
129
|
+
if True, returns a ReflectionSymmetricMesh
|
|
130
|
+
name : str, optional
|
|
131
|
+
a string naming the mesh
|
|
132
|
+
_theta_max: float, optional
|
|
133
|
+
internal parameter, to return an arc circle instead of a full circle
|
|
134
|
+
"""
|
|
135
|
+
assert length > 0, "Length of a cylinder should be given as a positive value."
|
|
136
|
+
assert radius > 0, "Radius of a cylinder should be given as a positive value."
|
|
137
|
+
|
|
138
|
+
assert len(resolution) == 3, "Resolution of a cylinder should be given as a 3-ple of values."
|
|
139
|
+
assert all([h >= 0 for h in resolution]), "Resolution of a cylinder should be given as positive values."
|
|
140
|
+
assert all([i == int(i) for i in resolution]), "Resolution of a cylinder should be given as integer values."
|
|
141
|
+
|
|
142
|
+
assert len(center) == 3, "Position of the center of a cylinder should be given a 3-ple of values."
|
|
143
|
+
|
|
144
|
+
nr, ntheta, nz = resolution
|
|
145
|
+
if faces_max_radius is not None:
|
|
146
|
+
dr, dtheta, dz = radius/nr, 2*pi*radius/ntheta, length/nz
|
|
147
|
+
estimated_max_radius = max(
|
|
148
|
+
np.hypot(dr, dtheta)/2, # Panel on disk side
|
|
149
|
+
np.hypot(dtheta, dz)/2, # Panel on cylinder itself
|
|
150
|
+
)
|
|
151
|
+
if estimated_max_radius > faces_max_radius:
|
|
152
|
+
nr = int(np.ceil(radius / (np.sqrt(2) * faces_max_radius)))
|
|
153
|
+
ntheta = 2*int(np.ceil(pi*radius / (np.sqrt(2) * faces_max_radius)))
|
|
154
|
+
nz = int(np.ceil(length / (np.sqrt(2) * faces_max_radius)))
|
|
155
|
+
|
|
156
|
+
if name is None:
|
|
157
|
+
name = f"cylinder_{next(Mesh._ids)}"
|
|
158
|
+
|
|
159
|
+
LOG.debug(f"New vertical cylinder of length {length}, radius {radius} and resolution {resolution}, named {name}.")
|
|
160
|
+
|
|
161
|
+
if reflection_symmetry and axial_symmetry:
|
|
162
|
+
raise NotImplementedError("Vertical cylinders with both symmetries have not been implemented.")
|
|
163
|
+
|
|
164
|
+
if reflection_symmetry:
|
|
165
|
+
if ntheta % 2 == 1:
|
|
166
|
+
raise ValueError("To use the reflection symmetry of the mesh, "
|
|
167
|
+
"it should have an even number of panels in this direction.")
|
|
168
|
+
|
|
169
|
+
half_cylinder = mesh_vertical_cylinder(length=length, radius=radius, center=(0, 0, 0),
|
|
170
|
+
resolution=(nr, ntheta//2, nz), reflection_symmetry=False, axial_symmetry=False,
|
|
171
|
+
name=f"half_{name}", _theta_max=_theta_max/2)
|
|
172
|
+
|
|
173
|
+
mesh = ReflectionSymmetricMesh(half_cylinder, plane=yOz_Plane, name=name)
|
|
174
|
+
|
|
175
|
+
elif axial_symmetry:
|
|
176
|
+
|
|
177
|
+
mesh_slice = mesh_vertical_cylinder(length=length, radius=radius, resolution=(nr, 1, nz), center=(0, 0, 0),
|
|
178
|
+
reflection_symmetry=False, axial_symmetry=False, name=f"slice_of_{name}", _theta_max=_theta_max/ntheta)
|
|
179
|
+
mesh = AxialSymmetricMesh(mesh_slice, axis=Oz_axis, nb_repetitions=ntheta - 1, name=name)
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
theta_range = np.linspace(0, _theta_max, ntheta+1)
|
|
183
|
+
z_range = np.linspace(-length/2, length/2, nz+1)
|
|
184
|
+
if nr > 0:
|
|
185
|
+
r_range = np.linspace(0.0, radius, nr+1)
|
|
186
|
+
nodes = np.concatenate([
|
|
187
|
+
np.array([(r*sin(t), r*cos(t), -length/2) for (r, t) in product(r_range, theta_range)]),
|
|
188
|
+
np.array([(radius*sin(t), radius*cos(t), z) for (z, t) in product(z_range, theta_range)]),
|
|
189
|
+
np.array([(r*sin(t), r*cos(t), length/2) for (r, t) in product(r_range[::-1], theta_range)]),
|
|
190
|
+
])
|
|
191
|
+
else:
|
|
192
|
+
r_range = np.array([])
|
|
193
|
+
nodes = np.array([(radius*sin(t), radius*cos(t), z) for (z, t) in product(z_range, theta_range)])
|
|
194
|
+
panels = np.array([(j+i*(ntheta+1), j+(i+1)*(ntheta+1), j+1+(i+1)*(ntheta+1), j+1+i*(ntheta+1), )
|
|
195
|
+
for (i, j) in product(range(nz+2*(len(r_range))), range(ntheta))])
|
|
196
|
+
|
|
197
|
+
mesh = Mesh(nodes, panels, name=name)
|
|
198
|
+
|
|
199
|
+
mesh.heal_mesh()
|
|
200
|
+
mesh.translate(center)
|
|
201
|
+
mesh.geometric_center = np.asarray(center, dtype=float)
|
|
202
|
+
return mesh
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def mesh_horizontal_cylinder(*, length=10.0, radius=1.0, center=(0, 0, 0),
|
|
206
|
+
resolution=(2, 8, 10), faces_max_radius=None,
|
|
207
|
+
reflection_symmetry=False, translation_symmetry=False, name=None, _theta_max=2*pi):
|
|
208
|
+
"""Cylinder aligned along Ox axis.
|
|
209
|
+
|
|
210
|
+
Total number of panels = (2*resolution[0] + resolution[2])*resolution[1]
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
length : float, optional
|
|
215
|
+
length of the cylinder
|
|
216
|
+
radius : float, optional
|
|
217
|
+
radius of the cylinder
|
|
218
|
+
center : 3-ple or array of shape (3,), optional
|
|
219
|
+
position of the geometric center of the cylinder
|
|
220
|
+
resolution : 3-ple of int, optional
|
|
221
|
+
(number of panel along a radius at the end, number of panels around a slice, number of slices)
|
|
222
|
+
Mnemonic: same ordering as the cylindrical coordinates (nr, ntheta, nz)
|
|
223
|
+
faces_max_radius : float, optional
|
|
224
|
+
maximal radius of a panel. (Default: no maximal radius.)
|
|
225
|
+
If the provided resolution is too coarse, the number of panels is
|
|
226
|
+
changed to fit the constraint on the maximal radius.
|
|
227
|
+
reflection_symmetry : bool, optional
|
|
228
|
+
if True, returns a ReflectionSymmetricMesh
|
|
229
|
+
translation_symmetry : bool, optional
|
|
230
|
+
if True, uses a TranslationalSymmetricMesh internally for the main part of the cylinder
|
|
231
|
+
name : str, optional
|
|
232
|
+
a string naming the mesh
|
|
233
|
+
_theta_max: float, optional
|
|
234
|
+
internal parameter, to return an arc circle instead of a full circle
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
assert length > 0, "Length of a cylinder should be given as a positive value."
|
|
238
|
+
assert radius > 0, "Radius of a cylinder should be given as a positive value."
|
|
239
|
+
|
|
240
|
+
assert len(resolution) == 3, "Resolution of a cylinder should be given as a 3-ple of values."
|
|
241
|
+
assert all([h >= 0 for h in resolution]), "Resolution of a cylinder should be given as positive values."
|
|
242
|
+
assert all([i == int(i) for i in resolution]), "Resolution of a cylinder should be given as integer values."
|
|
243
|
+
|
|
244
|
+
assert len(center) == 3, "Position of the center of a cylinder should be given a 3-ple of values."
|
|
245
|
+
|
|
246
|
+
if name is None:
|
|
247
|
+
name = f"cylinder_{next(Mesh._ids)}"
|
|
248
|
+
|
|
249
|
+
LOG.debug(f"New horizontal cylinder of length {length}, radius {radius} and resolution {resolution}, named {name}.")
|
|
250
|
+
|
|
251
|
+
nr, ntheta, nx = resolution
|
|
252
|
+
if faces_max_radius is not None:
|
|
253
|
+
dr, dtheta, dx = radius/nr, 2*pi*radius/ntheta, length/nx
|
|
254
|
+
estimated_max_radius = max(
|
|
255
|
+
np.hypot(dr, dtheta)/2, # Panel on disk side
|
|
256
|
+
np.hypot(dtheta, dx)/2, # Panel on cylinder itself
|
|
257
|
+
)
|
|
258
|
+
if estimated_max_radius > faces_max_radius:
|
|
259
|
+
nr = int(np.ceil(radius / (np.sqrt(2) * faces_max_radius)))
|
|
260
|
+
ntheta = 2*int(np.ceil(pi*radius / (np.sqrt(2) * faces_max_radius)))
|
|
261
|
+
nx = int(np.ceil(length / (np.sqrt(2) * faces_max_radius)))
|
|
262
|
+
|
|
263
|
+
if reflection_symmetry:
|
|
264
|
+
if ntheta % 2 == 1:
|
|
265
|
+
raise ValueError("To use the reflection symmetry of the mesh, "
|
|
266
|
+
"it should have an even number of panels in this direction.")
|
|
267
|
+
|
|
268
|
+
half_cylinder = mesh_horizontal_cylinder(
|
|
269
|
+
length=length, radius=radius, center=(0, 0, 0),
|
|
270
|
+
resolution=(nr, ntheta//2, nx),
|
|
271
|
+
reflection_symmetry=False, translation_symmetry=translation_symmetry,
|
|
272
|
+
name=f"half_{name}", _theta_max=_theta_max/2,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
mesh = ReflectionSymmetricMesh(half_cylinder, plane=xOz_Plane, name=name)
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
if translation_symmetry:
|
|
279
|
+
slice = mesh_horizontal_cylinder(
|
|
280
|
+
length=length/nx, radius=radius, center=(-length/2 + length/(2*nx), 0, 0),
|
|
281
|
+
resolution=(0, ntheta, 1),
|
|
282
|
+
reflection_symmetry=False, translation_symmetry=False,
|
|
283
|
+
name=f"slice_of_{name}", _theta_max=_theta_max,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
open_cylinder = TranslationalSymmetricMesh(
|
|
287
|
+
slice, translation=np.asarray([length / nx, 0.0, 0.0]),
|
|
288
|
+
nb_repetitions=nx-1, name=f"open_{name}")
|
|
289
|
+
|
|
290
|
+
else: # General case
|
|
291
|
+
theta_range = np.linspace(0, _theta_max, ntheta+1)
|
|
292
|
+
x_range = np.linspace(-length/2, length/2, nx+1)
|
|
293
|
+
nodes = np.array([(x, radius*sin(t), -radius*cos(t)) for (x, t) in product(x_range, theta_range)])
|
|
294
|
+
|
|
295
|
+
panels = np.array([(i+j*(ntheta+1), i+1+j*(ntheta+1), i+1+(j+1)*(ntheta+1), i+(j+1)*(ntheta+1))
|
|
296
|
+
for (i, j) in product(range(ntheta), range(nx))])
|
|
297
|
+
|
|
298
|
+
open_cylinder = Mesh(nodes, panels, name=f"open_{name}")
|
|
299
|
+
|
|
300
|
+
if nr > 0:
|
|
301
|
+
side = mesh_disk(radius=radius, center=(-length/2, 0, 0), normal=(-1, 0, 0),
|
|
302
|
+
reflection_symmetry=False, resolution=(nr, ntheta), name=f"side_of_{name}",
|
|
303
|
+
_theta_max=_theta_max)
|
|
304
|
+
other_side = side.mirrored(yOz_Plane, name=f"other_side_of_{name}")
|
|
305
|
+
mesh = CollectionOfMeshes([open_cylinder, side, other_side], name=name)
|
|
306
|
+
if not translation_symmetry:
|
|
307
|
+
mesh = mesh.merged()
|
|
308
|
+
else:
|
|
309
|
+
mesh = open_cylinder.copy(name=name)
|
|
310
|
+
|
|
311
|
+
mesh.heal_mesh()
|
|
312
|
+
mesh.translate(center)
|
|
313
|
+
mesh.geometric_center = np.asarray(center, dtype=float)
|
|
314
|
+
return mesh
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Generate rectangular bodies."""
|
|
2
|
+
# Copyright (C) 2017-2024 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
|
+
import logging
|
|
5
|
+
from itertools import product
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from capytaine.meshes.geometry import xOz_Plane, yOz_Plane
|
|
10
|
+
from capytaine.meshes.meshes import Mesh
|
|
11
|
+
from capytaine.meshes.symmetric import TranslationalSymmetricMesh, ReflectionSymmetricMesh
|
|
12
|
+
from capytaine.meshes.collections import CollectionOfMeshes
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
def mesh_rectangle(*, size=(5.0, 5.0), center=(0.0, 0.0, 0.0),
|
|
17
|
+
resolution=(5, 5), faces_max_radius=None,
|
|
18
|
+
normal=(0.0, 0.0, 1.0),
|
|
19
|
+
translation_symmetry=False, reflection_symmetry=False,
|
|
20
|
+
name=None):
|
|
21
|
+
"""One-sided rectangle.
|
|
22
|
+
|
|
23
|
+
By default, the rectangle is horizontal, the normals are oriented upwards.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
size : couple of floats, optional
|
|
28
|
+
dimensions of the rectangle (width and height)
|
|
29
|
+
center : 3-ple of floats, optional
|
|
30
|
+
position of the geometric center of the rectangle, default: (0, 0, 0)
|
|
31
|
+
resolution : couple of ints, optional
|
|
32
|
+
number of faces along each of the two directions
|
|
33
|
+
faces_max_radius : float, optional
|
|
34
|
+
maximal radius of a panel. (Default: no maximal radius.)
|
|
35
|
+
If the provided resolution is too coarse, the number of panels is
|
|
36
|
+
changed to fit the constraint on the maximal radius.
|
|
37
|
+
normal: 3-ple of floats, optional
|
|
38
|
+
normal vector, default: (0, 0, 1)
|
|
39
|
+
translation_symmetry : bool, optional
|
|
40
|
+
if True, use the translation symmetry to speed up the computations
|
|
41
|
+
reflection_symmetry : bool, optional
|
|
42
|
+
if True, use the reflection symmetry to speed up the computations
|
|
43
|
+
name : string, optional
|
|
44
|
+
a name for the body
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
assert len(size) == 2, "Size of a rectangle should be given as a couple of values."
|
|
48
|
+
assert all([h > 0 for h in size]), "Size of the rectangle mesh should be given as positive values."
|
|
49
|
+
|
|
50
|
+
assert len(resolution) == 2, "Resolution of a rectangle should be given as a couple a values."
|
|
51
|
+
assert all([h > 0 for h in resolution]), "Resolution of the rectangle mesh should be given as positive values."
|
|
52
|
+
assert all([i == int(i) for i in resolution]), "Resolution of a rectangle should be given as integer values."
|
|
53
|
+
|
|
54
|
+
assert len(center) == 3, "Position of the center of a rectangle should be given a 3-ple of values."
|
|
55
|
+
|
|
56
|
+
width, height = size
|
|
57
|
+
nw, nh = resolution
|
|
58
|
+
|
|
59
|
+
if faces_max_radius is not None:
|
|
60
|
+
estimated_max_radius = np.hypot(width/nw, height/nh)/2
|
|
61
|
+
if estimated_max_radius > faces_max_radius:
|
|
62
|
+
nw = int(np.ceil(width / (np.sqrt(2) * faces_max_radius)))
|
|
63
|
+
nh = int(np.ceil(height / (np.sqrt(2) * faces_max_radius)))
|
|
64
|
+
|
|
65
|
+
if name is None:
|
|
66
|
+
name = f"rectangle_{next(Mesh._ids)}"
|
|
67
|
+
|
|
68
|
+
if translation_symmetry and reflection_symmetry:
|
|
69
|
+
raise NotImplementedError("Rectangle generation with both reflection and translation symmetries "
|
|
70
|
+
"has not been implemented.")
|
|
71
|
+
|
|
72
|
+
if reflection_symmetry:
|
|
73
|
+
if nw % 2 == 1:
|
|
74
|
+
raise ValueError("To use the reflection symmetry of the mesh, "
|
|
75
|
+
"it should have an even number of panels in this direction.")
|
|
76
|
+
|
|
77
|
+
half_mesh = mesh_rectangle(size=(width/2, height), resolution=(nw//2, nh),
|
|
78
|
+
center=(0, -width/4, 0), normal=(0.0, 0.0, 1.0),
|
|
79
|
+
translation_symmetry=False, reflection_symmetry=False,
|
|
80
|
+
name=f"half_of_{name}")
|
|
81
|
+
mesh = ReflectionSymmetricMesh(half_mesh, plane=xOz_Plane, name=name)
|
|
82
|
+
|
|
83
|
+
elif translation_symmetry:
|
|
84
|
+
strip = mesh_rectangle(size=(width/nw, height), resolution=(1, nh),
|
|
85
|
+
center=(0, -width/2 + width/(2*nw), 0), normal=(0.0, 0.0, 1.0),
|
|
86
|
+
translation_symmetry=False, reflection_symmetry=False,
|
|
87
|
+
name=f"strip_of_{name}")
|
|
88
|
+
mesh = TranslationalSymmetricMesh(strip,
|
|
89
|
+
translation=np.asarray([0, width/nw, 0]),
|
|
90
|
+
nb_repetitions=int(nw)-1,
|
|
91
|
+
name=name)
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
y_range = np.linspace(-width/2, width/2, nw+1)
|
|
95
|
+
z_range = np.linspace(-height/2, height/2, nh+1)
|
|
96
|
+
nodes = np.array(list(product([0.0], y_range, z_range)), dtype=float)
|
|
97
|
+
panels = np.array([(j+i*(nh+1), j+1+i*(nh+1), j+1+(i+1)*(nh+1), j+(i+1)*(nh+1))
|
|
98
|
+
for (i, j) in product(range(nw), range(nh))])
|
|
99
|
+
|
|
100
|
+
mesh = Mesh(nodes, panels, name=name)
|
|
101
|
+
|
|
102
|
+
mesh.heal_mesh()
|
|
103
|
+
mesh.translate(center)
|
|
104
|
+
mesh.rotate_around_center_to_align_vectors(center, mesh.faces_normals[0], normal)
|
|
105
|
+
mesh.geometric_center = np.asarray(center, dtype=float)
|
|
106
|
+
return mesh
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def mesh_parallelepiped(size=(1.0, 1.0, 1.0), center=(0, 0, 0),
|
|
110
|
+
resolution=(4, 4, 4), faces_max_radius=None,
|
|
111
|
+
missing_sides=set(), reflection_symmetry=False, translation_symmetry=False,
|
|
112
|
+
name=None):
|
|
113
|
+
"""Six rectangles forming a parallelepiped.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
size : 3-ple of floats, optional
|
|
118
|
+
dimensions of the parallelepiped (width, thickness, height) for coordinates (x, y, z).
|
|
119
|
+
center : 3-ple of floats, optional
|
|
120
|
+
coordinates of the geometric center of the parallelepiped
|
|
121
|
+
resolution : 3-ple of ints, optional
|
|
122
|
+
number of faces along the three directions
|
|
123
|
+
faces_max_radius : float, optional
|
|
124
|
+
maximal radius of a panel. (Default: no maximal radius.)
|
|
125
|
+
If the provided resolution is too coarse, the number of panels is
|
|
126
|
+
changed to fit the constraint on the maximal radius.
|
|
127
|
+
missing_sides : set of string, optional
|
|
128
|
+
if one of the keyword "top", "bottom", "front", "back", "left", "right" is in the set,
|
|
129
|
+
then the corresponding side is not included in the parallelepiped.
|
|
130
|
+
May be ignored when building a mesh with a symmetry.
|
|
131
|
+
reflection_symmetry : bool, optional
|
|
132
|
+
use xOz and yOz symmetry plane to generate the mesh
|
|
133
|
+
translation_symmetry : bool, optional
|
|
134
|
+
if True, use the translation symmetry in the x direction to speed up the computations.
|
|
135
|
+
To use the translation symmetry in the y direction, create a x-symmetric body and then rotate it by pi/2.
|
|
136
|
+
name : string, optional
|
|
137
|
+
a name for the body
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
assert len(size) == 3, "Size of a rectangular parallelepiped should be given as a 3-ple of values."
|
|
141
|
+
assert all([h > 0 for h in size]), "Size of the rectangular mesh should be given as positive values."
|
|
142
|
+
|
|
143
|
+
assert len(resolution) == 3, "Resolution of a rectangular parallelepiped should be given as a 3-ple a values."
|
|
144
|
+
assert all([h > 0 for h in resolution]), "Resolution of the rectangular parallelepiped mesh " \
|
|
145
|
+
"should be given as positive values."
|
|
146
|
+
assert all([i == int(i) for i in resolution]), "Resolution of a rectangular parallelepiped " \
|
|
147
|
+
"should be given as integer values."
|
|
148
|
+
|
|
149
|
+
assert len(center) == 3, "Position of the center of a parallelepiped should be given a 3-ple of values."
|
|
150
|
+
|
|
151
|
+
width, thickness, height = size
|
|
152
|
+
nw, nth, nh = resolution
|
|
153
|
+
|
|
154
|
+
if faces_max_radius is not None:
|
|
155
|
+
dw, dh, dth = width/nw, height/nh, thickness/nth
|
|
156
|
+
estimated_max_radius = max(
|
|
157
|
+
np.hypot(dw, dh)/2,
|
|
158
|
+
np.hypot(dw, dth)/2,
|
|
159
|
+
np.hypot(dth, dh)/2,
|
|
160
|
+
)
|
|
161
|
+
if estimated_max_radius > faces_max_radius:
|
|
162
|
+
nw = int(np.ceil(width / (np.sqrt(2) * faces_max_radius)))
|
|
163
|
+
nth = int(np.ceil(thickness / (np.sqrt(2) * faces_max_radius)))
|
|
164
|
+
nh = int(np.ceil(height / (np.sqrt(2) * faces_max_radius)))
|
|
165
|
+
|
|
166
|
+
if name is None:
|
|
167
|
+
name = f"rectangular_parallelepiped_{next(Mesh._ids)}"
|
|
168
|
+
|
|
169
|
+
if translation_symmetry and reflection_symmetry:
|
|
170
|
+
raise NotImplementedError("Parallelepiped generation with both reflection and translation symmetries "
|
|
171
|
+
"has not been implemented.")
|
|
172
|
+
|
|
173
|
+
if reflection_symmetry:
|
|
174
|
+
if (nw % 2 == 1 or nth % 2 == 1):
|
|
175
|
+
raise ValueError("To use the reflection symmetry of the mesh, "
|
|
176
|
+
"it should have an even number of panels in this direction.")
|
|
177
|
+
|
|
178
|
+
missing_sides_in_quarter = missing_sides | {"right", "back"}
|
|
179
|
+
quarter_mesh = mesh_parallelepiped(
|
|
180
|
+
size=(width/2, thickness/2, height), resolution=(nw//2, nth//2, nh),
|
|
181
|
+
center=(-width/4, -thickness/4, 0), missing_sides=missing_sides_in_quarter,
|
|
182
|
+
reflection_symmetry=False, translation_symmetry=False,
|
|
183
|
+
name=f"quarter_of_{name}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
half_mesh = ReflectionSymmetricMesh(quarter_mesh, plane=yOz_Plane, name=f"half_of_{name}")
|
|
187
|
+
mesh = ReflectionSymmetricMesh(half_mesh, plane=xOz_Plane, name=f"{name}")
|
|
188
|
+
|
|
189
|
+
elif translation_symmetry:
|
|
190
|
+
|
|
191
|
+
missing_sides_in_strip = missing_sides | {"left", "right"}
|
|
192
|
+
strip = mesh_parallelepiped(
|
|
193
|
+
size=(width/nw, thickness, height), resolution=(1, nth, nh),
|
|
194
|
+
center=(-width/2 + width/(2*nw), 0, 0), missing_sides=missing_sides_in_strip,
|
|
195
|
+
reflection_symmetry=False, translation_symmetry=False,
|
|
196
|
+
name=f"strip_of_{name}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
open_parallelepiped = TranslationalSymmetricMesh(
|
|
200
|
+
strip,
|
|
201
|
+
translation=(width/nw, 0, 0), nb_repetitions=int(nw)-1,
|
|
202
|
+
name=f"body_of_{name}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
components_of_mesh = [open_parallelepiped]
|
|
206
|
+
if "right" not in missing_sides:
|
|
207
|
+
components_of_mesh.append(
|
|
208
|
+
mesh_rectangle(
|
|
209
|
+
size=(thickness, height), resolution=(nth, nh),
|
|
210
|
+
center=(width/2, 0, 0), normal=(1, 0, 0),
|
|
211
|
+
name=f"right_side_of_{name}"
|
|
212
|
+
))
|
|
213
|
+
if "left" not in missing_sides:
|
|
214
|
+
components_of_mesh.append(
|
|
215
|
+
mesh_rectangle(
|
|
216
|
+
size=(thickness, height), resolution=(nth, nh),
|
|
217
|
+
center=(-width/2, 0, 0), normal=(-1, 0, 0),
|
|
218
|
+
name=f"left_side_of_{name}"
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
mesh = CollectionOfMeshes(components_of_mesh, name=name)
|
|
222
|
+
|
|
223
|
+
else:
|
|
224
|
+
|
|
225
|
+
sides = []
|
|
226
|
+
if "left" not in missing_sides:
|
|
227
|
+
sides.append(
|
|
228
|
+
mesh_rectangle(size=(thickness, height), resolution=(nth, nh), center=(-width/2, 0, 0),
|
|
229
|
+
normal=(-1, 0, 0), name=f"left_of_{name}")
|
|
230
|
+
)
|
|
231
|
+
if "right" not in missing_sides:
|
|
232
|
+
sides.append(
|
|
233
|
+
mesh_rectangle(size=(thickness, height), resolution=(nth, nh), center=(width/2, 0, 0),
|
|
234
|
+
normal=(1, 0, 0), name=f"right_of_{name}")
|
|
235
|
+
)
|
|
236
|
+
if "front" not in missing_sides:
|
|
237
|
+
sides.append(
|
|
238
|
+
mesh_rectangle(size=(width, height), resolution=(nw, nh), center=(0, -thickness/2, 0),
|
|
239
|
+
normal=(0, -1, 0), name=f"front_of_{name}")
|
|
240
|
+
)
|
|
241
|
+
if "back" not in missing_sides:
|
|
242
|
+
sides.append(
|
|
243
|
+
mesh_rectangle(size=(width, height), resolution=(nw, nh), center=(0, thickness/2, 0),
|
|
244
|
+
normal=(0, 1, 0), name=f"back_of_{name}")
|
|
245
|
+
)
|
|
246
|
+
if "top" not in missing_sides:
|
|
247
|
+
sides.append(
|
|
248
|
+
mesh_rectangle(size=(thickness, width), resolution=(nth, nw), center=(0, 0, height/2),
|
|
249
|
+
normal=(0, 0, 1), name=f"top_of_{name}")
|
|
250
|
+
)
|
|
251
|
+
if "bottom" not in missing_sides:
|
|
252
|
+
sides.append(
|
|
253
|
+
mesh_rectangle(size=(thickness, width), resolution=(nth, nw), center=(0, 0, -height/2),
|
|
254
|
+
normal=(0, 0, -1), name=f"bottom_of_{name}")
|
|
255
|
+
)
|
|
256
|
+
mesh = CollectionOfMeshes(sides, name=name).merged()
|
|
257
|
+
|
|
258
|
+
mesh.heal_mesh()
|
|
259
|
+
mesh.translate(center)
|
|
260
|
+
mesh.geometric_center = np.asarray(center, dtype=float)
|
|
261
|
+
return mesh
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Generate spherical bodies."""
|
|
2
|
+
# Copyright (C) 2017-2024 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy import pi
|
|
8
|
+
|
|
9
|
+
from capytaine.meshes.geometry import Axis
|
|
10
|
+
from capytaine.meshes.meshes import Mesh
|
|
11
|
+
from capytaine.meshes.symmetric import AxialSymmetricMesh
|
|
12
|
+
|
|
13
|
+
LOG = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def mesh_sphere(*, radius=1.0, center=(0.0, 0.0, 0.0),
|
|
17
|
+
resolution=(10, 10), faces_max_radius=None,
|
|
18
|
+
axial_symmetry=False, name=None):
|
|
19
|
+
"""Sphere
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
radius : float
|
|
24
|
+
radius of the sphere
|
|
25
|
+
center : 3-ple or array of shape (3,)
|
|
26
|
+
position of the geometric center of the sphere
|
|
27
|
+
resolution : couple of ints
|
|
28
|
+
number of panels along a meridian (or number of parallels-1) and
|
|
29
|
+
along a parallel (or number of meridians-1)
|
|
30
|
+
faces_max_radius : float, optional
|
|
31
|
+
maximal radius of a panel. (Default: no maximal radius.)
|
|
32
|
+
If the provided resolution is too coarse, the number of panels is
|
|
33
|
+
changed to fit the constraint on the maximal radius.
|
|
34
|
+
axial_symmetry : bool
|
|
35
|
+
if True, use the axial symmetry to build the mesh (default: False)
|
|
36
|
+
name : string
|
|
37
|
+
a name identifying the sphere (default: "sphere_id" where id is an unique integer).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
if name is None:
|
|
41
|
+
name = f"sphere_{next(Mesh._ids)}"
|
|
42
|
+
|
|
43
|
+
ntheta, nphi = resolution
|
|
44
|
+
if faces_max_radius is not None:
|
|
45
|
+
perimeter = 2*np.pi*radius
|
|
46
|
+
estimated_max_radius = np.hypot(perimeter/ntheta, perimeter/nphi)/2
|
|
47
|
+
if estimated_max_radius > faces_max_radius:
|
|
48
|
+
ntheta = nphi = int(np.ceil(perimeter / (np.sqrt(2)*faces_max_radius)))
|
|
49
|
+
|
|
50
|
+
theta = np.linspace(0.0, pi, ntheta+1)
|
|
51
|
+
points_on_a_meridian = radius * np.stack([np.sin(theta), np.zeros_like(theta), -np.cos(theta)], axis=1)
|
|
52
|
+
|
|
53
|
+
symmetry_axis = Axis(vector=[0, 0, 1], point=[0, 0, 0])
|
|
54
|
+
mesh = AxialSymmetricMesh.from_profile(points_on_a_meridian, axis=symmetry_axis, nphi=nphi, name=name)
|
|
55
|
+
|
|
56
|
+
if not axial_symmetry:
|
|
57
|
+
mesh = mesh.merged()
|
|
58
|
+
|
|
59
|
+
mesh.heal_mesh()
|
|
60
|
+
mesh.translate(center)
|
|
61
|
+
mesh.geometric_center = np.asarray(center, dtype=float)
|
|
62
|
+
return mesh
|