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,5 +1,3 @@
1
- #!/usr/bin/env python
2
- # coding: utf-8
3
1
  """Definition of the problems to solve with the BEM solver, and the results of this resolution."""
4
2
  # Copyright (C) 2017-2023 Matthieu Ancellin
5
3
  # See LICENSE file at <https://github.com/capytaine/capytaine>
@@ -13,12 +11,14 @@ from scipy.optimize import newton
13
11
  from capytaine.tools.deprecation_handling import _get_water_depth
14
12
  from capytaine.meshes.collections import CollectionOfMeshes
15
13
  from capytaine.bem.airy_waves import airy_waves_velocity, froude_krylov_force
14
+ from capytaine.tools.symbolic_multiplication import SymbolicMultiplication
16
15
 
17
16
  LOG = logging.getLogger(__name__)
18
17
 
19
18
  _default_parameters = {'rho': 1000.0, 'g': 9.81, 'omega': 1.0,
20
- 'free_surface': 0.0, 'water_depth': np.infty,
21
- 'wave_direction': 0.0}
19
+ 'free_surface': 0.0, 'water_depth': np.inf,
20
+ 'wave_direction': 0.0, 'forward_speed': 0.0}
21
+
22
22
 
23
23
 
24
24
  class LinearPotentialFlowProblem:
@@ -32,9 +32,9 @@ class LinearPotentialFlowProblem:
32
32
  body: FloatingBody, optional
33
33
  The body interacting with the waves
34
34
  free_surface: float, optional
35
- The position of the free surface (accepted values: 0 and np.infty)
35
+ The position of the free surface (accepted values: 0 and np.inf)
36
36
  water_depth: float, optional
37
- The depth of water in m (default: np.infty)
37
+ The depth of water in m (default: np.inf)
38
38
  sea_bottom: float, optional
39
39
  The position of the sea bottom (deprecated: please prefer setting water_depth)
40
40
  omega: float, optional
@@ -45,6 +45,8 @@ class LinearPotentialFlowProblem:
45
45
  The angular wave number of the waves in rad/m
46
46
  wavelength: float, optional
47
47
  The wave length of the waves in m
48
+ forward_speed: float, optional
49
+ The speed of the body (in m/s, in the x direction, default: 0.0)
48
50
  rho: float, optional
49
51
  The density of water in kg/m3 (default: 1000.0)
50
52
  g: float, optional
@@ -58,24 +60,45 @@ class LinearPotentialFlowProblem:
58
60
  free_surface=_default_parameters['free_surface'],
59
61
  water_depth=None, sea_bottom=None,
60
62
  omega=None, period=None, wavenumber=None, wavelength=None,
63
+ forward_speed=_default_parameters['forward_speed'],
61
64
  rho=_default_parameters['rho'],
62
65
  g=_default_parameters['g'],
66
+ wave_direction=_default_parameters['wave_direction'],
63
67
  boundary_condition=None):
64
68
 
65
69
  self.body = body
66
70
  self.free_surface = float(free_surface)
67
71
  self.rho = float(rho)
68
72
  self.g = float(g)
73
+ self.forward_speed = float(forward_speed)
74
+ self.wave_direction = float(wave_direction) # Required for (diffraction problem) and (radiation problems with forward speed).
69
75
 
70
76
  self.boundary_condition = boundary_condition
71
77
 
72
78
  self.water_depth = _get_water_depth(free_surface, water_depth, sea_bottom, default_water_depth=_default_parameters["water_depth"])
73
79
  self.omega, self.period, self.wavenumber, self.wavelength, self.provided_freq_type = \
74
- self._get_frequencies(omega, period, wavenumber, wavelength)
80
+ self._get_frequencies(omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength)
75
81
 
76
82
  self._check_data()
77
83
 
78
- def _get_frequencies(self, omega, period, wavenumber, wavelength):
84
+ if forward_speed != 0.0:
85
+ dopplered_omega = self.omega - self.wavenumber*self.forward_speed*np.cos(self.wave_direction)
86
+ self.encounter_omega, self.encounter_period, self.encounter_wavenumber, self.encounter_wavelength, _ = \
87
+ self._get_frequencies(omega=abs(dopplered_omega))
88
+
89
+ if dopplered_omega >= 0.0:
90
+ self.encounter_wave_direction = self.wave_direction
91
+ else:
92
+ self.encounter_wave_direction = self.wave_direction + np.pi
93
+ else:
94
+ self.encounter_omega = self.omega
95
+ self.encounter_period = self.period
96
+ self.encounter_wavenumber = self.wavenumber
97
+ self.encounter_wavelength = self.wavelength
98
+ self.encounter_wave_direction = self.wave_direction
99
+
100
+
101
+ def _get_frequencies(self, *, omega=None, period=None, wavenumber=None, wavelength=None):
79
102
  frequency_data = dict(omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength)
80
103
  nb_provided_frequency_data = 4 - list(frequency_data.values()).count(None)
81
104
 
@@ -87,87 +110,115 @@ class LinearPotentialFlowProblem:
87
110
  provided_freq_type = 'omega'
88
111
  frequency_data = {'omega': _default_parameters['omega']}
89
112
  else:
90
- provided_freq_type = [k for k, v in frequency_data.items() if v is not None][0]
113
+ provided_freq_type = [k for (k, v) in frequency_data.items() if v is not None][0]
114
+
115
+ if ((float(frequency_data[provided_freq_type]) == 0.0 and provided_freq_type in {'omega', 'wavenumber'})
116
+ or (float(frequency_data[provided_freq_type]) == np.inf and provided_freq_type in {'period', 'wavelength'})):
117
+ omega = SymbolicMultiplication("0")
118
+ wavenumber = SymbolicMultiplication("0")
119
+ period = SymbolicMultiplication("∞")
120
+ wavelength = SymbolicMultiplication("∞")
121
+ elif ((float(frequency_data[provided_freq_type]) == 0.0 and provided_freq_type in {'period', 'wavelength'})
122
+ or (float(frequency_data[provided_freq_type]) == np.inf and provided_freq_type in {'omega', 'wavenumber'})):
123
+ omega = SymbolicMultiplication("∞")
124
+ wavenumber = SymbolicMultiplication("∞")
125
+ period = SymbolicMultiplication("0")
126
+ wavelength = SymbolicMultiplication("0")
127
+ else:
91
128
 
92
- if frequency_data[provided_freq_type] in {0.0, np.infty}:
93
- raise NotImplementedError("Zero and infinite frequencies are currently not supported.")
129
+ if provided_freq_type in {'omega', 'period'}:
130
+ if provided_freq_type == 'omega':
131
+ omega = frequency_data['omega']
132
+ period = 2*np.pi/omega
133
+ else: # provided_freq_type is 'period'
134
+ period = frequency_data['period']
135
+ omega = 2*np.pi/period
136
+
137
+ if self.water_depth == np.inf:
138
+ wavenumber = omega**2/self.g
139
+ else:
140
+ wavenumber = newton(lambda k: k*np.tanh(k*self.water_depth) - omega**2/self.g, x0=1.0)
141
+ wavelength = 2*np.pi/wavenumber
94
142
 
143
+ else: # provided_freq_type is 'wavelength' or 'wavenumber'
144
+ if provided_freq_type == 'wavelength':
145
+ wavelength = frequency_data['wavelength']
146
+ wavenumber = 2*np.pi/wavelength
147
+ else: # provided_freq_type is 'wavenumber'
148
+ wavenumber = frequency_data['wavenumber']
149
+ wavelength = 2*np.pi/wavenumber
95
150
 
96
- if provided_freq_type in {'omega', 'period'}:
97
- if provided_freq_type == 'omega':
98
- omega = frequency_data['omega']
151
+ omega = np.sqrt(self.g*wavenumber*np.tanh(wavenumber*self.water_depth))
99
152
  period = 2*np.pi/omega
100
- else: # provided_freq_type is 'period'
101
- period = frequency_data['period']
102
- omega = 2*np.pi/period
103
-
104
- if self.water_depth == np.infty:
105
- wavenumber = omega**2/self.g
106
- else:
107
- wavenumber = newton(lambda k: k*np.tanh(k*self.water_depth) - omega**2/self.g, x0=1.0)
108
- wavelength = 2*np.pi/wavenumber
109
-
110
- else: # provided_freq_type is 'wavelength' or 'wavenumber'
111
- if provided_freq_type == 'wavelength':
112
- wavelength = frequency_data['wavelength']
113
- wavenumber = 2*np.pi/wavelength
114
- else: # provided_freq_type is 'wavenumber'
115
- wavenumber = frequency_data['wavenumber']
116
- wavelength = 2*np.pi/wavenumber
117
-
118
- omega = np.sqrt(self.g*wavenumber*np.tanh(wavenumber*self.water_depth))
119
- period = 2*np.pi/omega
120
153
 
121
154
  return omega, period, wavenumber, wavelength, provided_freq_type
122
155
 
123
156
  def _check_data(self):
124
157
  """Sanity checks on the data."""
125
158
 
126
- if self.free_surface not in {0.0, np.infty}:
159
+ if self.free_surface not in {0.0, np.inf}:
127
160
  raise NotImplementedError(
128
161
  f"Free surface is {self.free_surface}. "
129
162
  "Only z=0 and z=∞ are accepted values for the free surface position."
130
163
  )
131
164
 
132
- if self.free_surface == np.infty and self.water_depth != np.infty:
165
+ if not (-2*np.pi-1e-3 <= self.wave_direction <= 2*np.pi+1e-3):
166
+ LOG.warning(f"The value {self.wave_direction} has been provided for the wave direction, and it does not look like an angle in radians. "
167
+ "The wave direction in Capytaine is defined in radians and not in degrees, so the result might not be what you expect. "
168
+ "If you were actually giving an angle in radians, use the modulo operator to give a value between -2π and 2π to disable this warning.")
169
+
170
+ if self.free_surface == np.inf and self.water_depth != np.inf:
171
+
133
172
  raise NotImplementedError(
134
173
  "Problems with a sea bottom but no free surface have not been implemented."
135
174
  )
136
175
 
137
176
  if self.water_depth < 0.0:
138
- raise ValueError("`water_depth` should be stricly positive.")
177
+ raise ValueError("`water_depth` should be strictly positive (provided water depth: {self.water_depth}).")
178
+
179
+ if float(self.omega) in {0, np.inf}:
180
+ if self.water_depth != np.inf:
181
+ LOG.warning(
182
+ f"Default Green function allows for {self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type))} only for infinite depth (provided water depth: {self.water_depth})."
183
+ )
184
+
185
+ if self.forward_speed != 0.0:
186
+ raise NotImplementedError(
187
+ f"omega={float(self.omega)} is only implemented without forward speed (provided forward speed: {self.forward_speed})."
188
+ )
139
189
 
140
- if self.omega in {0, np.infty} and self.water_depth != np.infty:
141
- raise NotImplementedError(
142
- f"omega={self.omega} is only implemented for infinite depth."
143
- )
144
190
 
145
191
  if self.body is not None:
146
192
  if ((isinstance(self.body.mesh, CollectionOfMeshes) and len(self.body.mesh) == 0)
147
193
  or len(self.body.mesh.faces) == 0):
148
- raise ValueError(f"The mesh of the body {self.body.name} is empty.")
194
+ raise ValueError(f"The mesh of the body {self.body.__short_str__()} is empty.")
149
195
 
150
- if (any(self.body.mesh.faces_centers[:, 2] >= self.free_surface)
151
- or any(self.body.mesh.faces_centers[:, 2] <= -self.water_depth)):
196
+ panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface + 1e-8
197
+ panels_below_sb = self.body.mesh.faces_centers[:, 2] <= -self.water_depth
198
+ if (any(panels_above_fs) or any(panels_below_sb)):
199
+
200
+ if not any(panels_below_sb):
201
+ issue = f"{np.count_nonzero(panels_above_fs)} panels above the free surface"
202
+ elif not any(panels_above_fs):
203
+ issue = f"{np.count_nonzero(panels_below_sb)} panels below the sea bottom"
204
+ else:
205
+ issue = (f"{np.count_nonzero(panels_above_fs)} panels above the free surface " +
206
+ f"and {np.count_nonzero(panels_below_sb)} panels below the sea bottom")
152
207
 
153
208
  LOG.warning(
154
- f"The mesh of the body {self.body.name} is not inside the domain.\n"
155
- "Check the position of the free_surface and the water_depth\n"
156
- "or use body.keep_immersed_part() to clip the mesh."
209
+ f"The mesh of the body {self.body.__short_str__()} has {issue}.\n" +
210
+ "It has been clipped to fit inside the domain.\n" +
211
+ "To remove this warning, clip the mesh manually with the `immersed_part()` method."
157
212
  )
158
213
 
159
- if self.wavelength < self.body.minimal_computable_wavelength:
160
- LOG.warning(f"Mesh resolution for {self}:\n"
161
- f"The resolution of the mesh '{self.body.mesh.name}' of the body '{self.body.name}' "
162
- f"might be insufficient for the wavelength λ={self.wavelength:.2e}.\n"
163
- f"This warning appears because the largest panel of this mesh has radius {self.body.mesh.faces_radiuses.max():.2e} > λ/8."
164
- )
214
+ self.body = self.body.immersed_part(free_surface=self.free_surface,
215
+ water_depth=self.water_depth)
165
216
 
166
217
  if self.boundary_condition is not None:
167
218
  if len(self.boundary_condition.shape) != 1:
168
- raise ValueError("Expected a 1-dimensional array as boundary_condition")
219
+ raise ValueError(f"Expected a 1-dimensional array as boundary_condition. Provided boundary condition's shape: {self.boundary_condition.shape}.")
169
220
 
170
- if self.boundary_condition.shape[0] != self.body.mesh.nb_faces:
221
+ if self.boundary_condition.shape[0] != self.body.mesh_including_lid.nb_faces:
171
222
  raise ValueError(
172
223
  f"The shape of the boundary condition ({self.boundary_condition.shape})"
173
224
  f"does not match the number of faces of the mesh ({self.body.mesh.nb_faces})."
@@ -180,10 +231,14 @@ class LinearPotentialFlowProblem:
180
231
  def _asdict(self):
181
232
  return {"body_name": self.body_name,
182
233
  "water_depth": self.water_depth,
183
- "omega": self.omega,
184
- "period": self.period,
185
- "wavelength": self.wavelength,
186
- "wavenumber": self.wavenumber,
234
+ "omega": float(self.omega),
235
+ "encounter_omega": float(self.encounter_omega),
236
+ "period": float(self.period),
237
+ "wavelength": float(self.wavelength),
238
+ "wavenumber": float(self.wavenumber),
239
+ "forward_speed": self.forward_speed,
240
+ "wave_direction": self.wave_direction,
241
+ "encounter_wave_direction": self.encounter_wave_direction,
187
242
  "rho": self.rho,
188
243
  "g": self.g}
189
244
 
@@ -200,9 +255,13 @@ class LinearPotentialFlowProblem:
200
255
 
201
256
  def __str__(self):
202
257
  """Do not display default values in str(problem)."""
203
- parameters = [f"body={self.body_name}",
204
- f"{self.provided_freq_type}={self.__getattribute__(self.provided_freq_type):.3f}",
258
+ parameters = [f"body={self.body.__short_str__() if self.body is not None else None}",
259
+ f"{self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type)):.3f}",
205
260
  f"water_depth={self.water_depth}"]
261
+
262
+ if not self.forward_speed == _default_parameters['forward_speed']:
263
+ parameters.append(f"forward_speed={self.forward_speed:.3f}")
264
+
206
265
  try:
207
266
  parameters.extend(self._str_other_attributes())
208
267
  except AttributeError:
@@ -223,10 +282,21 @@ class LinearPotentialFlowProblem:
223
282
  def _repr_pretty_(self, p, cycle):
224
283
  p.text(self.__str__())
225
284
 
285
+ def __rich_repr__(self):
286
+ yield "body", self.body, None
287
+ yield self.provided_freq_type, self.__getattribute__(self.provided_freq_type)
288
+ yield "water_depth", self.water_depth, _default_parameters["water_depth"]
289
+ try:
290
+ yield from self._specific_rich_repr()
291
+ except:
292
+ pass
293
+ yield "g", self.g, _default_parameters["g"]
294
+ yield "rho", self.rho, _default_parameters["rho"]
295
+
226
296
  def _astuple(self):
227
297
  return (self.body, self.free_surface, self.water_depth,
228
- self.omega, self.period, self.wavenumber, self.wavelength,
229
- self.rho, self.g)
298
+ float(self.omega), float(self.period), float(self.wavenumber), float(self.wavelength),
299
+ self.forward_speed, self.rho, self.g)
230
300
 
231
301
  def __eq__(self, other):
232
302
  if isinstance(other, LinearPotentialFlowProblem):
@@ -265,19 +335,17 @@ class DiffractionProblem(LinearPotentialFlowProblem):
265
335
  free_surface=_default_parameters['free_surface'],
266
336
  water_depth=None, sea_bottom=None,
267
337
  omega=None, period=None, wavenumber=None, wavelength=None,
338
+ forward_speed=_default_parameters['forward_speed'],
268
339
  rho=_default_parameters['rho'],
269
340
  g=_default_parameters['g'],
270
341
  wave_direction=_default_parameters['wave_direction']):
271
342
 
272
- self.wave_direction = float(wave_direction)
273
-
274
343
  super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
275
- omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength, rho=rho, g=g)
344
+ omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength, wave_direction=wave_direction,
345
+ forward_speed=forward_speed, rho=rho, g=g)
276
346
 
277
- if not (-2*np.pi-1e-3 <= self.wave_direction <= 2*np.pi+1e-3):
278
- LOG.warning(f"The value {self.wave_direction} has been provided for the wave direction, and it does not look like an angle in radians. "
279
- "The wave direction in Capytaine is defined in radians and not in degrees, so the result might not be what you expect. "
280
- "If you were actually giving an angle in radians, use the modulo operator to give a value between -2π and 2π to disable this warning.")
347
+ if float(self.omega) in {0.0, np.inf}:
348
+ raise NotImplementedError("DiffractionProblem does not support zero or infinite frequency.")
281
349
 
282
350
  if self.body is not None:
283
351
 
@@ -285,21 +353,21 @@ class DiffractionProblem(LinearPotentialFlowProblem):
285
353
  airy_waves_velocity(self.body.mesh.faces_centers, self)
286
354
  * self.body.mesh.faces_normals
287
355
  ).sum(axis=1)
356
+ # Note that even with forward speed, this is computed based on the
357
+ # frequency and not the encounter frequency.
358
+
359
+ if self.body.lid_mesh is not None:
360
+ self.boundary_condition = np.concatenate([self.boundary_condition, np.zeros(self.body.lid_mesh.nb_faces)])
288
361
 
289
362
  if len(self.body.dofs) == 0:
290
363
  LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
291
364
 
292
- def _astuple(self):
293
- return super()._astuple() + (self.wave_direction,)
294
-
295
- def _asdict(self):
296
- d = super()._asdict()
297
- d["wave_direction"] = self.wave_direction
298
- return d
299
-
300
365
  def _str_other_attributes(self):
301
366
  return [f"wave_direction={self.wave_direction:.3f}"]
302
367
 
368
+ def _specific_rich_repr(self):
369
+ yield "wave_direction", self.wave_direction, _default_parameters["wave_direction"]
370
+
303
371
  def make_results_container(self, *args, **kwargs):
304
372
  return DiffractionResult(self, *args, **kwargs)
305
373
 
@@ -312,6 +380,8 @@ class RadiationProblem(LinearPotentialFlowProblem):
312
380
  free_surface=_default_parameters['free_surface'],
313
381
  water_depth=None, sea_bottom=None,
314
382
  omega=None, period=None, wavenumber=None, wavelength=None,
383
+ forward_speed=_default_parameters['forward_speed'],
384
+ wave_direction=_default_parameters['wave_direction'],
315
385
  rho=_default_parameters['rho'],
316
386
  g=_default_parameters['g'],
317
387
  radiating_dof=None):
@@ -319,7 +389,8 @@ class RadiationProblem(LinearPotentialFlowProblem):
319
389
  self.radiating_dof = radiating_dof
320
390
 
321
391
  super().__init__(body=body, free_surface=free_surface, water_depth=water_depth, sea_bottom=sea_bottom,
322
- omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength, rho=rho, g=g)
392
+ omega=omega, period=period, wavenumber=wavenumber, wavelength=wavelength,
393
+ wave_direction=wave_direction, forward_speed=forward_speed, rho=rho, g=g)
323
394
 
324
395
  if self.body is not None:
325
396
 
@@ -330,13 +401,32 @@ class RadiationProblem(LinearPotentialFlowProblem):
330
401
  self.radiating_dof = next(iter(self.body.dofs))
331
402
 
332
403
  if self.radiating_dof not in self.body.dofs:
333
- LOG.error(f"In {self}: the radiating degree of freedom {self.radiating_dof} is not one of"
334
- f"the degrees of freedom of the body.\n"
335
- f"The dofs of the body are {list(self.body.dofs.keys())}")
336
- raise ValueError("Unrecognized degree of freedom name.")
404
+ raise ValueError(f"In {self}:\n"
405
+ f"the radiating dof {repr(self.radiating_dof)} is not one of the degrees of freedom of the body.\n"
406
+ f"The dofs of the body are {list(self.body.dofs.keys())}")
337
407
 
338
408
  dof = self.body.dofs[self.radiating_dof]
339
- self.boundary_condition = -1j*self.omega * np.sum(dof * self.body.mesh.faces_normals, axis=1)
409
+
410
+ self.boundary_condition = -1j * self.encounter_omega * np.sum(dof * self.body.mesh.faces_normals, axis=1)
411
+
412
+ if self.forward_speed != 0.0:
413
+ if self.radiating_dof.lower() == "pitch":
414
+ ddofdx_dot_n = np.array([nz for (nx, ny, nz) in self.body.mesh.faces_normals])
415
+ elif self.radiating_dof.lower() == "yaw":
416
+ ddofdx_dot_n = np.array([-ny for (nx, ny, nz) in self.body.mesh.faces_normals])
417
+ elif self.radiating_dof.lower() in {"surge", "sway", "heave", "roll"}:
418
+ ddofdx_dot_n = 0.0
419
+ else:
420
+ raise NotImplementedError(
421
+ "Radiation problem with forward speed is currently only implemented for a single rigid body.\n"
422
+ "Only radiating dofs with name in {'Surge', 'Sway', 'Heave', 'Roll', 'Pitch', 'Yaw'} are supported.\n"
423
+ f"Got instead `radiating_dof={self.radiating_dof}`"
424
+ )
425
+ self.boundary_condition += self.forward_speed * ddofdx_dot_n
426
+
427
+ if self.body.lid_mesh is not None:
428
+ self.boundary_condition = np.concatenate([self.boundary_condition, np.zeros(self.body.lid_mesh.nb_faces)])
429
+
340
430
 
341
431
  def _astuple(self):
342
432
  return super()._astuple() + (self.radiating_dof,)
@@ -347,7 +437,13 @@ class RadiationProblem(LinearPotentialFlowProblem):
347
437
  return d
348
438
 
349
439
  def _str_other_attributes(self):
350
- return [f"radiating_dof=\'{self.radiating_dof}\'"]
440
+ if self.forward_speed != 0.0:
441
+ return [f"wave_direction={self.wave_direction:.3f}, radiating_dof=\'{self.radiating_dof}\'"]
442
+ else:
443
+ return [f"radiating_dof=\'{self.radiating_dof}\'"]
444
+
445
+ def _specific_rich_repr(self):
446
+ yield "radiating_dof", self.radiating_dof
351
447
 
352
448
  def make_results_container(self, *args, **kwargs):
353
449
  return RadiationResult(self, *args, **kwargs)
@@ -358,48 +454,54 @@ class LinearPotentialFlowResult:
358
454
  def __init__(self, problem, forces=None, sources=None, potential=None, pressure=None):
359
455
  self.problem = problem
360
456
 
457
+ self.forces = forces if forces is not None else {}
361
458
  self.sources = sources
362
459
  self.potential = potential
363
460
  self.pressure = pressure
364
- self.fs_elevation = {}
461
+
462
+ self.fs_elevation = {} # Only used in legacy `get_free_surface_elevation`. To be removed?
365
463
 
366
464
  # Copy data from problem
367
465
  self.body = self.problem.body
368
466
  self.free_surface = self.problem.free_surface
369
467
  self.omega = self.problem.omega
468
+ self.period = self.problem.period
469
+ self.wavenumber = self.problem.wavenumber
470
+ self.wavelength = self.problem.wavelength
471
+ self.forward_speed = self.problem.forward_speed
472
+ self.wave_direction = self.problem.wave_direction
473
+ self.encounter_omega = self.problem.encounter_omega
474
+ self.encounter_period = self.problem.encounter_period
475
+ self.encounter_wavenumber = self.problem.encounter_wavenumber
476
+ self.encounter_wavelength = self.problem.encounter_wavelength
477
+ self.encounter_wave_direction = self.problem.encounter_wave_direction
370
478
  self.rho = self.problem.rho
371
479
  self.g = self.problem.g
372
480
  self.boundary_condition = self.problem.boundary_condition
373
481
  self.water_depth = self.problem.water_depth
374
482
  self.depth = self.problem.water_depth
375
- self.wavenumber = self.problem.wavenumber
376
- self.wavelength = self.problem.wavelength
377
- self.period = self.problem.period
378
483
  self.provided_freq_type = self.problem.provided_freq_type
379
484
  self.body_name = self.problem.body_name
380
485
  self.influenced_dofs = self.problem.influenced_dofs
381
486
 
382
- if forces is not None:
383
- for dof in self.influenced_dofs:
384
- self.store_force(dof, forces[dof])
385
-
386
- def store_force(self, dof, force):
387
- pass # Implemented in sub-classes
487
+ @property
488
+ def force(self):
489
+ # Just an alias
490
+ return self.forces
388
491
 
389
492
  __str__ = LinearPotentialFlowProblem.__str__
390
493
  __repr__ = LinearPotentialFlowProblem.__repr__
391
494
  _repr_pretty_ = LinearPotentialFlowProblem._repr_pretty_
495
+ __rich_repr__ = LinearPotentialFlowProblem.__rich_repr__
392
496
 
393
497
 
394
498
  class DiffractionResult(LinearPotentialFlowResult):
395
499
 
396
500
  def __init__(self, problem, *args, **kwargs):
397
- self.forces = {}
398
501
  super().__init__(problem, *args, **kwargs)
399
- self.wave_direction = self.problem.wave_direction
400
502
 
401
- def store_force(self, dof, force):
402
- self.forces[dof] = force
503
+ _str_other_attributes = DiffractionProblem._str_other_attributes
504
+ _specific_rich_repr = DiffractionProblem._specific_rich_repr
403
505
 
404
506
  @property
405
507
  def records(self):
@@ -415,20 +517,29 @@ class DiffractionResult(LinearPotentialFlowResult):
415
517
  class RadiationResult(LinearPotentialFlowResult):
416
518
 
417
519
  def __init__(self, problem, *args, **kwargs):
418
- self.added_masses = {}
419
- self.radiation_dampings = {}
420
520
  super().__init__(problem, *args, **kwargs)
421
521
  self.radiating_dof = self.problem.radiating_dof
422
522
 
423
- def store_force(self, dof, force):
424
- self.added_masses[dof] = force.real/self.omega**2
425
- self.radiation_dampings[dof] = force.imag/self.omega
523
+ _str_other_attributes = RadiationProblem._str_other_attributes
524
+ _specific_rich_repr = RadiationProblem._specific_rich_repr
525
+
526
+ @property
527
+ def added_mass(self):
528
+ return {dof: float(np.real(force)/(self.encounter_omega*self.encounter_omega)) for (dof, force) in self.forces.items()}
529
+
530
+ @property
531
+ def radiation_damping(self):
532
+ return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
533
+
534
+ # Aliases for backward compatibility
535
+ added_masses = added_mass
536
+ radiation_dampings = radiation_damping
426
537
 
427
538
  @property
428
539
  def records(self):
429
540
  params = self.problem._asdict()
430
541
  return [dict(params,
431
542
  influenced_dof=dof,
432
- added_mass=self.added_masses[dof],
433
- radiation_damping=self.radiation_dampings[dof])
543
+ added_mass=self.added_mass[dof],
544
+ radiation_damping=self.radiation_damping[dof])
434
545
  for dof in self.influenced_dofs]