capytaine 2.3.1__cp312-cp312-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.
Files changed (93) hide show
  1. capytaine/__about__.py +16 -0
  2. capytaine/__init__.py +36 -0
  3. capytaine/bem/__init__.py +0 -0
  4. capytaine/bem/airy_waves.py +111 -0
  5. capytaine/bem/engines.py +441 -0
  6. capytaine/bem/problems_and_results.py +600 -0
  7. capytaine/bem/solver.py +594 -0
  8. capytaine/bodies/__init__.py +4 -0
  9. capytaine/bodies/bodies.py +1221 -0
  10. capytaine/bodies/dofs.py +19 -0
  11. capytaine/bodies/predefined/__init__.py +6 -0
  12. capytaine/bodies/predefined/cylinders.py +151 -0
  13. capytaine/bodies/predefined/rectangles.py +111 -0
  14. capytaine/bodies/predefined/spheres.py +70 -0
  15. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  16. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  17. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  18. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  19. capytaine/green_functions/FinGreen3D/README.md +24 -0
  20. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  21. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  22. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  23. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  24. capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
  25. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  26. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  27. capytaine/green_functions/__init__.py +2 -0
  28. capytaine/green_functions/abstract_green_function.py +64 -0
  29. capytaine/green_functions/delhommeau.py +507 -0
  30. capytaine/green_functions/hams.py +204 -0
  31. capytaine/green_functions/libs/Delhommeau_float32.cpython-312-x86_64-linux-gnu.so +0 -0
  32. capytaine/green_functions/libs/Delhommeau_float64.cpython-312-x86_64-linux-gnu.so +0 -0
  33. capytaine/green_functions/libs/__init__.py +0 -0
  34. capytaine/io/__init__.py +0 -0
  35. capytaine/io/bemio.py +153 -0
  36. capytaine/io/legacy.py +328 -0
  37. capytaine/io/mesh_loaders.py +1086 -0
  38. capytaine/io/mesh_writers.py +692 -0
  39. capytaine/io/meshio.py +38 -0
  40. capytaine/io/wamit.py +479 -0
  41. capytaine/io/xarray.py +668 -0
  42. capytaine/matrices/__init__.py +16 -0
  43. capytaine/matrices/block.py +592 -0
  44. capytaine/matrices/block_toeplitz.py +325 -0
  45. capytaine/matrices/builders.py +89 -0
  46. capytaine/matrices/linear_solvers.py +232 -0
  47. capytaine/matrices/low_rank.py +395 -0
  48. capytaine/meshes/__init__.py +6 -0
  49. capytaine/meshes/clipper.py +465 -0
  50. capytaine/meshes/collections.py +342 -0
  51. capytaine/meshes/geometry.py +409 -0
  52. capytaine/meshes/mesh_like_protocol.py +37 -0
  53. capytaine/meshes/meshes.py +890 -0
  54. capytaine/meshes/predefined/__init__.py +6 -0
  55. capytaine/meshes/predefined/cylinders.py +314 -0
  56. capytaine/meshes/predefined/rectangles.py +261 -0
  57. capytaine/meshes/predefined/spheres.py +62 -0
  58. capytaine/meshes/properties.py +276 -0
  59. capytaine/meshes/quadratures.py +80 -0
  60. capytaine/meshes/quality.py +448 -0
  61. capytaine/meshes/surface_integrals.py +63 -0
  62. capytaine/meshes/symmetric.py +462 -0
  63. capytaine/post_pro/__init__.py +6 -0
  64. capytaine/post_pro/free_surfaces.py +88 -0
  65. capytaine/post_pro/impedance.py +92 -0
  66. capytaine/post_pro/kochin.py +54 -0
  67. capytaine/post_pro/rao.py +60 -0
  68. capytaine/tools/__init__.py +0 -0
  69. capytaine/tools/cache_on_disk.py +26 -0
  70. capytaine/tools/deprecation_handling.py +18 -0
  71. capytaine/tools/lists_of_points.py +52 -0
  72. capytaine/tools/lru_cache.py +49 -0
  73. capytaine/tools/optional_imports.py +27 -0
  74. capytaine/tools/prony_decomposition.py +150 -0
  75. capytaine/tools/symbolic_multiplication.py +149 -0
  76. capytaine/tools/timer.py +66 -0
  77. capytaine/ui/__init__.py +0 -0
  78. capytaine/ui/cli.py +28 -0
  79. capytaine/ui/rich.py +5 -0
  80. capytaine/ui/vtk/__init__.py +3 -0
  81. capytaine/ui/vtk/animation.py +329 -0
  82. capytaine/ui/vtk/body_viewer.py +28 -0
  83. capytaine/ui/vtk/helpers.py +82 -0
  84. capytaine/ui/vtk/mesh_viewer.py +461 -0
  85. capytaine-2.3.1.dist-info/LICENSE +674 -0
  86. capytaine-2.3.1.dist-info/METADATA +750 -0
  87. capytaine-2.3.1.dist-info/RECORD +93 -0
  88. capytaine-2.3.1.dist-info/WHEEL +6 -0
  89. capytaine-2.3.1.dist-info/entry_points.txt +3 -0
  90. capytaine.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
  91. capytaine.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
  92. capytaine.libs/libmvec-2-583a17db.28.so +0 -0
  93. 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