capytaine 3.0.0a1__cp312-cp312-macosx_15_0_x86_64.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.
- capytaine/.dylibs/libgcc_s.1.1.dylib +0 -0
- capytaine/.dylibs/libgfortran.5.dylib +0 -0
- capytaine/.dylibs/libquadmath.0.dylib +0 -0
- capytaine/__about__.py +21 -0
- capytaine/__init__.py +32 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +321 -0
- capytaine/bem/problems_and_results.py +601 -0
- capytaine/bem/solver.py +718 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +630 -0
- capytaine/bodies/dofs.py +146 -0
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/Delhommeau_float32.cpython-312-darwin.so +0 -0
- capytaine/green_functions/Delhommeau_float64.cpython-312-darwin.so +0 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +522 -0
- capytaine/green_functions/hams.py +210 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +228 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +673 -0
- capytaine/meshes/__init__.py +2 -0
- capytaine/meshes/abstract_meshes.py +375 -0
- capytaine/meshes/clean.py +302 -0
- capytaine/meshes/clip.py +347 -0
- capytaine/meshes/export.py +89 -0
- capytaine/meshes/geometry.py +259 -0
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +826 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +280 -0
- capytaine/meshes/predefined/rectangles.py +202 -0
- capytaine/meshes/predefined/spheres.py +55 -0
- capytaine/meshes/quality.py +159 -0
- capytaine/meshes/surface_integrals.py +82 -0
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +85 -0
- capytaine/post_pro/impedance.py +92 -0
- capytaine/post_pro/kochin.py +54 -0
- capytaine/post_pro/rao.py +60 -0
- capytaine/tools/__init__.py +0 -0
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/cache_on_disk.py +26 -0
- capytaine/tools/deprecation_handling.py +18 -0
- capytaine/tools/lists_of_points.py +52 -0
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +161 -0
- capytaine/tools/timer.py +90 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine-3.0.0a1.dist-info/LICENSE +674 -0
- capytaine-3.0.0a1.dist-info/METADATA +755 -0
- capytaine-3.0.0a1.dist-info/RECORD +65 -0
- capytaine-3.0.0a1.dist-info/WHEEL +6 -0
- capytaine-3.0.0a1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright 2025 Mews Labs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from itertools import chain
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
LOG = logging.getLogger(__name__)
|
|
21
|
+
SHOWED_PYVISTA_WARNING = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_mesh_quality(mesh, *, tol=1e-8):
|
|
25
|
+
"""
|
|
26
|
+
Perform a set of geometric and metric quality checks on mesh data.
|
|
27
|
+
|
|
28
|
+
Checks performed:
|
|
29
|
+
- Non-coplanar faces
|
|
30
|
+
- Non-convex faces
|
|
31
|
+
- Aspect ratio via PyVista (if available)
|
|
32
|
+
"""
|
|
33
|
+
non_coplanar = indices_of_non_coplanar_faces(mesh.vertices, mesh._faces)
|
|
34
|
+
if non_coplanar:
|
|
35
|
+
LOG.warning(f"{len(non_coplanar)} non-coplanar faces detected in {mesh}.")
|
|
36
|
+
|
|
37
|
+
non_convex = indices_of_non_convex_faces(mesh.vertices, mesh._faces)
|
|
38
|
+
if non_convex:
|
|
39
|
+
LOG.warning(f"{len(non_convex)} non-convex faces detected in {mesh}.")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
pv_mesh = mesh.export_to_pyvista()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
arq = pv_mesh.cell_quality("aspect_ratio").cell_data.get("aspect_ratio")
|
|
46
|
+
except AttributeError: # Older version of PyVista
|
|
47
|
+
arq = pv_mesh.compute_cell_quality("aspect_ratio").cell_data.get("CellQuality")
|
|
48
|
+
if arq is not None:
|
|
49
|
+
ratio_ok = np.sum(arq < 5) / len(arq)
|
|
50
|
+
if ratio_ok < 0.9:
|
|
51
|
+
LOG.info(f"Aspect Ratio of {mesh}:")
|
|
52
|
+
LOG.info(
|
|
53
|
+
f" Min: {np.min(arq):.3f} | Max: {np.max(arq):.3f} | Mean: {np.mean(arq):.3f}"
|
|
54
|
+
)
|
|
55
|
+
LOG.info(f" Elements with AR < 5: {ratio_ok*100:.1f}%")
|
|
56
|
+
LOG.warning(
|
|
57
|
+
"Low quality: more than 10% of elements have aspect ratio higher than 5."
|
|
58
|
+
)
|
|
59
|
+
except ImportError:
|
|
60
|
+
global SHOWED_PYVISTA_WARNING
|
|
61
|
+
if LOG.isEnabledFor(logging.INFO) and not SHOWED_PYVISTA_WARNING:
|
|
62
|
+
LOG.info("PyVista not installed, skipping aspect ratio check.")
|
|
63
|
+
SHOWED_PYVISTA_WARNING = True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extract_face_vertices(vertices, face):
|
|
67
|
+
return vertices[face]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_non_coplanar(vertices):
|
|
71
|
+
a, b, c, d = vertices
|
|
72
|
+
normal = np.cross(b - a, c - a)
|
|
73
|
+
deviation = np.abs(np.dot(normal, d - a))
|
|
74
|
+
return deviation > 1e-8
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def indices_of_non_coplanar_faces(vertices, faces):
|
|
78
|
+
"""
|
|
79
|
+
Identify the indices of quadrilateral faces that are not coplanar.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
vertices : np.ndarray
|
|
84
|
+
Array of vertex coordinates (n_vertices, 3).
|
|
85
|
+
faces : np.ndarray
|
|
86
|
+
Array of face indices (n_faces, 4) or (n_faces, 3).
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
list[int]
|
|
91
|
+
List of indices of non-coplanar quadrilateral faces.
|
|
92
|
+
"""
|
|
93
|
+
indices = []
|
|
94
|
+
for i, face in enumerate(faces):
|
|
95
|
+
if len(face) != 4:
|
|
96
|
+
continue # skip triangles or degenerate quads
|
|
97
|
+
verts = extract_face_vertices(vertices, face)
|
|
98
|
+
if is_non_coplanar(verts):
|
|
99
|
+
indices.append(i)
|
|
100
|
+
return indices
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_face_convex(vertices):
|
|
104
|
+
a, b, c, d = vertices
|
|
105
|
+
edges = [b - a, c - b, d - c, a - d]
|
|
106
|
+
normals = [np.cross(edges[i], edges[(i + 1) % 4]) for i in range(4)]
|
|
107
|
+
dot_signs = [np.dot(normals[0], n) for n in normals[1:]]
|
|
108
|
+
return all(s >= -1e-10 for s in dot_signs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def indices_of_non_convex_faces(vertices, faces):
|
|
112
|
+
"""
|
|
113
|
+
Identify indices of quadrilateral faces in the mesh that are not convex.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
mesh : Mesh
|
|
118
|
+
The input mesh containing faces and vertices.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
list[int]
|
|
123
|
+
List of indices of non-convex quadrilateral faces.
|
|
124
|
+
"""
|
|
125
|
+
indices = []
|
|
126
|
+
for i, face in enumerate(faces):
|
|
127
|
+
if len(face) != 4:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
verts = extract_face_vertices(vertices, face)
|
|
131
|
+
|
|
132
|
+
if not is_face_convex(verts):
|
|
133
|
+
indices.append(i)
|
|
134
|
+
|
|
135
|
+
return indices
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _is_valid(vertices, faces):
|
|
139
|
+
"""
|
|
140
|
+
Check that every face index is valid for the given vertices.
|
|
141
|
+
Now: an empty face-list is considered valid (as long as vertices exist).
|
|
142
|
+
"""
|
|
143
|
+
# If you have no vertices at all, only accept if also no faces
|
|
144
|
+
if len(vertices) == 0:
|
|
145
|
+
return len(faces) == 0
|
|
146
|
+
|
|
147
|
+
# If you have vertices but zero faces → that's fine
|
|
148
|
+
if len(faces) == 0:
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Otherwise, flatten all face‐indices and check bounds
|
|
152
|
+
all_idx = list(chain.from_iterable(faces))
|
|
153
|
+
if not all_idx:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
if min(all_idx) < 0 or max(all_idx) >= len(vertices):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
return True
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright 2025 Mews Labs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from abc import ABC
|
|
16
|
+
from typing import Tuple, Union
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
class SurfaceIntegralsMixin(ABC):
|
|
21
|
+
# Defines some methods inherited by AbstractMesh.
|
|
22
|
+
# There are located in this other module just to make the code more tidy.
|
|
23
|
+
|
|
24
|
+
def surface_integral(self, data, **kwargs):
|
|
25
|
+
"""Returns integral of given data along wet surface area."""
|
|
26
|
+
return np.sum(data * self.faces_areas, **kwargs)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def wet_surface_area(self) -> float:
|
|
30
|
+
"""Returns wet surface area."""
|
|
31
|
+
return self.immersed_part().surface_integral(1)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def volumes(self) -> Tuple[float, float, float]:
|
|
35
|
+
"""Returns volumes using x, y, z components of the mesh.
|
|
36
|
+
Should be the same for a regular mesh."""
|
|
37
|
+
norm_coord = self.faces_normals * self.faces_centers
|
|
38
|
+
return tuple(self.surface_integral(norm_coord.T, axis=1))
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def volume(self) -> float:
|
|
42
|
+
"""Returns volume of the mesh."""
|
|
43
|
+
return np.mean(self.volumes)
|
|
44
|
+
|
|
45
|
+
def waterplane_integral(self, data, **kwargs):
|
|
46
|
+
"""Returns integral of given data along water plane area."""
|
|
47
|
+
immersed_self = self.immersed_part()
|
|
48
|
+
return immersed_self.surface_integral(immersed_self.faces_normals[:,2] * data, **kwargs)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def disp_volume(self) -> float:
|
|
52
|
+
return self.immersed_part().volume
|
|
53
|
+
|
|
54
|
+
def disp_mass(self, *, rho=1000) -> float:
|
|
55
|
+
return rho * self.disp_volume
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def center_of_buoyancy(self) -> np.ndarray:
|
|
59
|
+
"""Returns center of buoyancy of the mesh."""
|
|
60
|
+
immersed_self = self.immersed_part()
|
|
61
|
+
coords_sq_norm = immersed_self.faces_normals * immersed_self.faces_centers**2
|
|
62
|
+
return immersed_self.surface_integral(coords_sq_norm.T, axis=1) / (2*immersed_self.volume)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def waterplane_area(self) -> float:
|
|
66
|
+
"""Returns water plane area of the mesh."""
|
|
67
|
+
immersed_self = self.immersed_part()
|
|
68
|
+
return -immersed_self.waterplane_integral(1)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def waterplane_center(self) -> Union[None, np.ndarray]:
|
|
72
|
+
"""Returns water plane center of the mesh.
|
|
73
|
+
Returns None if the mesh is full submerged.
|
|
74
|
+
"""
|
|
75
|
+
immersed_self = self.immersed_part()
|
|
76
|
+
waterplane_area = immersed_self.waterplane_area
|
|
77
|
+
if abs(waterplane_area) < 1e-10:
|
|
78
|
+
return None
|
|
79
|
+
else:
|
|
80
|
+
waterplane_center = -immersed_self.waterplane_integral(
|
|
81
|
+
immersed_self.faces_centers.T, axis=1) / waterplane_area
|
|
82
|
+
return waterplane_center[:-1]
|