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
capytaine/__about__.py CHANGED
@@ -5,7 +5,7 @@ __all__ = ["__title__", "__description__", "__version__", "__author__", "__uri__
5
5
  __title__ = "capytaine"
6
6
  __description__ = """Python BEM solver for linear potential flow, based on Nemoh"""
7
7
 
8
- __version__ = "2.1"
8
+ __version__ = "2.2.1"
9
9
 
10
10
  __author__ = "Matthieu Ancellin"
11
11
  __uri__ = "https://github.com/capytaine/capytaine"
capytaine/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
  # start delvewheel patch
6
- def _delvewheel_patch_1_5_4():
6
+ def _delvewheel_patch_1_9_0():
7
7
  import ctypes
8
8
  import os
9
9
  import platform
@@ -14,19 +14,22 @@ def _delvewheel_patch_1_5_4():
14
14
  if os.path.isdir(libs_dir):
15
15
  os.add_dll_directory(libs_dir)
16
16
  else:
17
- load_order_filepath = os.path.join(libs_dir, '.load-order-capytaine-2.1')
17
+ load_order_filepath = os.path.join(libs_dir, '.load-order-capytaine-2.2.1')
18
18
  if os.path.isfile(load_order_filepath):
19
- with open(os.path.join(libs_dir, '.load-order-capytaine-2.1')) as file:
19
+ import ctypes.wintypes
20
+ with open(os.path.join(libs_dir, '.load-order-capytaine-2.2.1')) as file:
20
21
  load_order = file.read().split()
22
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
23
+ kernel32.LoadLibraryExW.restype = ctypes.wintypes.HMODULE
24
+ kernel32.LoadLibraryExW.argtypes = ctypes.wintypes.LPCWSTR, ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD
21
25
  for lib in load_order:
22
26
  lib_path = os.path.join(os.path.join(libs_dir, lib))
23
- kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
24
- if os.path.isfile(lib_path) and not kernel32.LoadLibraryExW(ctypes.c_wchar_p(lib_path), None, 0x00000008):
27
+ if os.path.isfile(lib_path) and not kernel32.LoadLibraryExW(lib_path, None, 8):
25
28
  raise OSError('Error loading {}; {}'.format(lib, ctypes.FormatError(ctypes.get_last_error())))
26
29
 
27
30
 
28
- _delvewheel_patch_1_5_4()
29
- del _delvewheel_patch_1_5_4
31
+ _delvewheel_patch_1_9_0()
32
+ del _delvewheel_patch_1_9_0
30
33
  # end delvewheel patch
31
34
 
32
35
  from .__about__ import (
capytaine/bem/engines.py CHANGED
@@ -123,10 +123,10 @@ class BasicMatrixEngine(MatrixEngine):
123
123
 
124
124
  S_a, V_a = self.build_matrices(
125
125
  mesh1[0], mesh2[0], free_surface, water_depth, wavenumber,
126
- green_function)
126
+ green_function, adjoint_double_layer=adjoint_double_layer)
127
127
  S_b, V_b = self.build_matrices(
128
128
  mesh1[0], mesh2[1], free_surface, water_depth, wavenumber,
129
- green_function)
129
+ green_function, adjoint_double_layer=adjoint_double_layer)
130
130
 
131
131
  return BlockSymmetricToeplitzMatrix([[S_a, S_b]]), BlockSymmetricToeplitzMatrix([[V_a, V_b]])
132
132
 
@@ -193,7 +193,7 @@ class LinearPotentialFlowProblem:
193
193
  or len(self.body.mesh.faces) == 0):
194
194
  raise ValueError(f"The mesh of the body {self.body.__short_str__()} is empty.")
195
195
 
196
- panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface
196
+ panels_above_fs = self.body.mesh.faces_centers[:, 2] >= self.free_surface + 1e-8
197
197
  panels_below_sb = self.body.mesh.faces_centers[:, 2] <= -self.water_depth
198
198
  if (any(panels_above_fs) or any(panels_below_sb)):
199
199
 
@@ -218,7 +218,7 @@ class LinearPotentialFlowProblem:
218
218
  if len(self.boundary_condition.shape) != 1:
219
219
  raise ValueError(f"Expected a 1-dimensional array as boundary_condition. Provided boundary condition's shape: {self.boundary_condition.shape}.")
220
220
 
221
- 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:
222
222
  raise ValueError(
223
223
  f"The shape of the boundary condition ({self.boundary_condition.shape})"
224
224
  f"does not match the number of faces of the mesh ({self.body.mesh.nb_faces})."
@@ -256,7 +256,7 @@ class LinearPotentialFlowProblem:
256
256
  def __str__(self):
257
257
  """Do not display default values in str(problem)."""
258
258
  parameters = [f"body={self.body.__short_str__() if self.body is not None else None}",
259
- f"{self.provided_freq_type}={self.__getattribute__(self.provided_freq_type):.3f}",
259
+ f"{self.provided_freq_type}={float(self.__getattribute__(self.provided_freq_type)):.3f}",
260
260
  f"water_depth={self.water_depth}"]
261
261
 
262
262
  if not self.forward_speed == _default_parameters['forward_speed']:
@@ -345,7 +345,7 @@ class DiffractionProblem(LinearPotentialFlowProblem):
345
345
  forward_speed=forward_speed, rho=rho, g=g)
346
346
 
347
347
  if float(self.omega) in {0.0, np.inf}:
348
- raise NotImplementedError(f"DiffractionProblem does not support zero or infinite frequency.")
348
+ raise NotImplementedError("DiffractionProblem does not support zero or infinite frequency.")
349
349
 
350
350
  if self.body is not None:
351
351
 
@@ -356,6 +356,9 @@ class DiffractionProblem(LinearPotentialFlowProblem):
356
356
  # Note that even with forward speed, this is computed based on the
357
357
  # frequency and not the encounter frequency.
358
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)])
361
+
359
362
  if len(self.body.dofs) == 0:
360
363
  LOG.warning(f"The body {self.body.name} used in diffraction problem has no dofs!")
361
364
 
@@ -398,10 +401,9 @@ class RadiationProblem(LinearPotentialFlowProblem):
398
401
  self.radiating_dof = next(iter(self.body.dofs))
399
402
 
400
403
  if self.radiating_dof not in self.body.dofs:
401
- LOG.error(f"In {self}: the radiating degree of freedom {self.radiating_dof} is not one of"
402
- f"the degrees of freedom of the body.\n"
403
- f"The dofs of the body are {list(self.body.dofs.keys())}")
404
- 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())}")
405
407
 
406
408
  dof = self.body.dofs[self.radiating_dof]
407
409
 
@@ -422,6 +424,9 @@ class RadiationProblem(LinearPotentialFlowProblem):
422
424
  )
423
425
  self.boundary_condition += self.forward_speed * ddofdx_dot_n
424
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
+
425
430
 
426
431
  def _astuple(self):
427
432
  return super()._astuple() + (self.radiating_dof,)
@@ -524,7 +529,10 @@ class RadiationResult(LinearPotentialFlowResult):
524
529
 
525
530
  @property
526
531
  def radiation_damping(self):
527
- return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
532
+ if float(self.encounter_omega) in {0.0, np.inf} and self.forward_speed == 0.0:
533
+ return {dof: 0.0 for dof in self.forces.keys()}
534
+ else:
535
+ return {dof: float(np.imag(force)/self.encounter_omega) for (dof, force) in self.forces.items()}
528
536
 
529
537
  # Aliases for backward compatibility
530
538
  added_masses = added_mass
capytaine/bem/solver.py CHANGED
@@ -1,5 +1,5 @@
1
- # Copyright (C) 2017-2019 Matthieu Ancellin
2
- # See LICENSE file at <https://github.com/mancellin/capytaine>
1
+ # Copyright (C) 2017-2024 Matthieu Ancellin
2
+ # See LICENSE file at <https://github.com/capytaine/capytaine>
3
3
  """Solver for the BEM problem.
4
4
 
5
5
  .. code-block:: python
@@ -83,8 +83,9 @@ class BEMSolver:
83
83
  keep_details: bool, optional
84
84
  if True, store the sources and the potential on the floating body in the output object
85
85
  (default: True)
86
- _check_wavelength: bool, optional
87
- if True, check the mesh resolution with respect to the wavelength
86
+ _check_wavelength: bool, optional (default: True)
87
+ If True, the frequencies are compared to the mesh resolution and
88
+ the estimated first irregular frequency to warn the user.
88
89
 
89
90
  Returns
90
91
  -------
@@ -93,7 +94,9 @@ class BEMSolver:
93
94
  """
94
95
  LOG.info("Solve %s.", problem)
95
96
 
96
- if _check_wavelength: self._check_wavelength([problem])
97
+ if _check_wavelength:
98
+ self._check_wavelength_and_mesh_resolution([problem])
99
+ self._check_wavelength_and_irregular_frequencies([problem])
97
100
 
98
101
  if problem.forward_speed != 0.0:
99
102
  omega, wavenumber = problem.encounter_omega, problem.encounter_wavenumber
@@ -106,7 +109,7 @@ class BEMSolver:
106
109
  raise NotImplementedError("Direct solver is not able to solve problems with forward speed.")
107
110
 
108
111
  S, D = self.engine.build_matrices(
109
- problem.body.mesh, problem.body.mesh,
112
+ problem.body.mesh_including_lid, problem.body.mesh_including_lid,
110
113
  problem.free_surface, problem.water_depth, wavenumber,
111
114
  self.green_function, adjoint_double_layer=False
112
115
  )
@@ -116,7 +119,7 @@ class BEMSolver:
116
119
  sources = None
117
120
  else:
118
121
  S, K = self.engine.build_matrices(
119
- problem.body.mesh, problem.body.mesh,
122
+ problem.body.mesh_including_lid, problem.body.mesh_including_lid,
120
123
  problem.free_surface, problem.water_depth, wavenumber,
121
124
  self.green_function, adjoint_double_layer=True
122
125
  )
@@ -127,10 +130,11 @@ class BEMSolver:
127
130
  if problem.forward_speed != 0.0:
128
131
  result = problem.make_results_container(sources=sources)
129
132
  # Temporary result object to compute the ∇Φ term
130
- nabla_phi = self._compute_potential_gradient(problem.body.mesh, result)
133
+ nabla_phi = self._compute_potential_gradient(problem.body.mesh_including_lid, result)
131
134
  pressure += problem.rho * problem.forward_speed * nabla_phi[:, 0]
132
135
 
133
- forces = problem.body.integrate_pressure(pressure)
136
+ pressure_on_hull = pressure[:problem.body.mesh.nb_faces] # Discards pressure on lid if any
137
+ forces = problem.body.integrate_pressure(pressure_on_hull)
134
138
 
135
139
  if not keep_details:
136
140
  result = problem.make_results_container(forces)
@@ -156,13 +160,18 @@ class BEMSolver:
156
160
  By defaults: do not use joblib and solve sequentially.
157
161
  progress_bar: bool, optional (default: True)
158
162
  Display a progress bar while solving
163
+ _check_wavelength: bool, optional (default: True)
164
+ If True, the frequencies are compared to the mesh resolution and
165
+ the estimated first irregular frequency to warn the user.
159
166
 
160
167
  Returns
161
168
  -------
162
169
  list of LinearPotentialFlowResult
163
170
  the solved problems
164
171
  """
165
- if _check_wavelength: self._check_wavelength(problems)
172
+ if _check_wavelength:
173
+ self._check_wavelength_and_mesh_resolution(problems)
174
+ self._check_wavelength_and_irregular_frequencies(problems)
166
175
 
167
176
  if n_jobs == 1: # force sequential resolution
168
177
  problems = sorted(problems)
@@ -184,33 +193,64 @@ class BEMSolver:
184
193
  return results
185
194
 
186
195
  @staticmethod
187
- def _check_wavelength(problems):
196
+ def _check_wavelength_and_mesh_resolution(problems):
188
197
  """Display a warning if some of the problems have a mesh resolution
189
198
  that might not be sufficient for the given wavelength."""
199
+ LOG.debug("Check wavelength with mesh resolution.")
190
200
  risky_problems = [pb for pb in problems
191
- if pb.wavelength < pb.body.minimal_computable_wavelength]
201
+ if 0.0 < pb.wavelength < pb.body.minimal_computable_wavelength]
192
202
  nb_risky_problems = len(risky_problems)
193
203
  if nb_risky_problems == 1:
194
204
  pb = risky_problems[0]
195
205
  freq_type = risky_problems[0].provided_freq_type
196
206
  freq = pb.__getattribute__(freq_type)
197
207
  LOG.warning(f"Mesh resolution for {pb}:\n"
198
- f"The resolution of the mesh of the body {pb.body.__short_str__()} might "
199
- f"be insufficient for {freq_type}={freq}.\n"
200
- "This warning appears because the largest panel of this mesh "
201
- f"has radius {pb.body.mesh.faces_radiuses.max():.3f} > wavelength/8."
202
- )
208
+ f"The resolution of the mesh of the body {pb.body.__short_str__()} might "
209
+ f"be insufficient for {freq_type}={freq}.\n"
210
+ "This warning appears because the largest panel of this mesh "
211
+ f"has radius {pb.body.mesh.faces_radiuses.max():.3f} > wavelength/8."
212
+ )
203
213
  elif nb_risky_problems > 1:
204
214
  freq_type = risky_problems[0].provided_freq_type
205
215
  freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
206
216
  LOG.warning(f"Mesh resolution for {nb_risky_problems} problems:\n"
207
- "The resolution of the mesh might be insufficient "
217
+ "The resolution of the mesh might be insufficient "
208
218
  f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
209
- "This warning appears when the largest panel of this mesh "
210
- "has radius > wavelength/8."
211
- )
219
+ "This warning appears when the largest panel of this mesh "
220
+ "has radius > wavelength/8."
221
+ )
212
222
 
213
- def fill_dataset(self, dataset, bodies, *, method='indirect', n_jobs=1, **kwargs):
223
+ @staticmethod
224
+ def _check_wavelength_and_irregular_frequencies(problems):
225
+ """Display a warning if some of the problems might encounter irregular frequencies."""
226
+ LOG.debug("Check wavelength with estimated irregular frequency.")
227
+ risky_problems = [pb for pb in problems
228
+ if pb.body.first_irregular_frequency_estimate(g=pb.g) < pb.omega < np.inf]
229
+ nb_risky_problems = len(risky_problems)
230
+ if nb_risky_problems >= 1:
231
+ if any(pb.body.lid_mesh is None for pb in problems):
232
+ recommendation = "Setting a lid for the floating body is recommended."
233
+ else:
234
+ recommendation = "The lid might need to be closer to the free surface."
235
+ if nb_risky_problems == 1:
236
+ pb = risky_problems[0]
237
+ freq_type = risky_problems[0].provided_freq_type
238
+ freq = pb.__getattribute__(freq_type)
239
+ LOG.warning(f"Irregular frequencies for {pb}:\n"
240
+ f"The body {pb.body.__short_str__()} might display irregular frequencies "
241
+ f"for {freq_type}={freq}.\n"
242
+ + recommendation
243
+ )
244
+ elif nb_risky_problems > 1:
245
+ freq_type = risky_problems[0].provided_freq_type
246
+ freqs = np.array([float(pb.__getattribute__(freq_type)) for pb in risky_problems])
247
+ LOG.warning(f"Irregular frequencies for {nb_risky_problems} problems:\n"
248
+ "Irregular frequencies might be encountered "
249
+ f"for {freq_type} ranging from {freqs.min():.3f} to {freqs.max():.3f}.\n"
250
+ + recommendation
251
+ )
252
+
253
+ def fill_dataset(self, dataset, bodies, *, method='indirect', n_jobs=1, _check_wavelength=True, **kwargs):
214
254
  """Solve a set of problems defined by the coordinates of an xarray dataset.
215
255
 
216
256
  Parameters
@@ -227,6 +267,9 @@ class BEMSolver:
227
267
  By defaults: do not use joblib and solve sequentially.
228
268
  progress_bar: bool, optional (default: True)
229
269
  Display a progress bar while solving
270
+ _check_wavelength: bool, optional (default: True)
271
+ If True, the frequencies are compared to the mesh resolution and
272
+ the estimated first irregular frequency to warn the user.
230
273
 
231
274
  Returns
232
275
  -------
@@ -236,12 +279,12 @@ class BEMSolver:
236
279
  **self.exportable_settings}
237
280
  problems = problems_from_dataset(dataset, bodies)
238
281
  if 'theta' in dataset.coords:
239
- results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs)
282
+ results = self.solve_all(problems, keep_details=True, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength)
240
283
  kochin = kochin_data_array(results, dataset.coords['theta'])
241
284
  dataset = assemble_dataset(results, attrs=attrs, **kwargs)
242
285
  dataset.update(kochin)
243
286
  else:
244
- results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs)
287
+ results = self.solve_all(problems, keep_details=False, method=method, n_jobs=n_jobs, _check_wavelength=_check_wavelength)
245
288
  dataset = assemble_dataset(results, attrs=attrs, **kwargs)
246
289
  return dataset
247
290
 
@@ -271,7 +314,7 @@ class BEMSolver:
271
314
  They probably have not been stored by the solver because the option keep_details=True have not been set or the direct method has been used.
272
315
  Please re-run the resolution with the indirect method and keep_details=True.""")
273
316
 
274
- S, _ = self.green_function.evaluate(points, result.body.mesh, result.free_surface, result.water_depth, result.encounter_wavenumber)
317
+ S, _ = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber)
275
318
  potential = S @ result.sources # Sum the contributions of all panels in the mesh
276
319
  return potential.reshape(output_shape)
277
320
 
@@ -283,7 +326,7 @@ class BEMSolver:
283
326
  They probably have not been stored by the solver because the option keep_details=True have not been set.
284
327
  Please re-run the resolution with this option.""")
285
328
 
286
- _, gradG = self.green_function.evaluate(points, result.body.mesh, result.free_surface, result.water_depth, result.encounter_wavenumber,
329
+ _, gradG = self.green_function.evaluate(points, result.body.mesh_including_lid, result.free_surface, result.water_depth, result.encounter_wavenumber,
287
330
  early_dot_product=False)
288
331
  velocities = np.einsum('ijk,j->ik', gradG, result.sources) # Sum the contributions of all panels in the mesh
289
332
  return velocities.reshape((*output_shape, 3))
@@ -409,7 +452,7 @@ class BEMSolver:
409
452
  if chunk_size > mesh.nb_faces:
410
453
  S = self.engine.build_S_matrix(
411
454
  mesh,
412
- result.body.mesh,
455
+ result.body.mesh_including_lid,
413
456
  result.free_surface, result.water_depth, result.wavenumber,
414
457
  self.green_function
415
458
  )
@@ -421,7 +464,7 @@ class BEMSolver:
421
464
  faces_to_extract = list(range(i, min(i+chunk_size, mesh.nb_faces)))
422
465
  S = self.engine.build_S_matrix(
423
466
  mesh.extract_faces(faces_to_extract),
424
- result.body.mesh,
467
+ result.body.mesh_including_lid,
425
468
  result.free_surface, result.water_depth, result.wavenumber,
426
469
  self.green_function
427
470
  )