capytaine 2.3.1__cp314-cp314-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 +16 -0
- capytaine/__init__.py +48 -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.cp314-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/Delhommeau_float32.cp314-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp314-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp314-win_amd64.pyd +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/DELVEWHEEL +2 -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 +97 -0
- capytaine-2.3.1.dist-info/WHEEL +4 -0
- capytaine-2.3.1.dist-info/entry_points.txt +3 -0
- capytaine.libs/libgcc_s_seh-1.dll +0 -0
- capytaine.libs/libgfortran-5.dll +0 -0
- capytaine.libs/libgomp-1.dll +0 -0
- capytaine.libs/libquadmath-0.dll +0 -0
- capytaine.libs/libwinpthread-1.dll +0 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""Special meshes with symmetries, useful to speed up the computations."""
|
|
2
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/mancellin/capytaine>
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import reprlib
|
|
7
|
+
from typing import Union, Callable, Iterable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from capytaine.meshes.meshes import Mesh
|
|
12
|
+
from capytaine.meshes.collections import CollectionOfMeshes
|
|
13
|
+
from capytaine.meshes.geometry import Axis, Plane, xOy_Plane, Oz_axis, inplace_transformation
|
|
14
|
+
|
|
15
|
+
LOG = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SymmetricMesh(CollectionOfMeshes):
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
reprer = reprlib.Repr()
|
|
21
|
+
reprer.maxstring = 90
|
|
22
|
+
reprer.maxother = 90
|
|
23
|
+
slice_name = reprer.repr(self._meshes[0])
|
|
24
|
+
if self.name is not None:
|
|
25
|
+
return f"{self.__class__.__name__}({slice_name}, name={self.name})"
|
|
26
|
+
else:
|
|
27
|
+
return f"{self.__class__.__name__}({slice_name})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReflectionSymmetricMesh(SymmetricMesh):
|
|
31
|
+
"""A mesh with one vertical symmetry plane.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
half : Mesh or CollectionOfMeshes
|
|
36
|
+
a mesh describing half of the body
|
|
37
|
+
plane : Plane
|
|
38
|
+
the symmetry plane across which the half body is mirrored
|
|
39
|
+
name :str, optional
|
|
40
|
+
a name for the mesh
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, half: Union[Mesh, CollectionOfMeshes], plane: Plane, name=None):
|
|
44
|
+
assert isinstance(half, Mesh) or isinstance(half, CollectionOfMeshes)
|
|
45
|
+
assert isinstance(plane, Plane)
|
|
46
|
+
assert plane.normal[2] == 0, "Only vertical reflection planes are supported in ReflectionSymmetry classes."
|
|
47
|
+
|
|
48
|
+
other_half = half.mirrored(plane, name=f"mirrored_of_{half.name}")
|
|
49
|
+
|
|
50
|
+
if name is None:
|
|
51
|
+
name = f"reflection_of_{half.name}"
|
|
52
|
+
|
|
53
|
+
self.plane = plane.copy()
|
|
54
|
+
|
|
55
|
+
super().__init__((half, other_half), name=name)
|
|
56
|
+
|
|
57
|
+
if self.name is not None:
|
|
58
|
+
LOG.debug(f"New mirror symmetric mesh: {self.name}.")
|
|
59
|
+
else:
|
|
60
|
+
LOG.debug(f"New mirror symmetric mesh.")
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return f"{self.__class__.__name__}({self.half.__short_str__()}, plane={self.plane}, name=\"{self.name}\")"
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return f"{self.__class__.__name__}({self.half}, plane={self.plane}, name=\"{self.name}\")"
|
|
67
|
+
|
|
68
|
+
def __rich_repr__(self):
|
|
69
|
+
yield self.half
|
|
70
|
+
yield "plane", self.plane
|
|
71
|
+
yield "name", self.name
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def half(self):
|
|
75
|
+
return self[0]
|
|
76
|
+
|
|
77
|
+
def tree_view(self, fold_symmetry=True, **kwargs):
|
|
78
|
+
if fold_symmetry:
|
|
79
|
+
return (self.__short_str__() + '\n' + ' ├─' + self.half.tree_view().replace('\n', '\n │ ') + '\n'
|
|
80
|
+
+ f" └─mirrored copy of the above {self.half.__short_str__()}")
|
|
81
|
+
else:
|
|
82
|
+
return CollectionOfMeshes.tree_view(self, **kwargs)
|
|
83
|
+
|
|
84
|
+
def __deepcopy__(self, *args):
|
|
85
|
+
return ReflectionSymmetricMesh(self.half.copy(), self.plane, name=self.name)
|
|
86
|
+
|
|
87
|
+
def join_meshes(*meshes, name=None, return_masks=False):
|
|
88
|
+
assert all(isinstance(mesh, ReflectionSymmetricMesh) for mesh in meshes), \
|
|
89
|
+
"Only meshes with the same symmetry can be joined together."
|
|
90
|
+
assert all(meshes[0].plane == mesh.plane for mesh in meshes), \
|
|
91
|
+
"Only reflection symmetric meshes with the same reflection plane can be joined together."
|
|
92
|
+
if not return_masks:
|
|
93
|
+
name = name=f"half_of_{name}" if name is not None else None
|
|
94
|
+
half_mesh = meshes[0].half.join_meshes(
|
|
95
|
+
*(mesh.half for mesh in meshes[1:]),
|
|
96
|
+
name=name, return_masks=False
|
|
97
|
+
)
|
|
98
|
+
return ReflectionSymmetricMesh(half_mesh, plane=meshes[0].plane, name=name)
|
|
99
|
+
else:
|
|
100
|
+
name = name=f"half_of_{name}" if name is not None else None
|
|
101
|
+
half_mesh, half_masks = meshes[0].half.join_meshes(
|
|
102
|
+
*(mesh.half for mesh in meshes[1:]),
|
|
103
|
+
name=name, return_masks=True
|
|
104
|
+
)
|
|
105
|
+
masks = [np.concatenate([half_mask, half_mask]) for half_mask in half_masks]
|
|
106
|
+
joined = ReflectionSymmetricMesh(half_mesh, plane=meshes[0].plane, name=name)
|
|
107
|
+
return joined, masks
|
|
108
|
+
|
|
109
|
+
@inplace_transformation
|
|
110
|
+
def translate(self, vector):
|
|
111
|
+
self.plane.translate(vector)
|
|
112
|
+
CollectionOfMeshes.translate(self, vector)
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
@inplace_transformation
|
|
116
|
+
def rotate(self, axis: Axis, angle: float):
|
|
117
|
+
self.plane.rotate(axis, angle)
|
|
118
|
+
CollectionOfMeshes.rotate(self, axis, angle)
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
@inplace_transformation
|
|
122
|
+
def mirror(self, plane: Plane):
|
|
123
|
+
self.plane.mirror(plane)
|
|
124
|
+
CollectionOfMeshes.mirror(self, plane)
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def generate_lid(self, z=0.0, faces_max_radius=None, name=None):
|
|
128
|
+
if name is None:
|
|
129
|
+
name = "lid for {}".format(self.name)
|
|
130
|
+
return ReflectionSymmetricMesh(self.half.generate_lid(z, faces_max_radius), self.plane, name=name)
|
|
131
|
+
|
|
132
|
+
def extract_lid(self, plane=xOy_Plane):
|
|
133
|
+
hull, lid = self.half.extract_lid(plane)
|
|
134
|
+
return ReflectionSymmetricMesh(hull, self.plane), ReflectionSymmetricMesh(lid, self.plane)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TranslationalSymmetricMesh(SymmetricMesh):
|
|
138
|
+
"""A mesh with a repeating pattern by translation.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
mesh_slice : Mesh or CollectionOfMeshes
|
|
143
|
+
the pattern that will be repeated to form the whole body
|
|
144
|
+
translation : array(3)
|
|
145
|
+
the vector of the translation
|
|
146
|
+
nb_repetitions : int, optional
|
|
147
|
+
the number of repetitions of the pattern (excluding the original one, default: 1)
|
|
148
|
+
name : str, optional
|
|
149
|
+
a name for the mesh
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, mesh_slice: Union[Mesh, CollectionOfMeshes], translation, nb_repetitions=1, name=None):
|
|
153
|
+
assert isinstance(mesh_slice, Mesh) or isinstance(mesh_slice, CollectionOfMeshes)
|
|
154
|
+
assert isinstance(nb_repetitions, int)
|
|
155
|
+
assert nb_repetitions >= 1
|
|
156
|
+
|
|
157
|
+
translation = np.asarray(translation).copy()
|
|
158
|
+
assert translation.shape == (3,)
|
|
159
|
+
assert translation[2] == 0 # Only horizontal translation are supported.
|
|
160
|
+
|
|
161
|
+
slices = [mesh_slice]
|
|
162
|
+
for i in range(1, nb_repetitions+1):
|
|
163
|
+
slices.append(mesh_slice.translated(vector=i*translation, name=f"repetition_{i}_of_{mesh_slice.name}"))
|
|
164
|
+
|
|
165
|
+
if name is None:
|
|
166
|
+
name = f"translation_of_{mesh_slice.name}"
|
|
167
|
+
|
|
168
|
+
self.translation = translation
|
|
169
|
+
|
|
170
|
+
super().__init__(slices, name=name)
|
|
171
|
+
|
|
172
|
+
if self.name is not None:
|
|
173
|
+
LOG.debug(f"New translation symmetric mesh: {self.name}.")
|
|
174
|
+
else:
|
|
175
|
+
LOG.debug(f"New translation symmetric mesh.")
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def first_slice(self):
|
|
179
|
+
return self[0]
|
|
180
|
+
|
|
181
|
+
def __str__(self):
|
|
182
|
+
return f"{self.__class__.__name__}({self.first_slice.__short_str__()}, translation={self.translation}, nb_repetitions={len(self)-1}, name=\"{self.name}\")"
|
|
183
|
+
|
|
184
|
+
def __repr__(self):
|
|
185
|
+
return f"{self.__class__.__name__}({self.first_slice}, translation={self.translation}, nb_repetitions={len(self)-1}, name=\"{self.name}\")"
|
|
186
|
+
|
|
187
|
+
def __rich_repr__(self):
|
|
188
|
+
yield self.first_slice
|
|
189
|
+
yield "translation", self.translation
|
|
190
|
+
yield "nb_repetitions", len(self)-1
|
|
191
|
+
yield "name", self.name
|
|
192
|
+
|
|
193
|
+
def tree_view(self, fold_symmetry=True, **kwargs):
|
|
194
|
+
if fold_symmetry:
|
|
195
|
+
return (self.__short_str__() + '\n' + ' ├─' + self.first_slice.tree_view().replace('\n', '\n │ ') + '\n'
|
|
196
|
+
+ f" └─{len(self)-1} translated copies of the above {self.first_slice.__short_str__()}")
|
|
197
|
+
else:
|
|
198
|
+
return CollectionOfMeshes.tree_view(self, **kwargs)
|
|
199
|
+
|
|
200
|
+
def __deepcopy__(self, *args):
|
|
201
|
+
return TranslationalSymmetricMesh(self.first_slice.copy(), self.translation, nb_repetitions=len(self) - 1, name=self.name)
|
|
202
|
+
|
|
203
|
+
@inplace_transformation
|
|
204
|
+
def translate(self, vector):
|
|
205
|
+
CollectionOfMeshes.translate(self, vector)
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
@inplace_transformation
|
|
209
|
+
def rotate(self, axis: Axis, angle: float):
|
|
210
|
+
self.translation = axis.rotate_vectors([self.translation], angle)[0, :]
|
|
211
|
+
CollectionOfMeshes.rotate(self, axis, angle)
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
@inplace_transformation
|
|
215
|
+
def mirror(self, plane: Plane):
|
|
216
|
+
self.translation -= 2 * (self.translation @ plane.normal) * plane.normal
|
|
217
|
+
CollectionOfMeshes.mirror(self, plane)
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
def join_meshes(*meshes, name=None, return_masks=False):
|
|
221
|
+
assert all(isinstance(mesh, TranslationalSymmetricMesh) for mesh in meshes), \
|
|
222
|
+
"Only meshes with the same symmetry can be joined together."
|
|
223
|
+
assert all(np.allclose(meshes[0].translation, mesh.translation) for mesh in meshes), \
|
|
224
|
+
"Only translation symmetric meshes with the same translation vector can be joined together."
|
|
225
|
+
assert all(len(meshes[0]) == len(mesh) for mesh in meshes), \
|
|
226
|
+
"Only symmetric meshes with the same number of elements can be joined together."
|
|
227
|
+
if not return_masks:
|
|
228
|
+
strip_name = f"strip_of_{name}" if name is not None else None
|
|
229
|
+
mesh_strip = meshes[0].first_slice.join_meshes(
|
|
230
|
+
*(mesh.first_slice for mesh in meshes[1:]),
|
|
231
|
+
name=strip_name,
|
|
232
|
+
return_masks=False
|
|
233
|
+
)
|
|
234
|
+
return TranslationalSymmetricMesh(
|
|
235
|
+
mesh_strip,
|
|
236
|
+
translation=meshes[0].translation,
|
|
237
|
+
nb_repetitions=len(meshes[0]) - 1,
|
|
238
|
+
name=name
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
strip_name = f"strip_of_{name}" if name is not None else None
|
|
242
|
+
mesh_strip, strip_masks = meshes[0].first_slice.join_meshes(
|
|
243
|
+
*(mesh.first_slice for mesh in meshes[1:]),
|
|
244
|
+
name=strip_name,
|
|
245
|
+
return_masks=True
|
|
246
|
+
)
|
|
247
|
+
joined = TranslationalSymmetricMesh(
|
|
248
|
+
mesh_strip,
|
|
249
|
+
translation=meshes[0].translation,
|
|
250
|
+
nb_repetitions=len(meshes[0]) - 1,
|
|
251
|
+
name=name
|
|
252
|
+
)
|
|
253
|
+
masks = [np.concatenate([
|
|
254
|
+
strip_mask for _ in range(len(meshes[0]))
|
|
255
|
+
]) for strip_mask in strip_masks]
|
|
256
|
+
return joined, masks
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def build_regular_array_of_meshes(base_mesh, distance, nb_bodies):
|
|
260
|
+
"""Create an array of objects using TranslationalSymmetries.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
base_mesh : Mesh or CollectionOfMeshes or SymmetricMesh
|
|
265
|
+
The mesh to duplicate to create the array
|
|
266
|
+
distance : float
|
|
267
|
+
Center-to-center distance between objects in the array
|
|
268
|
+
nb_bodies : couple of ints
|
|
269
|
+
Number of objects in the x and y directions.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
TranslationalSymmetricMesh
|
|
274
|
+
"""
|
|
275
|
+
if nb_bodies[0] == 1:
|
|
276
|
+
line = base_mesh
|
|
277
|
+
else:
|
|
278
|
+
line = TranslationalSymmetricMesh(base_mesh, translation=(distance, 0.0, 0.0), nb_repetitions=nb_bodies[0] - 1,
|
|
279
|
+
name=f'line_of_{base_mesh.name}')
|
|
280
|
+
if nb_bodies[1] == 1:
|
|
281
|
+
array = line
|
|
282
|
+
else:
|
|
283
|
+
array = TranslationalSymmetricMesh(line, translation=(0.0, distance, 0.0), nb_repetitions=nb_bodies[1] - 1,
|
|
284
|
+
name=f'array_of_{base_mesh.name}')
|
|
285
|
+
return array
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class AxialSymmetricMesh(SymmetricMesh):
|
|
289
|
+
"""A mesh with a repeating pattern by rotation.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
mesh_slice : Mesh or CollectionOfMeshes
|
|
294
|
+
the pattern that will be repeated to form the whole body
|
|
295
|
+
axis : Axis, optional
|
|
296
|
+
symmetry axis
|
|
297
|
+
nb_repetitions : int, optional
|
|
298
|
+
the number of repetitions of the pattern (excluding the original one, default: 1)
|
|
299
|
+
name : str, optional
|
|
300
|
+
a name for the mesh
|
|
301
|
+
"""
|
|
302
|
+
def __init__(self, mesh_slice: Union[Mesh, CollectionOfMeshes], axis: Axis=Oz_axis, nb_repetitions: int=1, name=None):
|
|
303
|
+
assert isinstance(mesh_slice, Mesh) or isinstance(mesh_slice, CollectionOfMeshes)
|
|
304
|
+
assert isinstance(nb_repetitions, int)
|
|
305
|
+
assert nb_repetitions >= 1
|
|
306
|
+
assert isinstance(axis, Axis)
|
|
307
|
+
|
|
308
|
+
slices = [mesh_slice]
|
|
309
|
+
for i in range(1, nb_repetitions+1):
|
|
310
|
+
slices.append(mesh_slice.rotated(axis, angle=2*i*np.pi/(nb_repetitions+1),
|
|
311
|
+
name=f"rotation_{i}_of_{mesh_slice.name}"))
|
|
312
|
+
|
|
313
|
+
if name is None:
|
|
314
|
+
name = f"rotation_of_{mesh_slice.name}"
|
|
315
|
+
|
|
316
|
+
self.axis = axis.copy()
|
|
317
|
+
|
|
318
|
+
super().__init__(slices, name=name)
|
|
319
|
+
|
|
320
|
+
if not axis.is_parallel_to(Oz_axis):
|
|
321
|
+
LOG.warning(f"{self.name} is an axi-symmetric mesh along a non vertical axis.")
|
|
322
|
+
|
|
323
|
+
if self.name is not None:
|
|
324
|
+
LOG.debug(f"New rotation symmetric mesh: {self.name}.")
|
|
325
|
+
else:
|
|
326
|
+
LOG.debug(f"New rotation symmetric mesh.")
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def from_profile(profile: Union[Callable, Iterable[float]],
|
|
330
|
+
z_range: Iterable[float]=np.linspace(-5, 0, 20),
|
|
331
|
+
axis: Axis=Oz_axis,
|
|
332
|
+
nphi: int=20,
|
|
333
|
+
name=None):
|
|
334
|
+
"""Return a floating body using the axial symmetry.
|
|
335
|
+
The shape of the body can be defined either with a function defining the profile as [f(z), 0, z] for z in z_range.
|
|
336
|
+
Alternatively, the profile can be defined as a list of points.
|
|
337
|
+
The number of vertices along the vertical direction is len(z_range) in the first case and profile.shape[0] in the second case.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
profile : function(float → float) or array(N, 3)
|
|
342
|
+
define the shape of the body either as a function or a list of points.
|
|
343
|
+
z_range: array(N), optional
|
|
344
|
+
used only if the profile is defined as a function.
|
|
345
|
+
axis : Axis
|
|
346
|
+
symmetry axis
|
|
347
|
+
nphi : int, optional
|
|
348
|
+
number of vertical slices forming the body
|
|
349
|
+
name : str, optional
|
|
350
|
+
name of the generated body (optional)
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
AxialSymmetricMesh
|
|
355
|
+
the generated mesh
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
if name is None:
|
|
359
|
+
name = "axisymmetric_mesh"
|
|
360
|
+
|
|
361
|
+
if callable(profile):
|
|
362
|
+
z_range = np.asarray(z_range)
|
|
363
|
+
x_values = [profile(z) for z in z_range]
|
|
364
|
+
profile_array = np.stack([x_values, np.zeros(len(z_range)), z_range]).T
|
|
365
|
+
else:
|
|
366
|
+
profile_array = np.asarray(profile)
|
|
367
|
+
|
|
368
|
+
assert len(profile_array.shape) == 2
|
|
369
|
+
assert profile_array.shape[1] == 3
|
|
370
|
+
|
|
371
|
+
n = profile_array.shape[0]
|
|
372
|
+
angle = 2 * np.pi / nphi
|
|
373
|
+
|
|
374
|
+
nodes_slice = np.concatenate([profile_array, axis.rotate_points(profile_array, angle)])
|
|
375
|
+
faces_slice = np.array([[i, i+n, i+n+1, i+1] for i in range(n-1)])
|
|
376
|
+
body_slice = Mesh(nodes_slice, faces_slice, name=f"slice_of_{name}")
|
|
377
|
+
body_slice.merge_duplicates()
|
|
378
|
+
body_slice.heal_triangles()
|
|
379
|
+
|
|
380
|
+
return AxialSymmetricMesh(body_slice, axis=axis, nb_repetitions=nphi - 1, name=name)
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def first_slice(self):
|
|
384
|
+
return self[0]
|
|
385
|
+
|
|
386
|
+
def __str__(self):
|
|
387
|
+
return f"{self.__class__.__name__}({self.first_slice.__short_str__()}, axis={self.axis}, nb_repetitions={len(self)-1}, name=\"{self.name}\")"
|
|
388
|
+
|
|
389
|
+
def __repr__(self):
|
|
390
|
+
return f"{self.__class__.__name__}({self.first_slice}, axis={self.axis}, nb_repetitions={len(self)-1}, name=\"{self.name}\")"
|
|
391
|
+
|
|
392
|
+
def __rich_repr__(self):
|
|
393
|
+
yield self.first_slice
|
|
394
|
+
yield "axis", self.axis
|
|
395
|
+
yield "nb_repetitions", len(self)-1
|
|
396
|
+
yield "name", self.name
|
|
397
|
+
|
|
398
|
+
def tree_view(self, fold_symmetry=True, **kwargs):
|
|
399
|
+
if fold_symmetry:
|
|
400
|
+
return (self.__short_str__() + '\n' + ' ├─' + self.first_slice.tree_view().replace('\n', '\n │ ') + '\n'
|
|
401
|
+
+ f" └─{len(self)-1} rotated copies of the above {self.first_slice.__short_str__()}")
|
|
402
|
+
else:
|
|
403
|
+
return CollectionOfMeshes.tree_view(self, **kwargs)
|
|
404
|
+
|
|
405
|
+
def __deepcopy__(self, *args):
|
|
406
|
+
return AxialSymmetricMesh(self.first_slice.copy(), axis=self.axis.copy(), nb_repetitions=len(self) - 1, name=self.name)
|
|
407
|
+
|
|
408
|
+
def join_meshes(*meshes, name=None, return_masks=False):
|
|
409
|
+
assert all(isinstance(mesh, AxialSymmetricMesh) for mesh in meshes), \
|
|
410
|
+
"Only meshes with the same symmetry can be joined together."
|
|
411
|
+
assert all(meshes[0].axis == mesh.axis for mesh in meshes), \
|
|
412
|
+
"Only axisymmetric meshes with the same symmetry axis can be joined together."
|
|
413
|
+
assert all(len(meshes[0]) == len(mesh) for mesh in meshes), \
|
|
414
|
+
"Only axisymmetric meshes with the same number of elements can be joined together."
|
|
415
|
+
if not return_masks:
|
|
416
|
+
slice_name = f"slice_of_{name}" if name is not None else None
|
|
417
|
+
mesh_slice = meshes[0].first_slice.join_meshes(
|
|
418
|
+
*(mesh.first_slice for mesh in meshes[1:]),
|
|
419
|
+
name=slice_name,
|
|
420
|
+
return_masks=False
|
|
421
|
+
)
|
|
422
|
+
return AxialSymmetricMesh(
|
|
423
|
+
mesh_slice,
|
|
424
|
+
axis=meshes[0].axis,
|
|
425
|
+
nb_repetitions=len(meshes[0]) - 1,
|
|
426
|
+
name=name
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
slice_name = f"slice_of_{name}" if name is not None else None
|
|
430
|
+
mesh_slice, slice_masks = meshes[0].first_slice.join_meshes(
|
|
431
|
+
*(mesh.first_slice for mesh in meshes[1:]),
|
|
432
|
+
name=slice_name,
|
|
433
|
+
return_masks=True
|
|
434
|
+
)
|
|
435
|
+
joined = AxialSymmetricMesh(
|
|
436
|
+
mesh_slice,
|
|
437
|
+
axis=meshes[0].axis,
|
|
438
|
+
nb_repetitions=len(meshes[0]) - 1,
|
|
439
|
+
name=name
|
|
440
|
+
)
|
|
441
|
+
masks = [np.concatenate([
|
|
442
|
+
slice_mask for _ in range(len(meshes[0]))
|
|
443
|
+
]) for slice_mask in slice_masks]
|
|
444
|
+
return joined, masks
|
|
445
|
+
|
|
446
|
+
@inplace_transformation
|
|
447
|
+
def translate(self, vector):
|
|
448
|
+
self.axis.translate(vector)
|
|
449
|
+
CollectionOfMeshes.translate(self, vector)
|
|
450
|
+
return self
|
|
451
|
+
|
|
452
|
+
@inplace_transformation
|
|
453
|
+
def rotate(self, other_axis: Axis, angle: float):
|
|
454
|
+
self.axis.rotate(other_axis, angle)
|
|
455
|
+
CollectionOfMeshes.rotate(self, other_axis, angle)
|
|
456
|
+
return self
|
|
457
|
+
|
|
458
|
+
@inplace_transformation
|
|
459
|
+
def mirror(self, plane: Plane):
|
|
460
|
+
self.axis.mirror(plane)
|
|
461
|
+
CollectionOfMeshes.mirror(self, plane)
|
|
462
|
+
return self
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
2
|
+
# See LICENSE file at <https://github.com/mancellin/capytaine>
|
|
3
|
+
|
|
4
|
+
from capytaine.post_pro.rao import rao
|
|
5
|
+
from capytaine.post_pro.impedance import impedance, rao_transfer_function
|
|
6
|
+
from capytaine.post_pro.kochin import compute_kochin
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""This module implements objects describing a mesh on which the free surface elevation will be computed."""
|
|
2
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/mancellin/capytaine>
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from itertools import product
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from capytaine.meshes.meshes import Mesh
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FreeSurface():
|
|
16
|
+
"""A cartesian mesh on which the free surface elevation will be computed.
|
|
17
|
+
|
|
18
|
+
Has a :code:`mesh` attribute to behave kind of like FloatingBody when
|
|
19
|
+
building of the influence matrix.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
x_range: Tuple[float, float], optional
|
|
24
|
+
extreme values of the mesh in the x direction
|
|
25
|
+
nx: int, optional
|
|
26
|
+
number of cells in the x direction
|
|
27
|
+
y_range: Tuple[float, float], optional
|
|
28
|
+
extreme values of the mesh in the y direction
|
|
29
|
+
ny: int, optional
|
|
30
|
+
number of cells in the y direction
|
|
31
|
+
name: string, optional
|
|
32
|
+
a name for the free surface object
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
.. todo:: Generalize to non-cartesian meshes.
|
|
36
|
+
In particular, it could be of interest to build meshes having the
|
|
37
|
+
same symmetry as a given floating body to speed up the
|
|
38
|
+
construction of the influence matrix.
|
|
39
|
+
|
|
40
|
+
.. seealso::
|
|
41
|
+
|
|
42
|
+
:meth:`~capytaine.bem.nemoh.Nemoh.get_free_surface_elevation`
|
|
43
|
+
The main function requiring a FreeSurface object.
|
|
44
|
+
"""
|
|
45
|
+
def __init__(self, x_range=(-50.0, 50.0), nx=10, y_range=(-50.0, 50.0), ny=10, name=None):
|
|
46
|
+
self.x_range = x_range
|
|
47
|
+
self.nx = nx
|
|
48
|
+
self.y_range = y_range
|
|
49
|
+
self.ny = ny
|
|
50
|
+
|
|
51
|
+
if name is None:
|
|
52
|
+
self.name = f"free_surface_{next(Mesh._ids)}"
|
|
53
|
+
else:
|
|
54
|
+
self.name = name
|
|
55
|
+
|
|
56
|
+
self.mesh = self._generate_mesh()
|
|
57
|
+
|
|
58
|
+
def _generate_mesh(self):
|
|
59
|
+
"""Generate a 2D cartesian mesh."""
|
|
60
|
+
nodes = np.zeros(((self.nx+1)*(self.ny+1), 3), dtype=float)
|
|
61
|
+
panels = np.zeros((self.nx*self.ny, 4), dtype=int)
|
|
62
|
+
|
|
63
|
+
X = np.linspace(*self.x_range, self.nx+1)
|
|
64
|
+
Y = np.linspace(*self.y_range, self.ny+1)
|
|
65
|
+
for i, (x, y, z) in enumerate(product(X, Y, [0.0])):
|
|
66
|
+
nodes[i, :] = x, y, z
|
|
67
|
+
|
|
68
|
+
for k, (i, j) in enumerate(product(range(0, self.nx), range(0, self.ny))):
|
|
69
|
+
panels[k, :] = (j+i*(self.ny+1),
|
|
70
|
+
(j+1)+i*(self.ny+1),
|
|
71
|
+
(j+1)+(i+1)*(self.ny+1),
|
|
72
|
+
j+(i+1)*(self.ny+1))
|
|
73
|
+
|
|
74
|
+
return Mesh(nodes, panels, name=f"{self.name}_mesh")
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def area(self):
|
|
78
|
+
"""The total area covered by the mesh."""
|
|
79
|
+
return (np.abs(self.x_range[1] - self.x_range[0])
|
|
80
|
+
* np.abs(self.y_range[1] - self.y_range[0]))
|
|
81
|
+
|
|
82
|
+
def incoming_waves(self, problem: "DiffractionProblem") -> np.ndarray:
|
|
83
|
+
"""Free surface elevation of the undisturbed incoming waves
|
|
84
|
+
for a given diffraction problem.
|
|
85
|
+
Kept for legacy, but not recommended for use.
|
|
86
|
+
"""
|
|
87
|
+
from capytaine.bem.airy_waves import airy_waves_free_surface_elevation
|
|
88
|
+
return airy_waves_free_surface_elevation(self, problem)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Computation of the impendance matrix."""
|
|
2
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/mancellin/capytaine>
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
LOG = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def rao_transfer_function(dataset, dissipation=None, stiffness=None):
|
|
11
|
+
"""Complex-valued matrix used for the computation of the RAO.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
dataset: xarray Dataset
|
|
16
|
+
The hydrodynamical dataset.
|
|
17
|
+
This function supposes that variables named 'inertia_matrix' and 'hydrostatic_stiffness' are in the dataset.
|
|
18
|
+
Other variables can be computed by Capytaine, by those two should be manually added to the dataset.
|
|
19
|
+
dissipation: array, optional
|
|
20
|
+
An optional dissipation matrix (e.g. Power Take Off) to be included in the transfer function.
|
|
21
|
+
Default: none.
|
|
22
|
+
stiffness: array, optional
|
|
23
|
+
An optional stiffness matrix (e.g. mooring stiffness) to be included in the transfer function.
|
|
24
|
+
Default: none.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
xarray DataArray
|
|
29
|
+
The matrix as an array depending of omega and the degrees of freedom.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
if not hasattr(dataset, 'inertia_matrix'):
|
|
33
|
+
raise AttributeError('Computing the impedance matrix requires an `inertia_matrix` matrix to be defined in the hydrodynamical dataset')
|
|
34
|
+
|
|
35
|
+
if not hasattr(dataset, 'hydrostatic_stiffness'):
|
|
36
|
+
raise AttributeError('Computing the impedance matrix requires an `hydrostatic_stiffness` matrix to be defined in the hydrodynamical dataset')
|
|
37
|
+
|
|
38
|
+
if 'encounter_omega' in dataset.coords:
|
|
39
|
+
omega = dataset.coords['encounter_omega']
|
|
40
|
+
else:
|
|
41
|
+
omega = dataset.coords['omega']
|
|
42
|
+
|
|
43
|
+
# ASSEMBLE MATRICES
|
|
44
|
+
H = (-omega**2*(dataset['inertia_matrix'] + dataset['added_mass'])
|
|
45
|
+
- 1j*omega*dataset['radiation_damping']
|
|
46
|
+
+ dataset['hydrostatic_stiffness'])
|
|
47
|
+
|
|
48
|
+
if dissipation is not None:
|
|
49
|
+
H = H - 1j*omega*dissipation
|
|
50
|
+
|
|
51
|
+
if stiffness is not None:
|
|
52
|
+
H = H + stiffness
|
|
53
|
+
|
|
54
|
+
return H
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def impedance(dataset, dissipation=None, stiffness=None):
|
|
58
|
+
"""Complex-valued mechanical impedance matrix.
|
|
59
|
+
See Falnes for more theoretical details::
|
|
60
|
+
|
|
61
|
+
@book{falnes2002ocean,
|
|
62
|
+
title={Ocean Waves and Oscillating Systems: Linear Interactions Including Wave-Energy Extraction},
|
|
63
|
+
author={Falnes, J.},
|
|
64
|
+
isbn={9781139431934},
|
|
65
|
+
url={https://books.google.com/books?id=bl1FyQjCklgC},
|
|
66
|
+
year={2002},
|
|
67
|
+
publisher={Cambridge University Press}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
dataset: xarray Dataset
|
|
73
|
+
The hydrodynamical dataset.
|
|
74
|
+
This function supposes that variables named 'inertia_matrix' and 'hydrostatic_stiffness' are in the dataset.
|
|
75
|
+
Other variables can be computed by Capytaine, by those two should be manually added to the dataset.
|
|
76
|
+
dissipation: array, optional
|
|
77
|
+
An optional dissipation matrix (e.g. Power Take Off) to be included in the impedance.
|
|
78
|
+
Default: none.
|
|
79
|
+
stiffness: array, optional
|
|
80
|
+
An optional stiffness matrix (e.g. mooring stiffness) to be included in the impedance.
|
|
81
|
+
Default: none.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
xarray DataArray
|
|
86
|
+
The impedance as an array depending of omega and the degrees of freedom.
|
|
87
|
+
"""
|
|
88
|
+
if 'encounter_omega' in dataset.coords:
|
|
89
|
+
omega = dataset.coords['encounter_omega']
|
|
90
|
+
else:
|
|
91
|
+
omega = dataset.coords['omega']
|
|
92
|
+
return 1/(-1j * omega) * rao_transfer_function(dataset, dissipation, stiffness)
|