capytaine 2.1__cp39-cp39-win_amd64.whl → 2.2.1__cp39-cp39-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 +1 -1
- capytaine/__init__.py +10 -7
- capytaine/bem/engines.py +2 -2
- capytaine/bem/problems_and_results.py +17 -9
- capytaine/bem/solver.py +71 -28
- capytaine/bodies/bodies.py +133 -24
- capytaine/green_functions/delhommeau.py +103 -51
- capytaine/green_functions/libs/Delhommeau_float32.cp39-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/Delhommeau_float32.cp39-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp39-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp39-win_amd64.pyd +0 -0
- capytaine/io/mesh_loaders.py +49 -24
- capytaine/io/meshio.py +4 -1
- capytaine/io/xarray.py +17 -7
- capytaine/matrices/block.py +4 -2
- capytaine/matrices/linear_solvers.py +2 -3
- capytaine/matrices/low_rank.py +3 -1
- capytaine/meshes/clipper.py +3 -3
- capytaine/meshes/collections.py +13 -2
- capytaine/meshes/meshes.py +128 -4
- capytaine/meshes/predefined/cylinders.py +2 -2
- capytaine/meshes/properties.py +77 -0
- capytaine/post_pro/rao.py +1 -1
- capytaine/tools/cache_on_disk.py +3 -1
- capytaine/tools/symbolic_multiplication.py +23 -4
- capytaine/ui/vtk/body_viewer.py +2 -0
- capytaine-2.2.1.dist-info/DELVEWHEEL +2 -0
- capytaine-2.2.1.dist-info/METADATA +754 -0
- {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/RECORD +33 -37
- capytaine/green_functions/libs/XieDelhommeau_float32.cp39-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/XieDelhommeau_float32.cp39-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/XieDelhommeau_float64.cp39-win_amd64.dll.a +0 -0
- capytaine/green_functions/libs/XieDelhommeau_float64.cp39-win_amd64.pyd +0 -0
- capytaine-2.1.dist-info/DELVEWHEEL +0 -2
- capytaine-2.1.dist-info/METADATA +0 -756
- {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/LICENSE +0 -0
- {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/WHEEL +0 -0
- {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/entry_points.txt +0 -0
- capytaine.libs/{.load-order-capytaine-2.1 → .load-order-capytaine-2.2.1} +2 -2
capytaine/bodies/bodies.py
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""Floating bodies to be used in radiation-diffraction problems."""
|
|
2
|
-
# Copyright (C) 2017-
|
|
3
|
-
# See LICENSE file at <https://github.com/
|
|
2
|
+
# Copyright (C) 2017-2024 Matthieu Ancellin
|
|
3
|
+
# See LICENSE file at <https://github.com/capytaine/capytaine>
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import copy
|
|
7
7
|
from itertools import chain, accumulate, zip_longest
|
|
8
|
+
from functools import cached_property, lru_cache
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
10
11
|
import xarray as xr
|
|
11
12
|
|
|
12
|
-
from capytaine.
|
|
13
|
-
meshio = silently_import_optional_dependency("meshio")
|
|
14
|
-
|
|
13
|
+
from capytaine.meshes.collections import CollectionOfMeshes
|
|
15
14
|
from capytaine.meshes.geometry import Abstract3DObject, ClippableMixin, Plane, inplace_transformation
|
|
15
|
+
from capytaine.meshes.properties import connected_components, connected_components_of_waterline
|
|
16
16
|
from capytaine.meshes.meshes import Mesh
|
|
17
17
|
from capytaine.meshes.symmetric import build_regular_array_of_meshes
|
|
18
|
-
from capytaine.meshes.collections import CollectionOfMeshes
|
|
19
18
|
from capytaine.bodies.dofs import RigidBodyDofsPlaceholder
|
|
20
19
|
|
|
20
|
+
from capytaine.tools.optional_imports import silently_import_optional_dependency
|
|
21
|
+
meshio = silently_import_optional_dependency("meshio")
|
|
22
|
+
|
|
21
23
|
LOG = logging.getLogger(__name__)
|
|
22
24
|
|
|
23
25
|
TRANSLATION_DOFS_DIRECTIONS = {"surge": (1, 0, 0), "sway": (0, 1, 0), "heave": (0, 0, 1)}
|
|
@@ -38,8 +40,12 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
38
40
|
Parameters
|
|
39
41
|
----------
|
|
40
42
|
mesh : Mesh or CollectionOfMeshes, optional
|
|
41
|
-
the mesh describing the geometry of the floating body.
|
|
43
|
+
the mesh describing the geometry of the hull of the floating body.
|
|
42
44
|
If none is given, a empty one is created.
|
|
45
|
+
lid_mesh : Mesh or CollectionOfMeshes or None, optional
|
|
46
|
+
a mesh of an internal lid for irregular frequencies removal.
|
|
47
|
+
Unlike the mesh of the hull, no dof is defined on the lid_mesh.
|
|
48
|
+
If none is given, none is used when solving the Boundary Integral Equation.
|
|
43
49
|
dofs : dict, optional
|
|
44
50
|
the degrees of freedom of the body.
|
|
45
51
|
If none is given, a empty dictionary is initialized.
|
|
@@ -55,7 +61,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
55
61
|
If none is given, the one of the mesh is used.
|
|
56
62
|
"""
|
|
57
63
|
|
|
58
|
-
def __init__(self, mesh=None, dofs=None, mass=None, center_of_mass=None, name=None):
|
|
64
|
+
def __init__(self, mesh=None, dofs=None, mass=None, center_of_mass=None, name=None, *, lid_mesh=None):
|
|
59
65
|
if mesh is None:
|
|
60
66
|
self.mesh = Mesh(name="dummy_mesh")
|
|
61
67
|
|
|
@@ -69,6 +75,15 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
69
75
|
else:
|
|
70
76
|
raise TypeError("Unrecognized `mesh` object passed to the FloatingBody constructor.")
|
|
71
77
|
|
|
78
|
+
if lid_mesh is not None:
|
|
79
|
+
if lid_mesh.nb_faces == 0:
|
|
80
|
+
LOG.warning("Lid mesh %s provided for body initialization is empty. The lid mesh is ignored.", lid_mesh)
|
|
81
|
+
self.lid_mesh = None
|
|
82
|
+
else:
|
|
83
|
+
self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
|
|
84
|
+
else:
|
|
85
|
+
self.lid_mesh = None
|
|
86
|
+
|
|
72
87
|
if name is None and mesh is None:
|
|
73
88
|
self.name = "dummy_body"
|
|
74
89
|
elif name is None:
|
|
@@ -99,13 +114,15 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
99
114
|
|
|
100
115
|
@staticmethod
|
|
101
116
|
def from_meshio(mesh, name=None) -> 'FloatingBody':
|
|
102
|
-
"""Create a FloatingBody from a meshio mesh object.
|
|
117
|
+
"""Create a FloatingBody from a meshio mesh object.
|
|
118
|
+
Kinda deprecated, use cpt.load_mesh instead."""
|
|
103
119
|
from capytaine.io.meshio import load_from_meshio
|
|
104
120
|
return FloatingBody(mesh=load_from_meshio(mesh, name), name=name)
|
|
105
121
|
|
|
106
122
|
@staticmethod
|
|
107
123
|
def from_file(filename: str, file_format=None, name=None) -> 'FloatingBody':
|
|
108
|
-
"""Create a FloatingBody from a mesh file using meshmagick.
|
|
124
|
+
"""Create a FloatingBody from a mesh file using meshmagick.
|
|
125
|
+
Kinda deprecated, use cpt.load_mesh instead."""
|
|
109
126
|
from capytaine.io.mesh_loaders import load_mesh
|
|
110
127
|
if name is None: name = filename
|
|
111
128
|
mesh = load_mesh(filename, file_format, name=f"{name}_mesh")
|
|
@@ -115,6 +132,13 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
115
132
|
"""Arbitrary order. The point is to sort together the problems involving the same body."""
|
|
116
133
|
return self.name < other.name
|
|
117
134
|
|
|
135
|
+
@cached_property
|
|
136
|
+
def mesh_including_lid(self):
|
|
137
|
+
if self.lid_mesh is not None:
|
|
138
|
+
return CollectionOfMeshes([self.mesh, self.lid_mesh])
|
|
139
|
+
else:
|
|
140
|
+
return self.mesh
|
|
141
|
+
|
|
118
142
|
##########
|
|
119
143
|
# Dofs #
|
|
120
144
|
##########
|
|
@@ -173,7 +197,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
173
197
|
if hasattr(self, point_attr) and getattr(self, point_attr) is not None:
|
|
174
198
|
axis_point = getattr(self, point_attr)
|
|
175
199
|
LOG.info(f"The rotation dof {name} has been initialized around the point: "
|
|
176
|
-
f"
|
|
200
|
+
f"{self.__short_str__()}.{point_attr} = {getattr(self, point_attr)}")
|
|
177
201
|
break
|
|
178
202
|
else:
|
|
179
203
|
axis_point = np.array([0, 0, 0])
|
|
@@ -267,7 +291,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
267
291
|
"""Returns volume of the FloatingBody."""
|
|
268
292
|
return self.mesh.volume
|
|
269
293
|
|
|
270
|
-
def disp_mass(self, *, rho=1000):
|
|
294
|
+
def disp_mass(self, *, rho=1000.0):
|
|
271
295
|
return self.mesh.disp_mass(rho=rho)
|
|
272
296
|
|
|
273
297
|
@property
|
|
@@ -512,7 +536,9 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
512
536
|
raise AttributeError("Cannot compute hydrostatics stiffness on {} since no dof has been defined.".format(self.name))
|
|
513
537
|
|
|
514
538
|
def divergence_dof(influenced_dof):
|
|
515
|
-
if
|
|
539
|
+
if influenced_dof.lower() in [*TRANSLATION_DOFS_DIRECTIONS, *ROTATION_DOFS_AXIS]:
|
|
540
|
+
return 0.0 # Dummy value that is not actually used afterwards.
|
|
541
|
+
elif divergence is None:
|
|
516
542
|
return 0.0
|
|
517
543
|
elif isinstance(divergence, dict) and influenced_dof in divergence.keys():
|
|
518
544
|
return divergence[influenced_dof]
|
|
@@ -536,7 +562,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
536
562
|
K = hs_set.hydrostatic_stiffness.sel(influenced_dof=list(self.dofs.keys()), radiating_dof=list(self.dofs.keys()))
|
|
537
563
|
return K
|
|
538
564
|
|
|
539
|
-
def compute_rigid_body_inertia(self, *, rho=1000, output_type="body_dofs"):
|
|
565
|
+
def compute_rigid_body_inertia(self, *, rho=1000.0, output_type="body_dofs"):
|
|
540
566
|
"""
|
|
541
567
|
Inertia Mass matrix of the body for 6 rigid DOFs.
|
|
542
568
|
|
|
@@ -626,8 +652,8 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
|
|
|
626
652
|
|
|
627
653
|
if output_type == "body_dofs":
|
|
628
654
|
if len(non_rigid_dofs) > 0:
|
|
629
|
-
LOG.warning(f"Non-rigid dofs
|
|
630
|
-
|
|
655
|
+
LOG.warning(f"Non-rigid dofs {non_rigid_dofs} detected: their \
|
|
656
|
+
inertia coefficients are assigned as NaN.")
|
|
631
657
|
|
|
632
658
|
inertia_matrix_xr = total_mass_xr.sel(influenced_dof=body_dof_names,
|
|
633
659
|
radiating_dof=body_dof_names)
|
|
@@ -729,7 +755,17 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
729
755
|
def join_bodies(*bodies, name=None) -> 'FloatingBody':
|
|
730
756
|
if name is None:
|
|
731
757
|
name = "+".join(body.name for body in bodies)
|
|
732
|
-
meshes = CollectionOfMeshes(
|
|
758
|
+
meshes = CollectionOfMeshes(
|
|
759
|
+
[body.mesh for body in bodies],
|
|
760
|
+
name=f"{name}_mesh"
|
|
761
|
+
)
|
|
762
|
+
if all(body.lid_mesh is None for body in bodies):
|
|
763
|
+
lid_meshes = None
|
|
764
|
+
else:
|
|
765
|
+
lid_meshes = CollectionOfMeshes(
|
|
766
|
+
[body.lid_mesh for body in bodies if body.lid_mesh is not None],
|
|
767
|
+
name=f"{name}_lid_mesh"
|
|
768
|
+
)
|
|
733
769
|
dofs = FloatingBody.combine_dofs(bodies)
|
|
734
770
|
|
|
735
771
|
if all(body.mass is not None for body in bodies):
|
|
@@ -744,7 +780,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
744
780
|
new_cog = None
|
|
745
781
|
|
|
746
782
|
joined_bodies = FloatingBody(
|
|
747
|
-
mesh=meshes,
|
|
783
|
+
mesh=meshes, lid_mesh=lid_meshes, dofs=dofs,
|
|
784
|
+
mass=new_mass, center_of_mass=new_cog, name=name
|
|
748
785
|
)
|
|
749
786
|
|
|
750
787
|
for matrix_name in ["inertia_matrix", "hydrostatic_stiffness"]:
|
|
@@ -820,7 +857,6 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
820
857
|
if not isinstance(locations, np.ndarray):
|
|
821
858
|
raise TypeError('locations must be of type np.ndarray')
|
|
822
859
|
assert locations.shape[1] == 2, 'locations must be of shape nx2, received {:}'.format(locations.shape)
|
|
823
|
-
n = locations.shape[0]
|
|
824
860
|
|
|
825
861
|
fb_list = []
|
|
826
862
|
for idx, li in enumerate(locations):
|
|
@@ -836,6 +872,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
836
872
|
def extract_faces(self, id_faces_to_extract, return_index=False):
|
|
837
873
|
"""Create a new FloatingBody by extracting some faces from the mesh.
|
|
838
874
|
The dofs evolve accordingly.
|
|
875
|
+
The lid_mesh, center_of_mass, mass and hydrostatics data are discarded.
|
|
839
876
|
"""
|
|
840
877
|
if isinstance(self.mesh, CollectionOfMeshes):
|
|
841
878
|
raise NotImplementedError # TODO
|
|
@@ -857,7 +894,12 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
857
894
|
return new_body
|
|
858
895
|
|
|
859
896
|
def sliced_by_plane(self, plane):
|
|
860
|
-
|
|
897
|
+
"""Return the same body, but replace the mesh by a set of two meshes
|
|
898
|
+
corresponding to each sides of the plane."""
|
|
899
|
+
return FloatingBody(mesh=self.mesh.sliced_by_plane(plane),
|
|
900
|
+
lid_mesh=self.lid_mesh.sliced_by_plane(plane)
|
|
901
|
+
if self.lid_mesh is not None else None,
|
|
902
|
+
dofs=self.dofs, name=self.name)
|
|
861
903
|
|
|
862
904
|
def minced(self, nb_slices=(8, 8, 4)):
|
|
863
905
|
"""Experimental method decomposing the mesh as a hierarchical structure.
|
|
@@ -918,6 +960,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
918
960
|
@inplace_transformation
|
|
919
961
|
def mirror(self, plane):
|
|
920
962
|
self.mesh.mirror(plane)
|
|
963
|
+
if self.lid_mesh is not None:
|
|
964
|
+
self.lid_mesh.mirror(plane)
|
|
921
965
|
for dof in self.dofs:
|
|
922
966
|
self.dofs[dof] -= 2 * np.outer(np.dot(self.dofs[dof], plane.normal), plane.normal)
|
|
923
967
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
@@ -930,6 +974,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
930
974
|
@inplace_transformation
|
|
931
975
|
def translate(self, vector, *args, **kwargs):
|
|
932
976
|
self.mesh.translate(vector, *args, **kwargs)
|
|
977
|
+
if self.lid_mesh is not None:
|
|
978
|
+
self.lid_mesh.translate(vector, *args, **kwargs)
|
|
933
979
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
934
980
|
if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
|
|
935
981
|
self.__dict__[point_attr] = np.array(self.__dict__[point_attr]) + vector
|
|
@@ -938,6 +984,8 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
938
984
|
@inplace_transformation
|
|
939
985
|
def rotate(self, axis, angle):
|
|
940
986
|
self.mesh.rotate(axis, angle)
|
|
987
|
+
if self.lid_mesh is not None:
|
|
988
|
+
self.lid_mesh.rotate(axis, angle)
|
|
941
989
|
for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
|
|
942
990
|
if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
|
|
943
991
|
self.__dict__[point_attr] = axis.rotate_points([self.__dict__[point_attr]], angle)[0, :]
|
|
@@ -950,6 +998,11 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
950
998
|
# Clip mesh
|
|
951
999
|
LOG.info(f"Clipping {self.name} with respect to {plane}")
|
|
952
1000
|
self.mesh.clip(plane)
|
|
1001
|
+
if self.lid_mesh is not None:
|
|
1002
|
+
self.lid_mesh.clip(plane)
|
|
1003
|
+
if self.lid_mesh.nb_faces == 0:
|
|
1004
|
+
LOG.warning("Lid mesh %s is empty after clipping. The lid mesh is removed.", self.lid_mesh)
|
|
1005
|
+
self.lid_mesh = None
|
|
953
1006
|
|
|
954
1007
|
# Clip dofs
|
|
955
1008
|
ids = self.mesh._clipping_data['faces_ids']
|
|
@@ -976,11 +1029,25 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
976
1029
|
|
|
977
1030
|
def __str__(self):
|
|
978
1031
|
short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
|
|
979
|
-
|
|
1032
|
+
|
|
1033
|
+
if self.lid_mesh is not None:
|
|
1034
|
+
lid_mesh_str = self.lid_mesh.__short_str__()
|
|
1035
|
+
else:
|
|
1036
|
+
lid_mesh_str = str(None)
|
|
1037
|
+
|
|
1038
|
+
return (f"{self.__class__.__name__}(mesh={self.mesh.__short_str__()}, lid_mesh={lid_mesh_str}, "
|
|
1039
|
+
f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
|
|
980
1040
|
|
|
981
1041
|
def __repr__(self):
|
|
982
1042
|
short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
|
|
983
|
-
|
|
1043
|
+
|
|
1044
|
+
if self.lid_mesh is not None:
|
|
1045
|
+
lid_mesh_str = str(self.lid_mesh)
|
|
1046
|
+
else:
|
|
1047
|
+
lid_mesh_str = str(None)
|
|
1048
|
+
|
|
1049
|
+
return (f"{self.__class__.__name__}(mesh={str(self.mesh)}, lid_mesh={lid_mesh_str}, "
|
|
1050
|
+
f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
|
|
984
1051
|
|
|
985
1052
|
def _repr_pretty_(self, p, cycle):
|
|
986
1053
|
p.text(self.__str__())
|
|
@@ -990,6 +1057,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
990
1057
|
def __repr__(self):
|
|
991
1058
|
return '...'
|
|
992
1059
|
yield "mesh", self.mesh
|
|
1060
|
+
yield "lid_mesh", self.lid_mesh
|
|
993
1061
|
yield "dofs", {d: DofWithShortRepr() for d in self.dofs}
|
|
994
1062
|
if self.mass is not None:
|
|
995
1063
|
yield "mass", self.mass, None
|
|
@@ -1034,7 +1102,48 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
1034
1102
|
@property
|
|
1035
1103
|
def minimal_computable_wavelength(self):
|
|
1036
1104
|
"""For accuracy of the resolution, wavelength should not be smaller than this value."""
|
|
1037
|
-
|
|
1105
|
+
if self.lid_mesh is not None:
|
|
1106
|
+
return max(8*self.mesh.faces_radiuses.max(), 8*self.lid_mesh.faces_radiuses.max())
|
|
1107
|
+
else:
|
|
1108
|
+
return 8*self.mesh.faces_radiuses.max()
|
|
1109
|
+
|
|
1110
|
+
@lru_cache
|
|
1111
|
+
def first_irregular_frequency_estimate(self, *, g=9.81):
|
|
1112
|
+
r"""Estimates the angular frequency of the lowest irregular
|
|
1113
|
+
frequency.
|
|
1114
|
+
This is based on the formula for the lowest irregular frequency of a
|
|
1115
|
+
parallelepiped of size :math:`L \times B` and draft :math:`H`:
|
|
1116
|
+
|
|
1117
|
+
.. math::
|
|
1118
|
+
\omega = \sqrt{
|
|
1119
|
+
\frac{\pi g \sqrt{\frac{1}{B^2} + \frac{1}{L^2}}}
|
|
1120
|
+
{\tanh\left(\pi H \sqrt{\frac{1}{B^2} + \frac{1}{L^2}} \right)}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
The formula is applied to all shapes to get an estimate that is usually
|
|
1124
|
+
conservative.
|
|
1125
|
+
The definition of a lid (supposed to be fully covering and horizontal)
|
|
1126
|
+
is taken into account.
|
|
1127
|
+
"""
|
|
1128
|
+
if self.lid_mesh is None:
|
|
1129
|
+
draft = abs(self.mesh.vertices[:, 2].min())
|
|
1130
|
+
else:
|
|
1131
|
+
draft = abs(self.lid_mesh.vertices[:, 2].min())
|
|
1132
|
+
if draft < 1e-6:
|
|
1133
|
+
return np.inf
|
|
1134
|
+
|
|
1135
|
+
# Look for the x and y span of each components (e.g. for multibody) and
|
|
1136
|
+
# keep the one causing the lowest irregular frequency.
|
|
1137
|
+
# The draft is supposed to be same for all components.
|
|
1138
|
+
omega = np.inf
|
|
1139
|
+
for comp in connected_components(self.mesh):
|
|
1140
|
+
for ccomp in connected_components_of_waterline(comp):
|
|
1141
|
+
x_span = ccomp.vertices[:, 0].max() - ccomp.vertices[:, 0].min()
|
|
1142
|
+
y_span = ccomp.vertices[:, 1].max() - ccomp.vertices[:, 1].min()
|
|
1143
|
+
p = np.hypot(1/x_span, 1/y_span)
|
|
1144
|
+
omega_comp = np.sqrt(np.pi*g*p/(np.tanh(np.pi*draft*p)))
|
|
1145
|
+
omega = min(omega, omega_comp)
|
|
1146
|
+
return omega
|
|
1038
1147
|
|
|
1039
1148
|
def cluster_bodies(*bodies, name=None):
|
|
1040
1149
|
"""
|
|
@@ -1052,7 +1161,7 @@ respective inertia coefficients are assigned as NaN.")
|
|
|
1052
1161
|
FloatingBody
|
|
1053
1162
|
Array built from the provided bodies
|
|
1054
1163
|
"""
|
|
1055
|
-
from scipy.cluster.hierarchy import linkage
|
|
1164
|
+
from scipy.cluster.hierarchy import linkage
|
|
1056
1165
|
nb_buoys = len(bodies)
|
|
1057
1166
|
|
|
1058
1167
|
if any(body.center_of_buoyancy is None for body in bodies):
|
|
@@ -23,10 +23,11 @@ _default_parameters = dict(
|
|
|
23
23
|
tabulation_rmax=100.0,
|
|
24
24
|
tabulation_nz=372,
|
|
25
25
|
tabulation_zmin=-251.0,
|
|
26
|
-
tabulation_nb_integration_points=
|
|
27
|
-
|
|
26
|
+
tabulation_nb_integration_points=1001,
|
|
27
|
+
tabulation_grid_shape="scaled_nemoh3",
|
|
28
28
|
finite_depth_prony_decomposition_method="fortran",
|
|
29
|
-
floating_point_precision="float64"
|
|
29
|
+
floating_point_precision="float64",
|
|
30
|
+
gf_singularities="low_freq",
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
|
|
@@ -55,10 +56,16 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
55
56
|
Number of points for the numerical integration w.r.t. :math:`theta` of
|
|
56
57
|
Delhommeau's integrals
|
|
57
58
|
Default: 1000
|
|
58
|
-
|
|
59
|
+
tabulation_grid_shape: string, optional
|
|
59
60
|
Either :code:`"legacy"` or :code:`"scaled_nemoh3"`, which are the two
|
|
60
61
|
methods currently implemented.
|
|
61
62
|
Default: :code:`"scaled_nemoh3"`
|
|
63
|
+
tabulation_cache_dir: str or None, optional
|
|
64
|
+
Directory in which to save the tabulation file(s).
|
|
65
|
+
If None, the tabulation is not saved on disk.
|
|
66
|
+
Default: calls capytaine.tools.cache_on_disk.cache_directory(), which
|
|
67
|
+
returns the value of the environment variable CAPYTAINE_CACHE_DIR if
|
|
68
|
+
set, or else the default cache directory on your system.
|
|
62
69
|
finite_depth_prony_decomposition_method: string, optional
|
|
63
70
|
The implementation of the Prony decomposition used to compute the
|
|
64
71
|
finite water_depth Green function. Accepted values: :code:`'fortran'`
|
|
@@ -69,22 +76,26 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
69
76
|
Either :code:`'float32'` for single precision computations or
|
|
70
77
|
:code:`'float64'` for double precision computations.
|
|
71
78
|
Default: :code:`'float64'`.
|
|
79
|
+
gf_singularities: string, optional
|
|
80
|
+
Chose of the variant among the ways singularities can be extracted from
|
|
81
|
+
the Green function. Currently only affects the infinite depth Green
|
|
82
|
+
function.
|
|
83
|
+
Default: "low_freq".
|
|
72
84
|
|
|
73
85
|
Attributes
|
|
74
86
|
----------
|
|
75
87
|
fortran_core:
|
|
76
88
|
Compiled Fortran module with functions used to compute the Green
|
|
77
89
|
function.
|
|
78
|
-
|
|
90
|
+
tabulation_grid_shape_index: int
|
|
79
91
|
Integer passed to Fortran code to describe which method is used.
|
|
80
92
|
tabulated_r_range: numpy.array of shape (tabulation_nr,) and type floating_point_precision
|
|
81
93
|
tabulated_z_range: numpy.array of shape (tabulation_nz,) and type floating_point_precision
|
|
82
94
|
Coordinates of the tabulation points.
|
|
83
|
-
tabulated_integrals: numpy.array of shape (tabulation_nr, tabulation_nz,
|
|
95
|
+
tabulated_integrals: numpy.array of shape (tabulation_nr, tabulation_nz, nb_tabulated_values) and type floating_point_precision
|
|
84
96
|
Tabulated Delhommeau integrals.
|
|
85
97
|
"""
|
|
86
98
|
|
|
87
|
-
fortran_core_basename = "Delhommeau"
|
|
88
99
|
|
|
89
100
|
|
|
90
101
|
def __init__(self, *,
|
|
@@ -93,23 +104,43 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
93
104
|
tabulation_nz=_default_parameters["tabulation_nz"],
|
|
94
105
|
tabulation_zmin=_default_parameters["tabulation_zmin"],
|
|
95
106
|
tabulation_nb_integration_points=_default_parameters["tabulation_nb_integration_points"],
|
|
96
|
-
|
|
107
|
+
tabulation_grid_shape=_default_parameters["tabulation_grid_shape"],
|
|
108
|
+
tabulation_cache_dir=cache_directory(),
|
|
97
109
|
finite_depth_prony_decomposition_method=_default_parameters["finite_depth_prony_decomposition_method"],
|
|
98
110
|
floating_point_precision=_default_parameters["floating_point_precision"],
|
|
111
|
+
gf_singularities=_default_parameters["gf_singularities"],
|
|
99
112
|
):
|
|
100
113
|
|
|
101
|
-
self.
|
|
114
|
+
self.fortran_core = import_module(f"capytaine.green_functions.libs.Delhommeau_{floating_point_precision}")
|
|
102
115
|
|
|
103
|
-
self.
|
|
116
|
+
self.tabulation_grid_shape = tabulation_grid_shape
|
|
117
|
+
fortran_enum = {
|
|
118
|
+
'legacy': self.fortran_core.constants.legacy_grid,
|
|
119
|
+
'scaled_nemoh3': self.fortran_core.constants.scaled_nemoh3_grid,
|
|
120
|
+
}
|
|
121
|
+
self.tabulation_grid_shape_index = fortran_enum[tabulation_grid_shape]
|
|
104
122
|
|
|
105
|
-
self.
|
|
106
|
-
|
|
107
|
-
'
|
|
108
|
-
'
|
|
123
|
+
self.gf_singularities = gf_singularities
|
|
124
|
+
fortran_enum = {
|
|
125
|
+
'high_freq': self.fortran_core.constants.high_freq,
|
|
126
|
+
'low_freq': self.fortran_core.constants.low_freq,
|
|
127
|
+
'low_freq_with_rankine_part': self.fortran_core.constants.low_freq_with_rankine_part,
|
|
109
128
|
}
|
|
110
|
-
self.
|
|
129
|
+
self.gf_singularities_index = fortran_enum[gf_singularities]
|
|
130
|
+
|
|
131
|
+
self.floating_point_precision = floating_point_precision
|
|
132
|
+
self.tabulation_nb_integration_points = tabulation_nb_integration_points
|
|
111
133
|
|
|
112
|
-
self.
|
|
134
|
+
self.tabulation_cache_dir = tabulation_cache_dir
|
|
135
|
+
if tabulation_cache_dir is None:
|
|
136
|
+
self._create_tabulation(tabulation_nr, tabulation_rmax,
|
|
137
|
+
tabulation_nz, tabulation_zmin,
|
|
138
|
+
tabulation_nb_integration_points)
|
|
139
|
+
else:
|
|
140
|
+
self._create_or_load_tabulation(tabulation_nr, tabulation_rmax,
|
|
141
|
+
tabulation_nz, tabulation_zmin,
|
|
142
|
+
tabulation_nb_integration_points,
|
|
143
|
+
tabulation_cache_dir)
|
|
113
144
|
|
|
114
145
|
self.finite_depth_prony_decomposition_method = finite_depth_prony_decomposition_method
|
|
115
146
|
|
|
@@ -120,9 +151,10 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
120
151
|
'tabulation_nz': tabulation_nz,
|
|
121
152
|
'tabulation_zmin': tabulation_zmin,
|
|
122
153
|
'tabulation_nb_integration_points': tabulation_nb_integration_points,
|
|
123
|
-
'
|
|
154
|
+
'tabulation_grid_shape': tabulation_grid_shape,
|
|
124
155
|
'finite_depth_prony_decomposition_method': finite_depth_prony_decomposition_method,
|
|
125
156
|
'floating_point_precision': floating_point_precision,
|
|
157
|
+
'gf_singularities': gf_singularities,
|
|
126
158
|
}
|
|
127
159
|
|
|
128
160
|
self._hash = hash(self.exportable_settings.values())
|
|
@@ -152,7 +184,8 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
152
184
|
|
|
153
185
|
def _create_or_load_tabulation(self, tabulation_nr, tabulation_rmax,
|
|
154
186
|
tabulation_nz, tabulation_zmin,
|
|
155
|
-
tabulation_nb_integration_points
|
|
187
|
+
tabulation_nb_integration_points,
|
|
188
|
+
tabulation_cache_dir):
|
|
156
189
|
"""This method either:
|
|
157
190
|
- loads an existing tabulation saved on disk
|
|
158
191
|
- generates a new tabulation with the data provided as argument and save it on disk.
|
|
@@ -162,13 +195,12 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
162
195
|
tabulation_rmax = float(tabulation_rmax)
|
|
163
196
|
tabulation_zmin = float(tabulation_zmin)
|
|
164
197
|
|
|
165
|
-
filename = "tabulation_{}_{}_{}_{}_{}_{}_{}
|
|
166
|
-
self.
|
|
167
|
-
self.tabulation_method,
|
|
198
|
+
filename = "tabulation_{}_{}_{}_{}_{}_{}_{}.npz".format(
|
|
199
|
+
self.floating_point_precision, self.tabulation_grid_shape,
|
|
168
200
|
tabulation_nr, tabulation_rmax, tabulation_nz, tabulation_zmin,
|
|
169
201
|
tabulation_nb_integration_points
|
|
170
202
|
)
|
|
171
|
-
filepath = os.path.join(
|
|
203
|
+
filepath = os.path.join(tabulation_cache_dir, filename)
|
|
172
204
|
|
|
173
205
|
if os.path.exists(filepath):
|
|
174
206
|
LOG.info("Loading tabulation from %s", filepath)
|
|
@@ -178,22 +210,29 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
178
210
|
self.tabulated_integrals = loaded_arrays["values"]
|
|
179
211
|
|
|
180
212
|
else:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
self.tabulated_z_range = self.fortran_core.delhommeau_integrals.default_z_spacing(
|
|
186
|
-
tabulation_nz, tabulation_zmin, self.tabulation_method_index
|
|
187
|
-
)
|
|
188
|
-
self.tabulated_integrals = self.fortran_core.delhommeau_integrals.construct_tabulation(
|
|
189
|
-
self.tabulated_r_range, self.tabulated_z_range, tabulation_nb_integration_points
|
|
190
|
-
)
|
|
213
|
+
self._create_tabulation(tabulation_nr, tabulation_rmax,
|
|
214
|
+
tabulation_nz, tabulation_zmin,
|
|
215
|
+
tabulation_nb_integration_points)
|
|
191
216
|
LOG.debug("Saving tabulation in %s", filepath)
|
|
192
217
|
np.savez_compressed(
|
|
193
218
|
filepath, r_range=self.tabulated_r_range, z_range=self.tabulated_z_range,
|
|
194
219
|
values=self.tabulated_integrals
|
|
195
220
|
)
|
|
196
221
|
|
|
222
|
+
def _create_tabulation(self, tabulation_nr, tabulation_rmax,
|
|
223
|
+
tabulation_nz, tabulation_zmin,
|
|
224
|
+
tabulation_nb_integration_points):
|
|
225
|
+
LOG.warning("Precomputing tabulation, it may take a few seconds.")
|
|
226
|
+
self.tabulated_r_range = self.fortran_core.delhommeau_integrals.default_r_spacing(
|
|
227
|
+
tabulation_nr, tabulation_rmax, self.tabulation_grid_shape_index
|
|
228
|
+
)
|
|
229
|
+
self.tabulated_z_range = self.fortran_core.delhommeau_integrals.default_z_spacing(
|
|
230
|
+
tabulation_nz, tabulation_zmin, self.tabulation_grid_shape_index
|
|
231
|
+
)
|
|
232
|
+
self.tabulated_integrals = self.fortran_core.delhommeau_integrals.construct_tabulation(
|
|
233
|
+
self.tabulated_r_range, self.tabulated_z_range, tabulation_nb_integration_points,
|
|
234
|
+
)
|
|
235
|
+
|
|
197
236
|
@lru_cache(maxsize=128)
|
|
198
237
|
def find_best_exponential_decomposition(self, dimensionless_omega, dimensionless_wavenumber):
|
|
199
238
|
"""Compute the decomposition of a part of the finite water_depth Green function as a sum of exponential functions.
|
|
@@ -220,8 +259,8 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
220
259
|
the amplitude and growth rates of the exponentials
|
|
221
260
|
"""
|
|
222
261
|
|
|
223
|
-
LOG.debug(
|
|
224
|
-
|
|
262
|
+
LOG.debug("\tCompute Prony decomposition in finite water_depth Green function "
|
|
263
|
+
"for dimless_omega=%.2e and dimless_wavenumber=%.2e",
|
|
225
264
|
dimensionless_omega, dimensionless_wavenumber)
|
|
226
265
|
|
|
227
266
|
if self.finite_depth_prony_decomposition_method.lower() == 'python':
|
|
@@ -307,7 +346,10 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
307
346
|
elif wavenumber == np.inf:
|
|
308
347
|
coeffs = np.array((1.0, -1.0, 0.0))
|
|
309
348
|
else:
|
|
310
|
-
|
|
349
|
+
if self.gf_singularities == "high_freq":
|
|
350
|
+
coeffs = np.array((1.0, -1.0, 1.0))
|
|
351
|
+
else: # low_freq or low_freq_with_rankine_part
|
|
352
|
+
coeffs = np.array((1.0, 1.0, 1.0))
|
|
311
353
|
|
|
312
354
|
else: # Finite water_depth
|
|
313
355
|
if wavenumber == 0.0 or wavenumber == np.inf:
|
|
@@ -322,26 +364,37 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
322
364
|
if isinstance(mesh1, Mesh) or isinstance(mesh1, CollectionOfMeshes):
|
|
323
365
|
collocation_points = mesh1.faces_centers
|
|
324
366
|
nb_collocation_points = mesh1.nb_faces
|
|
325
|
-
if
|
|
326
|
-
early_dot_product_normals =
|
|
327
|
-
else:
|
|
367
|
+
if not adjoint_double_layer: # Computing the D matrix
|
|
368
|
+
early_dot_product_normals = mesh2.faces_normals
|
|
369
|
+
else: # Computing the K matrix
|
|
328
370
|
early_dot_product_normals = mesh1.faces_normals
|
|
329
|
-
|
|
371
|
+
|
|
372
|
+
elif isinstance(mesh1, np.ndarray) and mesh1.ndim == 2 and mesh1.shape[1] == 3:
|
|
330
373
|
# This is used when computing potential or velocity at given points in postprocessing
|
|
331
374
|
collocation_points = mesh1
|
|
332
375
|
nb_collocation_points = mesh1.shape[0]
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
376
|
+
if not adjoint_double_layer: # Computing the D matrix
|
|
377
|
+
early_dot_product_normals = mesh2.faces_normals
|
|
378
|
+
else: # Computing the K matrix
|
|
379
|
+
early_dot_product_normals = np.zeros((nb_collocation_points, 3))
|
|
380
|
+
# Dummy argument since this method is meant to be used either
|
|
381
|
+
# - to compute potential, then only S is needed and early_dot_product_normals is irrelevant,
|
|
382
|
+
# - to compute velocity, then the adjoint full gradient is needed and early_dot_product is False and this value is unused.
|
|
383
|
+
# TODO: add an only_S argument and return an error here if (early_dot_product and not only_S)
|
|
384
|
+
|
|
336
385
|
else:
|
|
337
|
-
raise ValueError(f"Unrecognized input for {self.__class__.__name__}.evaluate")
|
|
386
|
+
raise ValueError(f"Unrecognized first input for {self.__class__.__name__}.evaluate:\n{mesh1}")
|
|
387
|
+
|
|
388
|
+
if (np.any(abs(mesh2.faces_centers[:, 2]) < 1e-6) # free surface panel
|
|
389
|
+
and self.gf_singularities != "low_freq"):
|
|
390
|
+
raise NotImplementedError("Free surface panels are only supported for cpt.Delhommeau(..., gf_singularities='low_freq').")
|
|
338
391
|
|
|
339
392
|
if self.floating_point_precision == "float32":
|
|
340
393
|
dtype = "complex64"
|
|
341
394
|
elif self.floating_point_precision == "float64":
|
|
342
395
|
dtype = "complex128"
|
|
343
396
|
else:
|
|
344
|
-
raise NotImplementedError
|
|
397
|
+
raise NotImplementedError(f"Unsupported floating point precision: {self.floating_point_precision}")
|
|
345
398
|
|
|
346
399
|
S = np.empty((nb_collocation_points, mesh2.nb_faces), order="F", dtype=dtype)
|
|
347
400
|
K = np.empty((nb_collocation_points, mesh2.nb_faces, 1 if early_dot_product else 3), order="F", dtype=dtype)
|
|
@@ -355,9 +408,10 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
355
408
|
*mesh2.quadrature_points,
|
|
356
409
|
wavenumber, water_depth,
|
|
357
410
|
coeffs,
|
|
358
|
-
self.
|
|
411
|
+
self.tabulation_nb_integration_points, self.tabulation_grid_shape_index,
|
|
412
|
+
self.tabulated_r_range, self.tabulated_z_range, self.tabulated_integrals,
|
|
359
413
|
lamda_exp, a_exp,
|
|
360
|
-
mesh1 is mesh2, adjoint_double_layer,
|
|
414
|
+
mesh1 is mesh2, self.gf_singularities_index, adjoint_double_layer,
|
|
361
415
|
S, K
|
|
362
416
|
)
|
|
363
417
|
|
|
@@ -372,9 +426,7 @@ class Delhommeau(AbstractGreenFunction):
|
|
|
372
426
|
################################
|
|
373
427
|
|
|
374
428
|
class XieDelhommeau(Delhommeau):
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
Same arguments and methods as :class:`Delhommeau`.
|
|
378
|
-
"""
|
|
429
|
+
"""Legacy way to call the gf_singularities="low_freq" variant."""
|
|
379
430
|
|
|
380
|
-
|
|
431
|
+
def __init__(self, **kwargs):
|
|
432
|
+
super().__init__(gf_singularities="low_freq", **kwargs)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|