capytaine 2.0__cp38-cp38-win_amd64.whl → 2.2__cp38-cp38-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 (99) hide show
  1. capytaine/__about__.py +3 -3
  2. capytaine/__init__.py +16 -14
  3. capytaine/bem/airy_waves.py +4 -6
  4. capytaine/bem/engines.py +146 -25
  5. capytaine/bem/problems_and_results.py +217 -106
  6. capytaine/bem/solver.py +179 -47
  7. capytaine/bodies/__init__.py +1 -1
  8. capytaine/bodies/bodies.py +207 -39
  9. capytaine/bodies/predefined/__init__.py +0 -2
  10. capytaine/bodies/predefined/cylinders.py +0 -2
  11. capytaine/bodies/predefined/rectangles.py +0 -2
  12. capytaine/bodies/predefined/spheres.py +0 -2
  13. capytaine/green_functions/abstract_green_function.py +0 -3
  14. capytaine/green_functions/delhommeau.py +225 -63
  15. capytaine/green_functions/libs/Delhommeau_float32.cp38-win_amd64.dll.a +0 -0
  16. capytaine/green_functions/libs/Delhommeau_float32.cp38-win_amd64.pyd +0 -0
  17. capytaine/green_functions/libs/Delhommeau_float64.cp38-win_amd64.dll.a +0 -0
  18. capytaine/green_functions/libs/Delhommeau_float64.cp38-win_amd64.pyd +0 -0
  19. capytaine/io/bemio.py +17 -16
  20. capytaine/io/legacy.py +52 -20
  21. capytaine/io/mesh_loaders.py +49 -27
  22. capytaine/io/mesh_writers.py +1 -3
  23. capytaine/io/meshio.py +4 -1
  24. capytaine/io/xarray.py +73 -35
  25. capytaine/matrices/__init__.py +0 -2
  26. capytaine/matrices/block.py +23 -2
  27. capytaine/matrices/block_toeplitz.py +0 -2
  28. capytaine/matrices/builders.py +2 -4
  29. capytaine/matrices/linear_solvers.py +84 -7
  30. capytaine/matrices/low_rank.py +0 -2
  31. capytaine/meshes/__init__.py +0 -2
  32. capytaine/meshes/clipper.py +0 -3
  33. capytaine/meshes/collections.py +49 -20
  34. capytaine/meshes/geometry.py +3 -6
  35. capytaine/meshes/meshes.py +170 -81
  36. capytaine/meshes/predefined/__init__.py +0 -1
  37. capytaine/meshes/predefined/cylinders.py +48 -7
  38. capytaine/meshes/predefined/rectangles.py +43 -10
  39. capytaine/meshes/predefined/spheres.py +15 -4
  40. capytaine/meshes/properties.py +43 -2
  41. capytaine/meshes/quadratures.py +80 -0
  42. capytaine/meshes/quality.py +1 -3
  43. capytaine/meshes/surface_integrals.py +0 -1
  44. capytaine/meshes/symmetric.py +55 -14
  45. capytaine/post_pro/free_surfaces.py +4 -7
  46. capytaine/post_pro/impedance.py +12 -10
  47. capytaine/post_pro/kochin.py +5 -3
  48. capytaine/post_pro/rao.py +16 -22
  49. capytaine/tools/cache_on_disk.py +26 -0
  50. capytaine/tools/deprecation_handling.py +2 -2
  51. capytaine/tools/lists_of_points.py +13 -3
  52. capytaine/tools/lru_cache.py +23 -29
  53. capytaine/tools/optional_imports.py +0 -2
  54. capytaine/tools/prony_decomposition.py +0 -3
  55. capytaine/tools/symbolic_multiplication.py +107 -0
  56. capytaine/ui/cli.py +7 -27
  57. capytaine/ui/rich.py +5 -0
  58. capytaine/ui/vtk/__init__.py +0 -3
  59. capytaine/ui/vtk/animation.py +28 -8
  60. capytaine/ui/vtk/body_viewer.py +2 -2
  61. capytaine/ui/vtk/helpers.py +0 -3
  62. capytaine/ui/vtk/mesh_viewer.py +0 -3
  63. capytaine-2.2.dist-info/DELVEWHEEL +2 -0
  64. {capytaine-2.0.dist-info → capytaine-2.2.dist-info}/METADATA +32 -14
  65. capytaine-2.2.dist-info/RECORD +82 -0
  66. capytaine.libs/{.load-order-capytaine-2.0 → .load-order-capytaine-2.2} +2 -1
  67. capytaine.libs/libgcc_s_seh-1.dll +0 -0
  68. capytaine.libs/libgfortran-5.dll +0 -0
  69. capytaine.libs/libgomp-1.dll +0 -0
  70. capytaine.libs/libquadmath-0.dll +0 -0
  71. capytaine.libs/libwinpthread-1.dll +0 -0
  72. capytaine/green_functions/libDelhommeau/.gitignore +0 -5
  73. capytaine/green_functions/libDelhommeau/LICENSE +0 -203
  74. capytaine/green_functions/libDelhommeau/Makefile +0 -123
  75. capytaine/green_functions/libDelhommeau/README.md +0 -15
  76. capytaine/green_functions/libDelhommeau/benchmarks/openmp/benchmark_omp.f90 +0 -212
  77. capytaine/green_functions/libDelhommeau/benchmarks/openmp/display_mesh.py +0 -7
  78. capytaine/green_functions/libDelhommeau/benchmarks/openmp/read_output.py +0 -82
  79. capytaine/green_functions/libDelhommeau/benchmarks/profiling/benchmark_profiling.f90 +0 -201
  80. capytaine/green_functions/libDelhommeau/benchmarks/tabulations/benchmark_tabulation.f90 +0 -87
  81. capytaine/green_functions/libDelhommeau/examples/minimal/minimal_example.f90 +0 -213
  82. capytaine/green_functions/libDelhommeau/examples/minimal/minimal_example.py +0 -60
  83. capytaine/green_functions/libDelhommeau/src/Delhommeau_integrals.f90 +0 -311
  84. capytaine/green_functions/libDelhommeau/src/Green_Rankine.f90 +0 -148
  85. capytaine/green_functions/libDelhommeau/src/Green_wave.f90 +0 -303
  86. capytaine/green_functions/libDelhommeau/src/constants.f90 +0 -16
  87. capytaine/green_functions/libDelhommeau/src/float32.f90 +0 -7
  88. capytaine/green_functions/libDelhommeau/src/float64.f90 +0 -7
  89. capytaine/green_functions/libDelhommeau/src/matrices.f90 +0 -274
  90. capytaine/green_functions/libDelhommeau/src/old_Prony_decomposition.f90 +0 -636
  91. capytaine/green_functions/libs/XieDelhommeau_float32.cp38-win_amd64.dll.a +0 -0
  92. capytaine/green_functions/libs/XieDelhommeau_float32.cp38-win_amd64.pyd +0 -0
  93. capytaine/green_functions/libs/XieDelhommeau_float64.cp38-win_amd64.dll.a +0 -0
  94. capytaine/green_functions/libs/XieDelhommeau_float64.cp38-win_amd64.pyd +0 -0
  95. capytaine-2.0.dist-info/DELVEWHEEL +0 -2
  96. capytaine-2.0.dist-info/RECORD +0 -100
  97. {capytaine-2.0.dist-info → capytaine-2.2.dist-info}/LICENSE +0 -0
  98. {capytaine-2.0.dist-info → capytaine-2.2.dist-info}/WHEEL +0 -0
  99. {capytaine-2.0.dist-info → capytaine-2.2.dist-info}/entry_points.txt +0 -0
@@ -1,25 +1,25 @@
1
- #!/usr/bin/env python
2
- # coding: utf-8
3
1
  """Floating bodies to be used in radiation-diffraction problems."""
4
- # Copyright (C) 2017-2019 Matthieu Ancellin
5
- # 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>
6
4
 
7
5
  import logging
8
6
  import copy
9
- from itertools import chain, accumulate, product, zip_longest
7
+ from itertools import chain, accumulate, zip_longest
8
+ from functools import cached_property
10
9
 
11
10
  import numpy as np
12
11
  import xarray as xr
13
12
 
14
- from capytaine.tools.optional_imports import silently_import_optional_dependency
15
- meshio = silently_import_optional_dependency("meshio")
16
-
13
+ from capytaine.meshes.collections import CollectionOfMeshes
17
14
  from capytaine.meshes.geometry import Abstract3DObject, ClippableMixin, Plane, inplace_transformation
15
+ from capytaine.meshes.properties import connected_components, connected_components_of_waterline
18
16
  from capytaine.meshes.meshes import Mesh
19
17
  from capytaine.meshes.symmetric import build_regular_array_of_meshes
20
- from capytaine.meshes.collections import CollectionOfMeshes
21
18
  from capytaine.bodies.dofs import RigidBodyDofsPlaceholder
22
19
 
20
+ from capytaine.tools.optional_imports import silently_import_optional_dependency
21
+ meshio = silently_import_optional_dependency("meshio")
22
+
23
23
  LOG = logging.getLogger(__name__)
24
24
 
25
25
  TRANSLATION_DOFS_DIRECTIONS = {"surge": (1, 0, 0), "sway": (0, 1, 0), "heave": (0, 0, 1)}
@@ -40,8 +40,12 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
40
40
  Parameters
41
41
  ----------
42
42
  mesh : Mesh or CollectionOfMeshes, optional
43
- the mesh describing the geometry of the floating body.
43
+ the mesh describing the geometry of the hull of the floating body.
44
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.
45
49
  dofs : dict, optional
46
50
  the degrees of freedom of the body.
47
51
  If none is given, a empty dictionary is initialized.
@@ -57,7 +61,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
57
61
  If none is given, the one of the mesh is used.
58
62
  """
59
63
 
60
- 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):
61
65
  if mesh is None:
62
66
  self.mesh = Mesh(name="dummy_mesh")
63
67
 
@@ -71,6 +75,11 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
71
75
  else:
72
76
  raise TypeError("Unrecognized `mesh` object passed to the FloatingBody constructor.")
73
77
 
78
+ if lid_mesh is not None:
79
+ self.lid_mesh = lid_mesh.with_normal_vector_going_down(inplace=False)
80
+ else:
81
+ self.lid_mesh = None
82
+
74
83
  if name is None and mesh is None:
75
84
  self.name = "dummy_body"
76
85
  elif name is None:
@@ -84,6 +93,9 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
84
93
  else:
85
94
  self.center_of_mass = None
86
95
 
96
+ if self.mesh.nb_vertices > 0 and self.mesh.nb_faces > 0:
97
+ self.mesh.heal_mesh()
98
+
87
99
  if dofs is None:
88
100
  self.dofs = {}
89
101
  elif isinstance(dofs, RigidBodyDofsPlaceholder):
@@ -94,21 +106,19 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
94
106
  else:
95
107
  self.dofs = dofs
96
108
 
97
- if self.mesh.nb_vertices == 0 or self.mesh.nb_faces == 0:
98
- LOG.warning(f"New floating body (with empty mesh!): {self.name}.")
99
- else:
100
- self.mesh.heal_mesh()
101
- LOG.info(f"New floating body: {self.name}.")
109
+ LOG.info(f"New floating body: {self.__str__()}.")
102
110
 
103
111
  @staticmethod
104
112
  def from_meshio(mesh, name=None) -> 'FloatingBody':
105
- """Create a FloatingBody from a meshio mesh object."""
113
+ """Create a FloatingBody from a meshio mesh object.
114
+ Kinda deprecated, use cpt.load_mesh instead."""
106
115
  from capytaine.io.meshio import load_from_meshio
107
116
  return FloatingBody(mesh=load_from_meshio(mesh, name), name=name)
108
117
 
109
118
  @staticmethod
110
119
  def from_file(filename: str, file_format=None, name=None) -> 'FloatingBody':
111
- """Create a FloatingBody from a mesh file using meshmagick."""
120
+ """Create a FloatingBody from a mesh file using meshmagick.
121
+ Kinda deprecated, use cpt.load_mesh instead."""
112
122
  from capytaine.io.mesh_loaders import load_mesh
113
123
  if name is None: name = filename
114
124
  mesh = load_mesh(filename, file_format, name=f"{name}_mesh")
@@ -118,6 +128,13 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
118
128
  """Arbitrary order. The point is to sort together the problems involving the same body."""
119
129
  return self.name < other.name
120
130
 
131
+ @cached_property
132
+ def mesh_including_lid(self):
133
+ if self.lid_mesh is not None:
134
+ return CollectionOfMeshes([self.mesh, self.lid_mesh])
135
+ else:
136
+ return self.mesh
137
+
121
138
  ##########
122
139
  # Dofs #
123
140
  ##########
@@ -176,7 +193,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
176
193
  if hasattr(self, point_attr) and getattr(self, point_attr) is not None:
177
194
  axis_point = getattr(self, point_attr)
178
195
  LOG.info(f"The rotation dof {name} has been initialized around the point: "
179
- f"{self.name}.{point_attr} = {getattr(self, point_attr)}")
196
+ f"{self.__short_str__()}.{point_attr} = {getattr(self, point_attr)}")
180
197
  break
181
198
  else:
182
199
  axis_point = np.array([0, 0, 0])
@@ -270,7 +287,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
270
287
  """Returns volume of the FloatingBody."""
271
288
  return self.mesh.volume
272
289
 
273
- def disp_mass(self, *, rho=1000):
290
+ def disp_mass(self, *, rho=1000.0):
274
291
  return self.mesh.disp_mass(rho=rho)
275
292
 
276
293
  @property
@@ -351,7 +368,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
351
368
  except Exception as e:
352
369
  raise ValueError(
353
370
  f"Failed to infer the rotation center of {self.name} to compute rigid body hydrostatics.\n"
354
- f"Possible fix: add a `rotation_center` attibute to {self.name}.\n"
371
+ f"Possible fix: add a `rotation_center` attribute to {self.name}.\n"
355
372
  "Note that rigid body hydrostatic methods currently assume that the three rotation dofs have the same rotation center."
356
373
  ) from e
357
374
 
@@ -444,10 +461,10 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
444
461
  else:
445
462
  norm_hs_stiff = 0.0
446
463
  else:
447
- if self.mass is not None and np.isclose(self.mass, self.disp_mass(rho), rtol=1e-4):
464
+ if self.mass is not None and not np.isclose(self.mass, self.disp_mass(rho=rho), rtol=1e-4):
448
465
  raise NotImplementedError(
449
466
  f"Trying to compute the hydrostatic stiffness for dofs {radiating_dof_name} and {influenced_dof_name}"
450
- f"of body {self.name}, which is not neutrally buoyant (mass={body.mass}, disp_mass={body.disp_mass(rho)}.\n"
467
+ f"of body {self.name}, which is not neutrally buoyant (mass={self.mass}, disp_mass={self.disp_mass(rho=rho)}).\n"
451
468
  f"This case has not been implemented in Capytaine. You need either a single rigid body or a neutrally buoyant body."
452
469
  )
453
470
 
@@ -515,7 +532,9 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
515
532
  raise AttributeError("Cannot compute hydrostatics stiffness on {} since no dof has been defined.".format(self.name))
516
533
 
517
534
  def divergence_dof(influenced_dof):
518
- if divergence is None:
535
+ if influenced_dof.lower() in [*TRANSLATION_DOFS_DIRECTIONS, *ROTATION_DOFS_AXIS]:
536
+ return 0.0 # Dummy value that is not actually used afterwards.
537
+ elif divergence is None:
519
538
  return 0.0
520
539
  elif isinstance(divergence, dict) and influenced_dof in divergence.keys():
521
540
  return divergence[influenced_dof]
@@ -539,7 +558,7 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
539
558
  K = hs_set.hydrostatic_stiffness.sel(influenced_dof=list(self.dofs.keys()), radiating_dof=list(self.dofs.keys()))
540
559
  return K
541
560
 
542
- def compute_rigid_body_inertia(self, *, rho=1000, output_type="body_dofs"):
561
+ def compute_rigid_body_inertia(self, *, rho=1000.0, output_type="body_dofs"):
543
562
  """
544
563
  Inertia Mass matrix of the body for 6 rigid DOFs.
545
564
 
@@ -629,8 +648,8 @@ class FloatingBody(ClippableMixin, Abstract3DObject):
629
648
 
630
649
  if output_type == "body_dofs":
631
650
  if len(non_rigid_dofs) > 0:
632
- LOG.warning(f"Non-rigid dofs: {non_rigid_dofs} are detected and \
633
- respective inertia coefficients are assigned as NaN.")
651
+ LOG.warning(f"Non-rigid dofs {non_rigid_dofs} detected: their \
652
+ inertia coefficients are assigned as NaN.")
634
653
 
635
654
  inertia_matrix_xr = total_mass_xr.sel(influenced_dof=body_dof_names,
636
655
  radiating_dof=body_dof_names)
@@ -732,7 +751,17 @@ respective inertia coefficients are assigned as NaN.")
732
751
  def join_bodies(*bodies, name=None) -> 'FloatingBody':
733
752
  if name is None:
734
753
  name = "+".join(body.name for body in bodies)
735
- meshes = CollectionOfMeshes([body.mesh for body in bodies], name=f"{name}_mesh")
754
+ meshes = CollectionOfMeshes(
755
+ [body.mesh for body in bodies],
756
+ name=f"{name}_mesh"
757
+ )
758
+ if all(body.lid_mesh is None for body in bodies):
759
+ lid_meshes = None
760
+ else:
761
+ lid_meshes = CollectionOfMeshes(
762
+ [body.lid_mesh for body in bodies if body.lid_mesh is not None],
763
+ name=f"{name}_lid_mesh"
764
+ )
736
765
  dofs = FloatingBody.combine_dofs(bodies)
737
766
 
738
767
  if all(body.mass is not None for body in bodies):
@@ -747,7 +776,8 @@ respective inertia coefficients are assigned as NaN.")
747
776
  new_cog = None
748
777
 
749
778
  joined_bodies = FloatingBody(
750
- mesh=meshes, dofs=dofs, mass=new_mass, center_of_mass=new_cog, name=name
779
+ mesh=meshes, lid_mesh=lid_meshes, dofs=dofs,
780
+ mass=new_mass, center_of_mass=new_cog, name=name
751
781
  )
752
782
 
753
783
  for matrix_name in ["inertia_matrix", "hydrostatic_stiffness"]:
@@ -823,7 +853,6 @@ respective inertia coefficients are assigned as NaN.")
823
853
  if not isinstance(locations, np.ndarray):
824
854
  raise TypeError('locations must be of type np.ndarray')
825
855
  assert locations.shape[1] == 2, 'locations must be of shape nx2, received {:}'.format(locations.shape)
826
- n = locations.shape[0]
827
856
 
828
857
  fb_list = []
829
858
  for idx, li in enumerate(locations):
@@ -839,6 +868,7 @@ respective inertia coefficients are assigned as NaN.")
839
868
  def extract_faces(self, id_faces_to_extract, return_index=False):
840
869
  """Create a new FloatingBody by extracting some faces from the mesh.
841
870
  The dofs evolve accordingly.
871
+ The lid_mesh, center_of_mass, mass and hydrostatics data are discarded.
842
872
  """
843
873
  if isinstance(self.mesh, CollectionOfMeshes):
844
874
  raise NotImplementedError # TODO
@@ -860,7 +890,12 @@ respective inertia coefficients are assigned as NaN.")
860
890
  return new_body
861
891
 
862
892
  def sliced_by_plane(self, plane):
863
- return FloatingBody(mesh=self.mesh.sliced_by_plane(plane), dofs=self.dofs, name=self.name)
893
+ """Return the same body, but replace the mesh by a set of two meshes
894
+ corresponding to each sides of the plane."""
895
+ return FloatingBody(mesh=self.mesh.sliced_by_plane(plane),
896
+ lid_mesh=self.lid_mesh.sliced_by_plane(plane)
897
+ if self.lid_mesh is not None else None,
898
+ dofs=self.dofs, name=self.name)
864
899
 
865
900
  def minced(self, nb_slices=(8, 8, 4)):
866
901
  """Experimental method decomposing the mesh as a hierarchical structure.
@@ -921,27 +956,35 @@ respective inertia coefficients are assigned as NaN.")
921
956
  @inplace_transformation
922
957
  def mirror(self, plane):
923
958
  self.mesh.mirror(plane)
959
+ if self.lid_mesh is not None:
960
+ self.lid_mesh.mirror(plane)
924
961
  for dof in self.dofs:
925
962
  self.dofs[dof] -= 2 * np.outer(np.dot(self.dofs[dof], plane.normal), plane.normal)
926
963
  for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
927
964
  if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
928
- self.__dict__[point_attr] -= 2 * (np.dot(self.__dict__[point_attr], plane.normal) - plane.c) * plane.normal
965
+ point = np.array(self.__dict__[point_attr])
966
+ shift = - 2 * (np.dot(point, plane.normal) - plane.c) * plane.normal
967
+ self.__dict__[point_attr] = point + shift
929
968
  return self
930
969
 
931
970
  @inplace_transformation
932
971
  def translate(self, vector, *args, **kwargs):
933
972
  self.mesh.translate(vector, *args, **kwargs)
973
+ if self.lid_mesh is not None:
974
+ self.lid_mesh.translate(vector, *args, **kwargs)
934
975
  for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
935
976
  if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
936
- self.__dict__[point_attr] += vector
977
+ self.__dict__[point_attr] = np.array(self.__dict__[point_attr]) + vector
937
978
  return self
938
979
 
939
980
  @inplace_transformation
940
981
  def rotate(self, axis, angle):
941
982
  self.mesh.rotate(axis, angle)
983
+ if self.lid_mesh is not None:
984
+ self.lid_mesh.rotate(axis, angle)
942
985
  for point_attr in ('geometric_center', 'rotation_center', 'center_of_mass'):
943
986
  if point_attr in self.__dict__ and self.__dict__[point_attr] is not None:
944
- self.__dict__[point_attr] = axis.rotate_points([self.__dict__[point_attr]], angle)
987
+ self.__dict__[point_attr] = axis.rotate_points([self.__dict__[point_attr]], angle)[0, :]
945
988
  for dof in self.dofs:
946
989
  self.dofs[dof] = axis.rotate_vectors(self.dofs[dof], angle)
947
990
  return self
@@ -951,6 +994,8 @@ respective inertia coefficients are assigned as NaN.")
951
994
  # Clip mesh
952
995
  LOG.info(f"Clipping {self.name} with respect to {plane}")
953
996
  self.mesh.clip(plane)
997
+ if self.lid_mesh is not None:
998
+ self.lid_mesh.clip(plane)
954
999
 
955
1000
  # Clip dofs
956
1001
  ids = self.mesh._clipping_data['faces_ids']
@@ -966,15 +1011,52 @@ respective inertia coefficients are assigned as NaN.")
966
1011
  # Display #
967
1012
  #############
968
1013
 
1014
+ def __short_str__(self):
1015
+ return (f"{self.__class__.__name__}(..., name=\"{self.name}\")")
1016
+
1017
+ def _optional_params_str(self):
1018
+ items = []
1019
+ if self.mass is not None: items.append(f"mass={self.mass}, ")
1020
+ if self.center_of_mass is not None: items.append(f"center_of_mass={self.center_of_mass}, ")
1021
+ return ''.join(items)
1022
+
969
1023
  def __str__(self):
970
- return self.name
1024
+ short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
1025
+
1026
+ if self.lid_mesh is not None:
1027
+ lid_mesh_str = self.lid_mesh.__short_str__()
1028
+ else:
1029
+ lid_mesh_str = str(None)
1030
+
1031
+ return (f"{self.__class__.__name__}(mesh={self.mesh.__short_str__()}, lid_mesh={lid_mesh_str}, "
1032
+ f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
971
1033
 
972
1034
  def __repr__(self):
973
- return (f"{self.__class__.__name__}(mesh={self.mesh.name}, "
974
- f"dofs={{{', '.join(self.dofs.keys())}}}, name={self.name})")
1035
+ short_dofs = '{' + ', '.join('"{}": ...'.format(d) for d in self.dofs) + '}'
1036
+
1037
+ if self.lid_mesh is not None:
1038
+ lid_mesh_str = str(self.lid_mesh)
1039
+ else:
1040
+ lid_mesh_str = str(None)
1041
+
1042
+ return (f"{self.__class__.__name__}(mesh={str(self.mesh)}, lid_mesh={lid_mesh_str}, "
1043
+ f"dofs={short_dofs}, {self._optional_params_str()}name=\"{self.name}\")")
975
1044
 
976
1045
  def _repr_pretty_(self, p, cycle):
977
- p.text(self.__repr__())
1046
+ p.text(self.__str__())
1047
+
1048
+ def __rich_repr__(self):
1049
+ class DofWithShortRepr:
1050
+ def __repr__(self):
1051
+ return '...'
1052
+ yield "mesh", self.mesh
1053
+ yield "lid_mesh", self.lid_mesh
1054
+ yield "dofs", {d: DofWithShortRepr() for d in self.dofs}
1055
+ if self.mass is not None:
1056
+ yield "mass", self.mass, None
1057
+ if self.center_of_mass is not None:
1058
+ yield "center_of_mass", tuple(self.center_of_mass)
1059
+ yield "name", self.name
978
1060
 
979
1061
  def show(self, **kwargs):
980
1062
  from capytaine.ui.vtk.body_viewer import FloatingBodyViewer
@@ -1013,5 +1095,91 @@ respective inertia coefficients are assigned as NaN.")
1013
1095
  @property
1014
1096
  def minimal_computable_wavelength(self):
1015
1097
  """For accuracy of the resolution, wavelength should not be smaller than this value."""
1016
- return 8*self.mesh.faces_radiuses.max()
1098
+ if self.lid_mesh is not None:
1099
+ return max(8*self.mesh.faces_radiuses.max(), 8*self.lid_mesh.faces_radiuses.max())
1100
+ else:
1101
+ return 8*self.mesh.faces_radiuses.max()
1102
+
1103
+ def first_irregular_frequency_estimate(self, *, g=9.81):
1104
+ r"""Estimates the angular frequency of the lowest irregular
1105
+ frequency.
1106
+ This is based on the formula for the lowest irregular frequency of a
1107
+ parallelepiped of size :math:`L \times B` and draft :math:`H`:
1108
+
1109
+ .. math::
1110
+ \omega = \sqrt{
1111
+ \frac{\pi g \sqrt{\frac{1}{B^2} + \frac{1}{L^2}}}
1112
+ {\tanh\left(\pi H \sqrt{\frac{1}{B^2} + \frac{1}{L^2}} \right)}
1113
+ }
1114
+
1115
+ The formula is applied to all shapes to get an estimate that is usually
1116
+ conservative.
1117
+ The definition of a lid (supposed to be fully covering and horizontal)
1118
+ is taken into account.
1119
+ """
1120
+ if self.lid_mesh is None:
1121
+ draft = abs(self.mesh.vertices[:, 2].min())
1122
+ else:
1123
+ draft = abs(self.lid_mesh.vertices[:, 2].min())
1124
+ if draft < 1e-6:
1125
+ return np.inf
1126
+
1127
+ # Look for the x and y span of each components (e.g. for multibody) and
1128
+ # keep the one causing the lowest irregular frequency.
1129
+ # The draft is supposed to be same for all components.
1130
+ omega = np.inf
1131
+ for comp in connected_components(self.mesh):
1132
+ for ccomp in connected_components_of_waterline(comp):
1133
+ x_span = ccomp.vertices[:, 0].max() - ccomp.vertices[:, 0].min()
1134
+ y_span = ccomp.vertices[:, 1].max() - ccomp.vertices[:, 1].min()
1135
+ p = np.hypot(1/x_span, 1/y_span)
1136
+ omega_comp = np.sqrt(np.pi*g*p/(np.tanh(np.pi*draft*p)))
1137
+ omega = min(omega, omega_comp)
1138
+ return omega
1139
+
1140
+ def cluster_bodies(*bodies, name=None):
1141
+ """
1142
+ Builds a hierarchical clustering from a group of bodies
1143
+
1144
+ Parameters
1145
+ ----------
1146
+ bodies: list
1147
+ a list of bodies
1148
+ name: str, optional
1149
+ a name for the new body
1150
+
1151
+ Returns
1152
+ -------
1153
+ FloatingBody
1154
+ Array built from the provided bodies
1155
+ """
1156
+ from scipy.cluster.hierarchy import linkage
1157
+ nb_buoys = len(bodies)
1158
+
1159
+ if any(body.center_of_buoyancy is None for body in bodies):
1160
+ raise ValueError("The center of buoyancy of each body needs to be known for clustering")
1161
+ buoys_positions = np.stack([body.center_of_buoyancy for body in bodies])[:,:2]
1162
+
1163
+ ln_matrix = linkage(buoys_positions, method='centroid', metric='euclidean')
1164
+
1165
+ node_list = list(bodies) # list of nodes of the tree: the first nodes are single bodies
1166
+
1167
+ # Join the bodies, with an ordering consistent with the dendrogram.
1168
+ # Done by reading the linkage matrix: its i-th row contains the labels
1169
+ # of the two nodes that are merged to form the (n + i)-th node
1170
+ for ii in range(len(ln_matrix)):
1171
+ node_tag = ii + nb_buoys # the first nb_buoys tags are already taken
1172
+ merge_left = int(ln_matrix[ii,0])
1173
+ merge_right = int(ln_matrix[ii,1])
1174
+ # The new node is the parent of merge_left and merge_right
1175
+ new_node_ls = [node_list[merge_left], node_list[merge_right]]
1176
+ new_node = FloatingBody.join_bodies(*new_node_ls, name='node_{:d}'.format(node_tag))
1177
+ node_list.append(new_node)
1178
+
1179
+ # The last node is the parent of all others
1180
+ all_buoys = new_node
1181
+
1182
+ if name is not None:
1183
+ all_buoys.name = name
1017
1184
 
1185
+ return all_buoys
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env python
2
- # coding: utf-8
3
1
  # Copyright (C) 2017-2019 Matthieu Ancellin
4
2
  # See LICENSE file at <https://github.com/mancellin/capytaine>
5
3
 
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env python
2
- # coding: utf-8
3
1
  """Legacy interfaces to predefined meshes"""
4
2
  # Copyright (C) 2017-2022 Matthieu Ancellin
5
3
  # See LICENSE file at <https://github.com/capytaine/capytaine>
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env python
2
- # coding: utf-8
3
1
  """Legacy interfaces to predefined meshes"""
4
2
  # Copyright (C) 2017-2022 Matthieu Ancellin
5
3
  # See LICENSE file at <https://github.com/capytaine/capytaine>
@@ -68,5 +68,3 @@ class Sphere(FloatingBody):
68
68
  self.radius = radius
69
69
  self.geometric_center = np.array(center, dtype=float)
70
70
  FloatingBody.__init__(self, mesh=mesh, name=name)
71
-
72
-
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env python
2
- # coding: utf-8
3
1
  """Abstract structure of a class used to compute the Green function"""
4
2
  # Copyright (C) 2017-2019 Matthieu Ancellin
5
3
  # See LICENSE file at <https://github.com/mancellin/capytaine>
@@ -12,4 +10,3 @@ class AbstractGreenFunction(ABC):
12
10
  @abstractmethod
13
11
  def evaluate(self, mesh1, mesh2, free_surface, sea_bottom, wavenumber):
14
12
  pass
15
-