capytaine 2.2.1__cp39-cp39-win_amd64.whl → 2.3__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 (49) hide show
  1. capytaine/__about__.py +1 -1
  2. capytaine/__init__.py +7 -6
  3. capytaine/bem/airy_waves.py +7 -2
  4. capytaine/bem/problems_and_results.py +78 -34
  5. capytaine/bem/solver.py +127 -39
  6. capytaine/bodies/bodies.py +30 -10
  7. capytaine/bodies/predefined/rectangles.py +2 -0
  8. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  9. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  10. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  11. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  12. capytaine/green_functions/FinGreen3D/README.md +24 -0
  13. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  14. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  15. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  16. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  17. capytaine/green_functions/LiangWuNoblesse/Makefile +18 -0
  18. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  19. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  20. capytaine/green_functions/abstract_green_function.py +55 -3
  21. capytaine/green_functions/delhommeau.py +186 -115
  22. capytaine/green_functions/hams.py +204 -0
  23. capytaine/green_functions/libs/Delhommeau_float32.cp39-win_amd64.dll.a +0 -0
  24. capytaine/green_functions/libs/Delhommeau_float32.cp39-win_amd64.pyd +0 -0
  25. capytaine/green_functions/libs/Delhommeau_float64.cp39-win_amd64.dll.a +0 -0
  26. capytaine/green_functions/libs/Delhommeau_float64.cp39-win_amd64.pyd +0 -0
  27. capytaine/io/bemio.py +14 -2
  28. capytaine/io/mesh_loaders.py +1 -1
  29. capytaine/io/wamit.py +479 -0
  30. capytaine/io/xarray.py +257 -113
  31. capytaine/matrices/linear_solvers.py +1 -1
  32. capytaine/meshes/clipper.py +1 -0
  33. capytaine/meshes/collections.py +11 -1
  34. capytaine/meshes/mesh_like_protocol.py +37 -0
  35. capytaine/meshes/meshes.py +17 -6
  36. capytaine/meshes/symmetric.py +11 -2
  37. capytaine/post_pro/kochin.py +4 -4
  38. capytaine/tools/lists_of_points.py +3 -3
  39. capytaine/tools/prony_decomposition.py +60 -4
  40. capytaine/tools/symbolic_multiplication.py +12 -0
  41. capytaine/tools/timer.py +64 -0
  42. capytaine-2.3.dist-info/DELVEWHEEL +2 -0
  43. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/METADATA +9 -2
  44. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/RECORD +48 -32
  45. capytaine-2.2.1.dist-info/DELVEWHEEL +0 -2
  46. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/LICENSE +0 -0
  47. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/WHEEL +0 -0
  48. {capytaine-2.2.1.dist-info → capytaine-2.3.dist-info}/entry_points.txt +0 -0
  49. capytaine.libs/{.load-order-capytaine-2.2.1 → .load-order-capytaine-2.3} +2 -2
capytaine/io/wamit.py ADDED
@@ -0,0 +1,479 @@
1
+ import numpy as np
2
+ import logging
3
+ from typing import Union, Iterable, Tuple, TextIO
4
+ import xarray
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ DOF_INDEX = {"Surge": 1, "Sway": 2, "Heave": 3, "Roll": 4, "Pitch": 5, "Yaw": 6}
9
+
10
+ DOF_TYPE = {
11
+ dof: "trans" if dof in {"Surge", "Sway", "Heave"} else "rot" for dof in DOF_INDEX
12
+ }
13
+ K_LOOKUP = {
14
+ ("trans", "trans"): 3,
15
+ ("trans", "rot"): 4,
16
+ ("rot", "trans"): 4,
17
+ ("rot", "rot"): 5,
18
+ }
19
+
20
+
21
+ def get_dof_index_and_k(dof_i: str, dof_j: str) -> Tuple[int, int, int]:
22
+ """Get the degree of freedom indices and the corresponding stiffness matrix index.
23
+
24
+ Parameters
25
+ ----------
26
+ dof_i: str
27
+ The name of the first degree of freedom.
28
+ dof_j: str
29
+ The name of the second degree of freedom.
30
+
31
+ Returns
32
+ -------
33
+ tuple
34
+ A tuple containing the indices (i, j) and the stiffness matrix index k.
35
+ """
36
+ i = DOF_INDEX[dof_i]
37
+ j = DOF_INDEX[dof_j]
38
+ t_i = DOF_TYPE[dof_i]
39
+ t_j = DOF_TYPE[dof_j]
40
+ k = K_LOOKUP[(t_i, t_j)]
41
+ return i, j, k
42
+
43
+
44
+ def check_dataset_ready_for_export(ds: xarray.Dataset) -> None:
45
+ """
46
+ Sanity checks to validate that the dataset is exportable to BEMIO/WAMIT-like formats.
47
+
48
+ Parameters
49
+ ----------
50
+ ds : xarray.Dataset
51
+ The dataset to be validated.
52
+
53
+ Raises
54
+ ------
55
+ ValueError
56
+ If any unsupported coordinate has multiple values or
57
+ if non-rigid-body DOFs are present.
58
+ """
59
+ # 1. Check for singleton coordinates
60
+ critical_coords = ["water_depth", "g", "rho"]
61
+ coords_with_multiple_values = [
62
+ k
63
+ for k in critical_coords
64
+ if k in ds.coords and len(ds.coords[k].dims) > 0 and ds.sizes[k] > 1
65
+ ]
66
+
67
+ if coords_with_multiple_values:
68
+ msg = (
69
+ "Export formats like WAMIT require only one value for each of the following coordinates: "
70
+ f"{', '.join(critical_coords)}.\n"
71
+ f"Problematic dimensions: {coords_with_multiple_values}.\n"
72
+ "You can extract a subset using:\n"
73
+ f" ds_slice = ds.sel({', '.join([f'{k}={str(ds.coords[k].values[0])}' for k in coords_with_multiple_values])})"
74
+ )
75
+ raise ValueError(msg)
76
+
77
+ # 2. Check for rigid-body DOFs only
78
+ rigid_body_dofs = ("Surge", "Sway", "Heave", "Roll", "Pitch", "Yaw")
79
+ if "influenced_dof" in ds.coords:
80
+ dofs = set(ds.influenced_dof.values)
81
+ non_rigid_dofs = dofs.difference(set(rigid_body_dofs))
82
+ if non_rigid_dofs:
83
+ raise ValueError(
84
+ "WAMIT Export is only supported for single rigid body.\n"
85
+ f"Unexpected DOFs: {non_rigid_dofs}.\n"
86
+ f"Allowed DOFs: {rigid_body_dofs}"
87
+ )
88
+
89
+
90
+ def identify_frequency_axis(
91
+ dataset: Union[xarray.Dataset, xarray.DataArray],
92
+ ) -> Tuple[str, np.ndarray, np.ndarray]:
93
+ """
94
+ Identify the frequency axis in the dataset and return its values along with the periods.
95
+
96
+ Parameters
97
+ ----------
98
+ dataset : xarray.Dataset or xarray.DataArray
99
+ Dataset that must include 'period' coordinate and at least one of:
100
+ 'omega', 'freq', 'period', 'wavenumber', or 'wavelength' as dimension.
101
+
102
+ Returns
103
+ -------
104
+ freq_key : str
105
+ The name of the main frequency-like coordinate.
106
+ freq_vals : np.ndarray
107
+ The values from the frequency coordinate (as present in the dataset).
108
+ period_vals : np.ndarray
109
+ The values of the 'period' coordinate in seconds.
110
+
111
+ Raises
112
+ ------
113
+ ValueError
114
+ If 'period' is not a coordinate or if no frequency-like dimension is found.
115
+ """
116
+ allowed_keys = {"omega", "freq", "wavenumber", "wavelength", "period"}
117
+ dataset_dims = set(dataset.dims)
118
+ keys_in_dataset = dataset_dims & allowed_keys
119
+
120
+ if "period" not in dataset.coords:
121
+ raise ValueError("Dataset must contain 'period' as a coordinate.")
122
+
123
+ # Prioritize 'period' if it is one of the dimensions
124
+ if "period" in dataset_dims:
125
+ freq_key = "period"
126
+ elif len(keys_in_dataset) >= 1:
127
+ freq_key = sorted(keys_in_dataset)[0] # deterministic choice
128
+ else:
129
+ raise ValueError(
130
+ "Dataset must contain at least one frequency-like dimension among: "
131
+ "'omega', 'freq', 'wavenumber', 'wavelength', or 'period'."
132
+ )
133
+
134
+ freq_vals = np.asarray(dataset[freq_key].values)
135
+ period_vals = np.asarray(dataset["period"].values)
136
+
137
+ return freq_key, freq_vals, period_vals
138
+
139
+
140
+ def export_wamit_hst(
141
+ dataset: xarray.Dataset, filename: str, length_scale: float = 1.0
142
+ ) -> None:
143
+ """
144
+ Export the nondimensional hydrostatic stiffness matrix to a WAMIT .hst file.
145
+
146
+ Format:
147
+ I J C(I,J)
148
+
149
+ Parameters
150
+ ----------
151
+ dataset: xarray.Dataset
152
+ Must contain 'hydrostatics' field with 'hydrostatic_stiffness' (named-dict or labeled 6x6 array).
153
+ Must also contain 'rho' and 'g' (either in dataset or within hydrostatics).
154
+ filename: str
155
+ Output path for the .hst file.
156
+ length_scale: float
157
+ Reference length scale L for nondimensionalization.
158
+ """
159
+ if "hydrostatic_stiffness" not in dataset:
160
+ raise ValueError("Dataset must contain a 'hydrostatic_stiffness' field.")
161
+
162
+ # Reduce all extra dimensions to their first value, except the last two (should be 6x6)
163
+ hydrostatic = dataset["hydrostatic_stiffness"]
164
+ C = np.asarray(hydrostatic)
165
+ if C is None or C.shape != (6, 6):
166
+ raise ValueError("'hydrostatic_stiffness' must be a 6x6 matrix.")
167
+
168
+ rho = float(np.atleast_1d(dataset.get("rho")).item())
169
+ g = float(np.atleast_1d(dataset.get("g")).item())
170
+
171
+ # DOF order used in Capytaine
172
+ dof_names = ["Surge", "Sway", "Heave", "Roll", "Pitch", "Yaw"]
173
+
174
+ with open(filename, "w") as f:
175
+ for i_local, dof_i in enumerate(dof_names):
176
+ for j_local, dof_j in enumerate(dof_names):
177
+ cij = C[i_local, j_local]
178
+ i, j, k = get_dof_index_and_k(dof_i, dof_j)
179
+ norm = rho * g * (length_scale**k)
180
+ cij_nd = cij / norm
181
+ f.write(f"{i:5d} {j:5d} {cij_nd:12.6e}\n")
182
+
183
+
184
+ def export_wamit_1(
185
+ dataset: xarray.Dataset, filename: str, length_scale: float = 1.0
186
+ ) -> None:
187
+ """
188
+ Export added mass and radiation damping coefficients to a WAMIT .1 file.
189
+
190
+ Coefficients are normalized as:
191
+ Aij = Aij / (rho * length_scale^k)
192
+ Bij = Bij / (omega * rho * length_scale^k)
193
+
194
+ Format:
195
+ PER I J Aij [Bij]
196
+
197
+ Special handling:
198
+ - For PER = -1 (omega = inf), only Aij is written.
199
+ - For PER = 0 (omega = 0), only Aij is written.
200
+
201
+ Parameters
202
+ ----------
203
+ dataset: xarray.Dataset
204
+ Must contain 'added_mass', 'radiation_damping', and either 'omega' or 'period'.
205
+ filename: str
206
+ Output .1 file.
207
+ length_scale: float
208
+ Reference length scale (L) used for normalization.
209
+
210
+ Raises
211
+ ------
212
+ ValueError
213
+ If required fields are missing or forward speed is not zero.
214
+ """
215
+ if "added_mass" not in dataset or "radiation_damping" not in dataset:
216
+ raise ValueError("Missing 'added_mass' or 'radiation_damping' in dataset.")
217
+
218
+ if not np.isclose(dataset["forward_speed"].item(), 0.0):
219
+ raise ValueError("Forward speed must be zero for WAMIT export.")
220
+
221
+ rho = dataset["rho"].item()
222
+ added_mass = dataset["added_mass"]
223
+ damping = dataset["radiation_damping"]
224
+ omegas = dataset["omega"]
225
+ dofs = list(added_mass.coords["influenced_dof"].values)
226
+
227
+ # Determine main frequency coordinate
228
+ freq_key, freq_vals, period_vals = identify_frequency_axis(dataset=added_mass)
229
+
230
+ # Separate lines into blocks depending on period type
231
+ period_blocks = {
232
+ "T_zero": [], # period == 0 → omega = inf → PER = -1
233
+ "T_inf": [], # period == inf → omega = 0 → PER = 0
234
+ "T_regular": [], # finite, non-zero periods
235
+ }
236
+
237
+ for omega, freq_val, period in zip(omegas, freq_vals, period_vals):
238
+ for dof_i in dofs:
239
+ for dof_j in dofs:
240
+ j_dof, i_dof, k = get_dof_index_and_k(dof_i, dof_j)
241
+ A = added_mass.sel(
242
+ {
243
+ freq_key: freq_val,
244
+ "influenced_dof": dof_i,
245
+ "radiating_dof": dof_j,
246
+ }
247
+ ).item()
248
+ norm = rho * (length_scale**k)
249
+ A_norm = A / norm
250
+
251
+ if np.isclose(period, 0.0):
252
+ # Case PER = -1 (omega = inf)
253
+ line = f"{-1.0:12.6e}\t{i_dof:5d}\t{j_dof:5d}\t{A_norm:12.6e}\n"
254
+ period_blocks["T_zero"].append(line)
255
+ elif np.isinf(period):
256
+ # Case PER = 0 (omega = 0)
257
+ line = f"{0.0:12.6e}\t{i_dof:5d}\t{j_dof:5d}\t{A_norm:12.6e}\n"
258
+ period_blocks["T_inf"].append(line)
259
+ else:
260
+ B = damping.sel(
261
+ {
262
+ freq_key: freq_val,
263
+ "influenced_dof": dof_i,
264
+ "radiating_dof": dof_j,
265
+ }
266
+ ).item()
267
+ B_norm = B / (omega * norm)
268
+ line = f"{period:12.6e}\t{i_dof:5d}\t{j_dof:5d}\t{A_norm:12.6e}\t{B_norm:12.6e}\n"
269
+ period_blocks["T_regular"].append((period, line))
270
+
271
+ # Sort regular lines by increasing period
272
+ sorted_regular = sorted(period_blocks["T_regular"], key=lambda t: t[0])
273
+ sorted_lines = [line for _, line in sorted_regular]
274
+
275
+ # Write to file
276
+ with open(filename, "w") as f:
277
+ f.writelines(period_blocks["T_zero"])
278
+ f.writelines(period_blocks["T_inf"])
279
+ f.writelines(sorted_lines)
280
+
281
+
282
+ def _format_excitation_line(
283
+ period: float, beta_deg: float, i_dof: int, force: complex
284
+ ) -> str:
285
+ """Format a WAMIT excitation line.
286
+
287
+ Parameters
288
+ ----------
289
+ period: float
290
+ Wave period.
291
+ beta_deg: float
292
+ Wave direction (degrees).
293
+ i_dof: int
294
+ Degree of freedom index.
295
+ force: complex
296
+ Excitation force (complex).
297
+
298
+ Returns
299
+ -------
300
+ str
301
+ Formatted excitation line.
302
+ """
303
+ force_conj = np.conj(force)
304
+ mod_f = np.abs(force_conj)
305
+ phi_f = np.degrees(np.angle(force_conj))
306
+ return "{:12.6e}\t{:12.6f}\t{:5d}\t{:12.6e}\t{:12.3f}\t{:12.6e}\t{:12.6e}\n".format(
307
+ period, beta_deg, i_dof, mod_f, phi_f, force_conj.real, force_conj.imag
308
+ )
309
+
310
+
311
+ def _write_wamit_excitation_line(
312
+ f: TextIO,
313
+ freq_key: str,
314
+ freq_val: float,
315
+ period: float,
316
+ beta: float,
317
+ dof: str,
318
+ field: xarray.DataArray,
319
+ rho: float,
320
+ g: float,
321
+ wave_amplitude: float,
322
+ length_scale: float,
323
+ ):
324
+ """Write a single line for WAMIT .3 file format, using freq_key (omega or period)."""
325
+ beta_deg = np.degrees(beta)
326
+ i_dof = DOF_INDEX.get(dof)
327
+ if i_dof is None:
328
+ raise KeyError(f"DOF '{dof}' is not recognized in DOF_INDEX mapping.")
329
+
330
+ dof_type = DOF_TYPE.get(dof, "trans")
331
+ m = 2 if dof_type == "trans" else 3
332
+ norm = rho * g * wave_amplitude * (length_scale**m)
333
+
334
+ # Select value using appropriate key (omega or period)
335
+ force = field.sel(
336
+ {freq_key: freq_val, "wave_direction": beta, "influenced_dof": dof}
337
+ ).item()
338
+ force_normalized = force / norm
339
+
340
+ line = _format_excitation_line(period, beta_deg, i_dof, force_normalized)
341
+ f.write(line)
342
+
343
+
344
+ def _export_wamit_excitation_force(
345
+ dataset: xarray.Dataset,
346
+ field_name: str,
347
+ filename: str,
348
+ length_scale: float = 1.0,
349
+ wave_amplitude: float = 1.0,
350
+ ):
351
+ """
352
+ Export excitation-like forces to a WAMIT .3-style file.
353
+
354
+ Format:
355
+ PER BETA I Fmagnitude Fphase Freal Fimaginary
356
+ """
357
+ forward_speed = dataset["forward_speed"].values
358
+ if not np.isclose(forward_speed, 0.0):
359
+ raise ValueError("Forward speed must be zero for WAMIT export.")
360
+
361
+ if field_name not in dataset:
362
+ raise ValueError(f"Missing field '{field_name}' in dataset.")
363
+
364
+ field = dataset[field_name]
365
+ rho = dataset["rho"].item()
366
+ betas = field.coords["wave_direction"].values
367
+ dofs = list(field.coords["influenced_dof"].values)
368
+ g = dataset["g"].item()
369
+
370
+ # Determine main frequency coordinate
371
+ freq_key, freq_vals, period_vals = identify_frequency_axis(dataset=dataset)
372
+
373
+ # Sort by increasing period
374
+ sorted_indices = np.argsort(period_vals)
375
+ sorted_periods = period_vals[sorted_indices]
376
+ sorted_freqs = freq_vals[sorted_indices]
377
+
378
+ with open(filename, "w") as f:
379
+ for freq_val, period in zip(sorted_freqs, sorted_periods):
380
+ # Skip WAMIT special cases
381
+ if np.isclose(freq_val, 0.0) or np.isinf(freq_val):
382
+ continue
383
+ for beta in betas:
384
+ for dof in dofs:
385
+ _write_wamit_excitation_line(
386
+ f=f,
387
+ freq_key=freq_key,
388
+ freq_val=freq_val,
389
+ period=period,
390
+ beta=beta,
391
+ dof=dof,
392
+ field=field,
393
+ rho=rho,
394
+ g=g,
395
+ wave_amplitude=wave_amplitude,
396
+ length_scale=length_scale,
397
+ )
398
+
399
+
400
+ def export_wamit_3(dataset: xarray.Dataset, filename: str) -> None:
401
+ """Export total excitation to WAMIT .3 file.
402
+
403
+ Parameters
404
+ ----------
405
+ dataset: xarray.Dataset
406
+ Dataset containing the desired complex-valued force field.
407
+ filename: str
408
+ Output path for the .3 file.
409
+ """
410
+ _export_wamit_excitation_force(dataset, "excitation_force", filename)
411
+
412
+
413
+ def export_wamit_3fk(dataset: xarray.Dataset, filename: str) -> None:
414
+ """Export Froude-Krylov contribution to WAMIT .3fk file.
415
+
416
+ Parameters
417
+ ----------
418
+ dataset: xarray.Dataset
419
+ Dataset containing the desired complex-valued force field.
420
+ filename: str
421
+ Output path for the .3fk file.
422
+ """
423
+ _export_wamit_excitation_force(dataset, "Froude_Krylov_force", filename)
424
+
425
+
426
+ def export_wamit_3sc(dataset: xarray.Dataset, filename: str) -> None:
427
+ """Export scattered (diffraction) contribution to WAMIT .3sc file.
428
+
429
+ Parameters
430
+ ----------
431
+ dataset: xarray.Dataset
432
+ Dataset containing the desired complex-valued force field.
433
+ filename: str
434
+ Output path for the .3sc file.
435
+ """
436
+ _export_wamit_excitation_force(dataset, "diffraction_force", filename)
437
+
438
+
439
+ def export_to_wamit(
440
+ dataset: xarray.Dataset,
441
+ problem_name: str,
442
+ exports: Iterable[str] = ("1", "3", "3fk", "3sc", "hst"),
443
+ ) -> None:
444
+ """
445
+ Master function to export a Capytaine dataset to WAMIT-format files.
446
+
447
+ Parameters
448
+ ----------
449
+ dataset: xarray.Dataset
450
+ Dataset containing the desired complex-valued force field.
451
+ problem_name: str
452
+ Base filename for WAMIT files (e.g. "output" → output.1, output.3fk, etc.).
453
+ exports: iterable of str
454
+ Which files to export: any combination of "1", "3", "3fk", "3sc", "hst".
455
+ """
456
+ export_map = {
457
+ "1": ("radiation coefficients", export_wamit_1, ".1"),
458
+ "3": ("total excitation force", export_wamit_3, ".3"),
459
+ "3fk": ("Froude-Krylov force", export_wamit_3fk, ".3fk"),
460
+ "3sc": ("diffraction force", export_wamit_3sc, ".3sc"),
461
+ "hst": ("hydrostatics", export_wamit_hst, ".hst"),
462
+ }
463
+ check_dataset_ready_for_export(dataset)
464
+
465
+ for key in exports:
466
+ if key not in export_map:
467
+ logger.warning(
468
+ f"Export to WAMIT format: unknown option '{key}' — skipping."
469
+ )
470
+ continue
471
+
472
+ description, func, ext = export_map[key]
473
+ filepath = f"{problem_name}{ext}"
474
+
475
+ try:
476
+ func(dataset, filepath)
477
+ logger.info(f"Export to WAMIT format: exported {filepath} ({description})")
478
+ except Exception as e:
479
+ logger.warning(f"Export to WAMIT format: did not export {filepath}: {e}")