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.
Files changed (39) hide show
  1. capytaine/__about__.py +1 -1
  2. capytaine/__init__.py +10 -7
  3. capytaine/bem/engines.py +2 -2
  4. capytaine/bem/problems_and_results.py +17 -9
  5. capytaine/bem/solver.py +71 -28
  6. capytaine/bodies/bodies.py +133 -24
  7. capytaine/green_functions/delhommeau.py +103 -51
  8. capytaine/green_functions/libs/Delhommeau_float32.cp39-win_amd64.dll.a +0 -0
  9. capytaine/green_functions/libs/Delhommeau_float32.cp39-win_amd64.pyd +0 -0
  10. capytaine/green_functions/libs/Delhommeau_float64.cp39-win_amd64.dll.a +0 -0
  11. capytaine/green_functions/libs/Delhommeau_float64.cp39-win_amd64.pyd +0 -0
  12. capytaine/io/mesh_loaders.py +49 -24
  13. capytaine/io/meshio.py +4 -1
  14. capytaine/io/xarray.py +17 -7
  15. capytaine/matrices/block.py +4 -2
  16. capytaine/matrices/linear_solvers.py +2 -3
  17. capytaine/matrices/low_rank.py +3 -1
  18. capytaine/meshes/clipper.py +3 -3
  19. capytaine/meshes/collections.py +13 -2
  20. capytaine/meshes/meshes.py +128 -4
  21. capytaine/meshes/predefined/cylinders.py +2 -2
  22. capytaine/meshes/properties.py +77 -0
  23. capytaine/post_pro/rao.py +1 -1
  24. capytaine/tools/cache_on_disk.py +3 -1
  25. capytaine/tools/symbolic_multiplication.py +23 -4
  26. capytaine/ui/vtk/body_viewer.py +2 -0
  27. capytaine-2.2.1.dist-info/DELVEWHEEL +2 -0
  28. capytaine-2.2.1.dist-info/METADATA +754 -0
  29. {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/RECORD +33 -37
  30. capytaine/green_functions/libs/XieDelhommeau_float32.cp39-win_amd64.dll.a +0 -0
  31. capytaine/green_functions/libs/XieDelhommeau_float32.cp39-win_amd64.pyd +0 -0
  32. capytaine/green_functions/libs/XieDelhommeau_float64.cp39-win_amd64.dll.a +0 -0
  33. capytaine/green_functions/libs/XieDelhommeau_float64.cp39-win_amd64.pyd +0 -0
  34. capytaine-2.1.dist-info/DELVEWHEEL +0 -2
  35. capytaine-2.1.dist-info/METADATA +0 -756
  36. {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/LICENSE +0 -0
  37. {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/WHEEL +0 -0
  38. {capytaine-2.1.dist-info → capytaine-2.2.1.dist-info}/entry_points.txt +0 -0
  39. capytaine.libs/{.load-order-capytaine-2.1 → .load-order-capytaine-2.2.1} +2 -2
@@ -1,23 +1,25 @@
1
1
  """Floating bodies to be used in radiation-diffraction problems."""
2
- # Copyright (C) 2017-2019 Matthieu Ancellin
3
- # See LICENSE file at <https://github.com/mancellin/capytaine>
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.tools.optional_imports import silently_import_optional_dependency
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"FloatingBody(..., name={self.name}).{point_attr} = {getattr(self, point_attr)}")
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 divergence is None:
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: {non_rigid_dofs} are detected and \
630
- respective inertia coefficients are assigned as NaN.")
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([body.mesh for body in bodies], name=f"{name}_mesh")
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, dofs=dofs, mass=new_mass, center_of_mass=new_cog, name=name
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
- return FloatingBody(mesh=self.mesh.sliced_by_plane(plane), dofs=self.dofs, name=self.name)
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
- return (f"{self.__class__.__name__}(mesh={self.mesh.__short_str__()}, dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
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
- return (f"{self.__class__.__name__}(mesh={str(self.mesh)}, dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
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
- return 8*self.mesh.faces_radiuses.max()
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, dendrogram
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=1000,
27
- tabulation_method="scaled_nemoh3",
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
- tabulation_method: string, optional
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
- tabulation_method_index: int
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, 2, 2) and type floating_point_precision
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
- tabulation_method=_default_parameters["tabulation_method"],
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.floating_point_precision = floating_point_precision
114
+ self.fortran_core = import_module(f"capytaine.green_functions.libs.Delhommeau_{floating_point_precision}")
102
115
 
103
- self.fortran_core = import_module(f"capytaine.green_functions.libs.{self.fortran_core_basename}_{self.floating_point_precision}")
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.tabulation_method = tabulation_method
106
- fortran_indices_for_methods = {
107
- 'legacy': self.fortran_core.delhommeau_integrals.legacy_method,
108
- 'scaled_nemoh3': self.fortran_core.delhommeau_integrals.scaled_nemoh3_method,
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.tabulation_method_index = fortran_indices_for_methods[tabulation_method]
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._create_or_load_tabulation(tabulation_nr, tabulation_rmax, tabulation_nz, tabulation_zmin, tabulation_nb_integration_points)
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
- 'tabulation_method': tabulation_method,
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_{}_{}_{}_{}_{}_{}_{}_{}.npz".format(
166
- self.fortran_core_basename, self.floating_point_precision,
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(cache_directory(), filename)
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
- LOG.warning("Precomputing tabulation, it may take a few seconds.")
182
- self.tabulated_r_range = self.fortran_core.delhommeau_integrals.default_r_spacing(
183
- tabulation_nr, tabulation_rmax, self.tabulation_method_index
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(f"\tCompute Prony decomposition in finite water_depth Green function "
224
- f"for dimless_omega=%.2e and dimless_wavenumber=%.2e",
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
- coeffs = np.array((1.0, 1.0, 1.0))
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 ( adjoint_double_layer == False ):
326
- early_dot_product_normals = np.zeros((nb_collocation_points, 3)) # Should not be used
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
- elif isinstance(mesh1, np.ndarray) and mesh1.ndim ==2 and mesh1.shape[1] == 3:
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
- early_dot_product_normals = np.zeros((nb_collocation_points, 3)) # Should not be used
334
- if ( adjoint_double_layer == False ):
335
- raise NotImplementedError("Using a list of points as collocation points is not supported in computing adjoint double layer matrices.")
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.tabulation_method_index, self.tabulated_r_range, self.tabulated_z_range, self.tabulated_integrals,
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
- """Variant of Nemoh's Green function, more accurate near the free surface.
376
-
377
- Same arguments and methods as :class:`Delhommeau`.
378
- """
429
+ """Legacy way to call the gf_singularities="low_freq" variant."""
379
430
 
380
- fortran_core_basename = "XieDelhommeau"
431
+ def __init__(self, **kwargs):
432
+ super().__init__(gf_singularities="low_freq", **kwargs)