capytaine 2.3.1__cp310-cp310-macosx_14_0_arm64.whl → 3.0.0a1__cp310-cp310-macosx_14_0_arm64.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/__about__.py +7 -2
- capytaine/__init__.py +8 -12
- capytaine/bem/engines.py +234 -354
- capytaine/bem/problems_and_results.py +14 -13
- capytaine/bem/solver.py +204 -80
- capytaine/bodies/bodies.py +278 -869
- capytaine/bodies/dofs.py +136 -9
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/{libs/Delhommeau_float32.cpython-310-darwin.so → Delhommeau_float32.cpython-310-darwin.so} +0 -0
- capytaine/green_functions/{libs/Delhommeau_float64.cpython-310-darwin.so → Delhommeau_float64.cpython-310-darwin.so} +0 -0
- capytaine/green_functions/abstract_green_function.py +2 -2
- capytaine/green_functions/delhommeau.py +31 -16
- capytaine/green_functions/hams.py +19 -13
- capytaine/io/legacy.py +3 -103
- capytaine/io/xarray.py +11 -6
- capytaine/meshes/__init__.py +2 -6
- 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 +244 -394
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +617 -681
- capytaine/meshes/predefined/cylinders.py +22 -56
- capytaine/meshes/predefined/rectangles.py +26 -85
- capytaine/meshes/predefined/spheres.py +4 -11
- capytaine/meshes/quality.py +118 -407
- capytaine/meshes/surface_integrals.py +48 -29
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/free_surfaces.py +1 -4
- capytaine/post_pro/kochin.py +10 -10
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/lists_of_points.py +2 -2
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/symbolic_multiplication.py +13 -1
- capytaine/tools/timer.py +58 -34
- {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +7 -2
- capytaine-3.0.0a1.dist-info/RECORD +65 -0
- capytaine/bodies/predefined/__init__.py +0 -6
- capytaine/bodies/predefined/cylinders.py +0 -151
- capytaine/bodies/predefined/rectangles.py +0 -111
- capytaine/bodies/predefined/spheres.py +0 -70
- capytaine/green_functions/FinGreen3D/.gitignore +0 -1
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
- capytaine/green_functions/FinGreen3D/LICENSE +0 -165
- capytaine/green_functions/FinGreen3D/Makefile +0 -16
- capytaine/green_functions/FinGreen3D/README.md +0 -24
- capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
- capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
- capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
- capytaine/green_functions/LiangWuNoblesse/Makefile +0 -16
- capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/mesh_loaders.py +0 -1086
- capytaine/io/mesh_writers.py +0 -692
- capytaine/io/meshio.py +0 -38
- capytaine/matrices/__init__.py +0 -16
- capytaine/matrices/block.py +0 -592
- capytaine/matrices/block_toeplitz.py +0 -325
- capytaine/matrices/builders.py +0 -89
- capytaine/matrices/linear_solvers.py +0 -232
- capytaine/matrices/low_rank.py +0 -395
- capytaine/meshes/clipper.py +0 -465
- capytaine/meshes/collections.py +0 -342
- capytaine/meshes/mesh_like_protocol.py +0 -37
- capytaine/meshes/properties.py +0 -276
- capytaine/meshes/quadratures.py +0 -80
- capytaine/meshes/symmetric.py +0 -462
- capytaine/tools/lru_cache.py +0 -49
- capytaine/ui/vtk/__init__.py +0 -3
- capytaine/ui/vtk/animation.py +0 -329
- capytaine/ui/vtk/body_viewer.py +0 -28
- capytaine/ui/vtk/helpers.py +0 -82
- capytaine/ui/vtk/mesh_viewer.py +0 -461
- capytaine-2.3.1.dist-info/RECORD +0 -92
- {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
- {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/WHEEL +0 -0
- {capytaine-2.3.1.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
capytaine/meshes/quality.py
CHANGED
|
@@ -1,448 +1,159 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
""
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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.
|
|
6
14
|
|
|
7
15
|
import logging
|
|
16
|
+
from itertools import chain
|
|
8
17
|
|
|
9
18
|
import numpy as np
|
|
10
19
|
|
|
11
|
-
from capytaine.meshes.geometry import inplace_transformation
|
|
12
|
-
from capytaine.meshes.properties import compute_connectivity
|
|
13
|
-
|
|
14
20
|
LOG = logging.getLogger(__name__)
|
|
21
|
+
SHOWED_PYVISTA_WARNING = False
|
|
15
22
|
|
|
16
23
|
|
|
17
|
-
def
|
|
18
|
-
"""Merges the duplicate vertices of the mesh in place.
|
|
19
|
-
|
|
20
|
-
Parameters
|
|
21
|
-
----------
|
|
22
|
-
atol : float, optional
|
|
23
|
-
Absolute tolerance. default is 1e-8
|
|
24
|
-
|
|
25
|
-
Returns
|
|
26
|
-
-------
|
|
27
|
-
new_id : ndarray
|
|
28
|
-
Array of indices that merges the vertices.
|
|
24
|
+
def check_mesh_quality(mesh, *, tol=1e-8):
|
|
29
25
|
"""
|
|
30
|
-
|
|
26
|
+
Perform a set of geometric and metric quality checks on mesh data.
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
37
64
|
|
|
38
|
-
nv_final = mesh.nb_vertices
|
|
39
65
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if delta_n == 0:
|
|
43
|
-
LOG.debug("\t--> No duplicate vertices have been found")
|
|
44
|
-
else:
|
|
45
|
-
LOG.debug("\t--> Initial number of vertices : %u", nv_init)
|
|
46
|
-
LOG.debug("\t--> Final number of vertices : %u", nv_final)
|
|
47
|
-
LOG.debug("\t--> %u vertices have been merged", delta_n)
|
|
66
|
+
def extract_face_vertices(vertices, face):
|
|
67
|
+
return vertices[face]
|
|
48
68
|
|
|
49
|
-
# if mesh._has_connectivity():
|
|
50
|
-
# mesh._remove_connectivity()
|
|
51
69
|
|
|
52
|
-
|
|
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
|
|
53
75
|
|
|
54
76
|
|
|
55
|
-
def
|
|
56
|
-
"""
|
|
77
|
+
def indices_of_non_coplanar_faces(vertices, faces):
|
|
78
|
+
"""
|
|
79
|
+
Identify the indices of quadrilateral faces that are not coplanar.
|
|
57
80
|
|
|
58
81
|
Parameters
|
|
59
82
|
----------
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
that have to be merged
|
|
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).
|
|
65
87
|
|
|
66
88
|
Returns
|
|
67
89
|
-------
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
every node is different
|
|
71
|
-
newID : ndarray
|
|
72
|
-
array of the new new vertices IDs
|
|
73
|
-
"""
|
|
74
|
-
# This function is a bottleneck in the clipping routines
|
|
75
|
-
# TODO: use np.unique to cluster groups --> acceleration !!
|
|
76
|
-
|
|
77
|
-
# atol = pow(10, -decimals)
|
|
78
|
-
|
|
79
|
-
arr = np.asarray(arr)
|
|
80
|
-
|
|
81
|
-
nv, nbdim = arr.shape
|
|
82
|
-
|
|
83
|
-
levels = [0, nv]
|
|
84
|
-
iperm = np.arange(nv)
|
|
85
|
-
|
|
86
|
-
for dim in range(nbdim):
|
|
87
|
-
# Sorting the first dimension
|
|
88
|
-
values = arr[:, dim].copy()
|
|
89
|
-
if dim > 0:
|
|
90
|
-
values = values[iperm]
|
|
91
|
-
levels_tmp = []
|
|
92
|
-
for (ilevel, istart) in enumerate(levels[:-1]):
|
|
93
|
-
istop = levels[ilevel+1]
|
|
94
|
-
|
|
95
|
-
if istop-istart > 1:
|
|
96
|
-
level_values = values[istart:istop]
|
|
97
|
-
iperm_view = iperm[istart:istop]
|
|
98
|
-
|
|
99
|
-
iperm_tmp = level_values.argsort()
|
|
100
|
-
|
|
101
|
-
level_values[:] = level_values[iperm_tmp]
|
|
102
|
-
iperm_view[:] = iperm_view[iperm_tmp]
|
|
103
|
-
|
|
104
|
-
levels_tmp.append(istart)
|
|
105
|
-
vref = values[istart]
|
|
106
|
-
|
|
107
|
-
for idx in range(istart, istop):
|
|
108
|
-
cur_val = values[idx]
|
|
109
|
-
if np.abs(cur_val - vref) > atol:
|
|
110
|
-
levels_tmp.append(idx)
|
|
111
|
-
vref = cur_val
|
|
112
|
-
|
|
113
|
-
else:
|
|
114
|
-
levels_tmp.append(levels[ilevel])
|
|
115
|
-
if len(levels_tmp) == nv:
|
|
116
|
-
# No duplicate rows
|
|
117
|
-
# if verbose:
|
|
118
|
-
# LOG.debug "\t -> No duplicate _vertices detected :)"
|
|
119
|
-
newID = np.arange(nv)
|
|
120
|
-
|
|
121
|
-
levels_tmp.append(nv)
|
|
122
|
-
levels = levels_tmp
|
|
123
|
-
|
|
124
|
-
else:
|
|
125
|
-
# Building the new merged node list
|
|
126
|
-
arr_tmp = []
|
|
127
|
-
newID = np.arange(nv)
|
|
128
|
-
for (ilevel, istart) in enumerate(levels[:-1]):
|
|
129
|
-
istop = levels[ilevel+1]
|
|
130
|
-
|
|
131
|
-
arr_tmp.append(arr[iperm[istart]])
|
|
132
|
-
newID[iperm[list(range(istart, istop))]] = ilevel
|
|
133
|
-
arr = np.array(arr_tmp, dtype=float)
|
|
134
|
-
# Applying renumbering to cells
|
|
135
|
-
# if F is not None:
|
|
136
|
-
# for cell in F:
|
|
137
|
-
# cell[:] = newID[cell]
|
|
138
|
-
|
|
139
|
-
# if verbose:
|
|
140
|
-
# nv_new = arr.shape[0]
|
|
141
|
-
# LOG.debug "\t -> Initial number of nodes : {:d}".format(nv)
|
|
142
|
-
# LOG.debug "\t -> New number of nodes : {:d}".format(nv_new)
|
|
143
|
-
# LOG.debug "\t -> {:d} nodes have been merged".format(nv-nv_new)
|
|
144
|
-
|
|
145
|
-
# if F is not None:
|
|
146
|
-
# if return_index:
|
|
147
|
-
# return arr, F, newID
|
|
148
|
-
# else:
|
|
149
|
-
# return arr, F
|
|
150
|
-
# else:
|
|
151
|
-
return arr, newID
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@inplace_transformation
|
|
155
|
-
def heal_normals(mesh):
|
|
156
|
-
"""Heals the mesh's normals orientations so that they have a consistent orientation and try to make them outward.
|
|
157
|
-
"""
|
|
158
|
-
# TODO: return the different groups of a mesh in case it is made of several unrelated groups
|
|
159
|
-
|
|
160
|
-
nv = mesh.nb_vertices
|
|
161
|
-
nf = mesh.nb_faces
|
|
162
|
-
faces = mesh._faces
|
|
163
|
-
|
|
164
|
-
# Building connectivities
|
|
165
|
-
connectivities = compute_connectivity(mesh)
|
|
166
|
-
v_v = connectivities["v_v"]
|
|
167
|
-
v_f = connectivities["v_f"]
|
|
168
|
-
f_f = connectivities["f_f"]
|
|
169
|
-
boundaries = connectivities["boundaries"]
|
|
170
|
-
|
|
171
|
-
if len(boundaries) > 0:
|
|
172
|
-
mesh_closed = False
|
|
173
|
-
else:
|
|
174
|
-
mesh_closed = True
|
|
175
|
-
|
|
176
|
-
# Flooding the mesh to find inconsistent normals
|
|
177
|
-
type_cell = np.zeros(nf, dtype=np.int32)
|
|
178
|
-
type_cell[:] = 4
|
|
179
|
-
type_cell[mesh.triangles_ids] = 3
|
|
180
|
-
|
|
181
|
-
f_vis = np.zeros(nf, dtype=bool)
|
|
182
|
-
f_vis[0] = True
|
|
183
|
-
stack = [0]
|
|
184
|
-
nb_reversed = 0
|
|
185
|
-
while 1:
|
|
186
|
-
if len(stack) == 0:
|
|
187
|
-
if np.any(np.logical_not(f_vis)):
|
|
188
|
-
iface = np.where(np.logical_not(f_vis))[0][0]
|
|
189
|
-
stack.append(iface)
|
|
190
|
-
f_vis[iface] = True
|
|
191
|
-
else:
|
|
192
|
-
break
|
|
193
|
-
|
|
194
|
-
iface = stack.pop()
|
|
195
|
-
face = faces[iface]
|
|
196
|
-
s1 = set(face)
|
|
197
|
-
|
|
198
|
-
for iadj_f in f_f[iface]:
|
|
199
|
-
if f_vis[iadj_f]:
|
|
200
|
-
continue
|
|
201
|
-
f_vis[iadj_f] = True
|
|
202
|
-
# Removing the other pointer
|
|
203
|
-
f_f[iadj_f].remove(iface) # So as it won't go from iadj_f to iface in the future
|
|
204
|
-
|
|
205
|
-
# Shared vertices
|
|
206
|
-
adjface = faces[iadj_f]
|
|
207
|
-
s2 = set(adjface)
|
|
208
|
-
# try:
|
|
209
|
-
common_vertices = list(s1 & s2)
|
|
210
|
-
if len(common_vertices) == 2:
|
|
211
|
-
i_v1, i_v2 = common_vertices
|
|
212
|
-
else:
|
|
213
|
-
LOG.warning('faces %u and %u have more than 2 vertices in common !', iface, iadj_f)
|
|
214
|
-
continue
|
|
215
|
-
|
|
216
|
-
# Checking normal consistency
|
|
217
|
-
face_ref = np.roll(face[:type_cell[iface]], -np.where(face == i_v1)[0][0])
|
|
218
|
-
adj_face_ref = np.roll(adjface[:type_cell[iadj_f]], -np.where(adjface == i_v1)[0][0])
|
|
219
|
-
|
|
220
|
-
if face_ref[1] == i_v2:
|
|
221
|
-
i = 1
|
|
222
|
-
else:
|
|
223
|
-
i = -1
|
|
224
|
-
|
|
225
|
-
if adj_face_ref[i] == i_v2:
|
|
226
|
-
# Reversing normal
|
|
227
|
-
nb_reversed += 1
|
|
228
|
-
faces[iadj_f] = np.flipud(faces[iadj_f])
|
|
229
|
-
|
|
230
|
-
# Appending to the stack
|
|
231
|
-
stack.append(iadj_f)
|
|
232
|
-
|
|
233
|
-
LOG.debug("* Healing normals to make them consistent and if possible outward")
|
|
234
|
-
if nb_reversed > 0:
|
|
235
|
-
LOG.debug('\t--> %u faces have been reversed to make normals consistent across the mesh' % (nb_reversed))
|
|
236
|
-
else:
|
|
237
|
-
LOG.debug("\t--> Normals orientations are consistent")
|
|
238
|
-
|
|
239
|
-
mesh._faces = faces
|
|
240
|
-
|
|
241
|
-
# Checking if the normals are outward
|
|
242
|
-
if mesh_closed:
|
|
243
|
-
zmax = np.max(mesh._vertices[:, 2])
|
|
244
|
-
|
|
245
|
-
areas = mesh.faces_areas
|
|
246
|
-
normals = mesh.faces_normals
|
|
247
|
-
centers = mesh.faces_centers
|
|
248
|
-
# areas, normals, centers = get_all_faces_properties(vertices, faces)
|
|
249
|
-
|
|
250
|
-
hs = (np.array([(centers[:, 2] - zmax) * areas, ] * 3).T * normals).sum(axis=0)
|
|
251
|
-
|
|
252
|
-
tol = 1e-9
|
|
253
|
-
if np.fabs(hs[0]) > tol or np.fabs(hs[1]) > tol:
|
|
254
|
-
LOG.warning("\t--> the mesh does not seem watertight although marked as closed...")
|
|
255
|
-
|
|
256
|
-
if hs[2] < 0:
|
|
257
|
-
flipped = True
|
|
258
|
-
mesh.flip_normals()
|
|
259
|
-
else:
|
|
260
|
-
flipped = False
|
|
261
|
-
|
|
262
|
-
if flipped:
|
|
263
|
-
LOG.debug('\t--> Every normals have been reversed to be outward')
|
|
264
|
-
|
|
265
|
-
else:
|
|
266
|
-
LOG.debug("\t--> Mesh is not closed, Capytaine cannot test if the normals are outward")
|
|
267
|
-
|
|
268
|
-
return mesh
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
@inplace_transformation
|
|
272
|
-
def remove_unused_vertices(mesh):
|
|
273
|
-
"""Removes unused vertices in the mesh in place.
|
|
274
|
-
|
|
275
|
-
Those are vertices that are not used by any face connectivity.
|
|
90
|
+
list[int]
|
|
91
|
+
List of indices of non-coplanar quadrilateral faces.
|
|
276
92
|
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if nb_used_v < nv:
|
|
286
|
-
new_id__v = np.arange(nv)
|
|
287
|
-
new_id__v[used_v] = np.arange(nb_used_v)
|
|
288
|
-
faces = new_id__v[faces]
|
|
289
|
-
vertices = vertices[used_v]
|
|
290
|
-
|
|
291
|
-
mesh._vertices, mesh._faces = vertices, faces
|
|
292
|
-
|
|
293
|
-
LOG.debug("* Removing unused vertices in the mesh:")
|
|
294
|
-
if nb_used_v < nv:
|
|
295
|
-
unused_v = np.where(np.logical_not(used_v))[0]
|
|
296
|
-
vlist_str = '[' + ', '.join(str(iV) for iV in unused_v) + ']'
|
|
297
|
-
LOG.debug("\t--> %u unused vertices have been removed" % (nv - nb_used_v))
|
|
298
|
-
else:
|
|
299
|
-
LOG.debug("\t--> No unused vertices")
|
|
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
|
|
300
101
|
|
|
301
|
-
return mesh
|
|
302
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)
|
|
303
109
|
|
|
304
|
-
@inplace_transformation
|
|
305
|
-
def heal_triangles(mesh):
|
|
306
|
-
"""Makes the triangle connectivity consistent (in place).
|
|
307
110
|
|
|
308
|
-
|
|
111
|
+
def indices_of_non_convex_faces(vertices, faces):
|
|
309
112
|
"""
|
|
310
|
-
faces
|
|
311
|
-
|
|
312
|
-
quads = faces[:, 0] != faces[:, -1]
|
|
313
|
-
nquads_init = sum(quads)
|
|
314
|
-
|
|
315
|
-
faces[quads] = np.roll(faces[quads], 1, axis=1)
|
|
316
|
-
quads = faces[:, 0] != faces[:, -1]
|
|
317
|
-
|
|
318
|
-
faces[quads] = np.roll(faces[quads], 1, axis=1)
|
|
319
|
-
quads = faces[:, 0] != faces[:, -1]
|
|
320
|
-
|
|
321
|
-
faces[quads] = np.roll(faces[quads], 1, axis=1)
|
|
322
|
-
quads = faces[:, 0] != faces[:, -1]
|
|
323
|
-
nquads_final = sum(quads)
|
|
324
|
-
|
|
325
|
-
mesh._faces = faces
|
|
326
|
-
|
|
327
|
-
LOG.debug("* Ensuring consistent definition of triangles:")
|
|
328
|
-
if nquads_final < nquads_init:
|
|
329
|
-
LOG.debug("\t--> %u triangles were described the wrong way and have been corrected" % (
|
|
330
|
-
nquads_init - nquads_final))
|
|
331
|
-
else:
|
|
332
|
-
LOG.debug("\t--> Triangle description is consistent")
|
|
333
|
-
|
|
334
|
-
return mesh
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
@inplace_transformation
|
|
338
|
-
def remove_degenerated_faces(mesh, rtol=1e-5):
|
|
339
|
-
"""Removes tiny triangles from the mesh (in place).
|
|
340
|
-
|
|
341
|
-
Tiny triangles are those whose area is lower than the mean triangle area in the mesh times the relative
|
|
342
|
-
tolerance given.
|
|
113
|
+
Identify indices of quadrilateral faces in the mesh that are not convex.
|
|
343
114
|
|
|
344
115
|
Parameters
|
|
345
116
|
----------
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
"""
|
|
117
|
+
mesh : Mesh
|
|
118
|
+
The input mesh containing faces and vertices.
|
|
349
119
|
|
|
350
|
-
|
|
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
|
|
351
129
|
|
|
352
|
-
|
|
353
|
-
areas = mesh.faces_areas
|
|
354
|
-
area_threshold = areas.mean() * float(rtol)
|
|
130
|
+
verts = extract_face_vertices(vertices, face)
|
|
355
131
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
nb_removed = mesh.nb_faces - faces.shape[0]
|
|
359
|
-
LOG.debug('* Removing degenerated faces')
|
|
360
|
-
if nb_removed > 0:
|
|
361
|
-
LOG.debug('\t-->%u degenerated faces have been removed' % nb_removed)
|
|
362
|
-
else:
|
|
363
|
-
LOG.debug('\t--> No degenerated faces')
|
|
132
|
+
if not is_face_convex(verts):
|
|
133
|
+
indices.append(i)
|
|
364
134
|
|
|
365
|
-
|
|
135
|
+
return indices
|
|
366
136
|
|
|
367
|
-
return mesh
|
|
368
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
|
|
369
146
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
147
|
+
# If you have vertices but zero faces → that's fine
|
|
148
|
+
if len(faces) == 0:
|
|
149
|
+
return True
|
|
373
150
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# http://vtk.org/gitweb?p=VTK.git;a=blob;f=Filters/Verdict/Testing/Python/MeshQuality.py
|
|
379
|
-
polydata = mesh._vtk_polydata()
|
|
380
|
-
quality = vtk.vtkMeshQuality()
|
|
381
|
-
quality.SetInputData(polydata)
|
|
382
|
-
|
|
383
|
-
def DumpQualityStats(iq, arrayname):
|
|
384
|
-
an = iq.GetOutput().GetFieldData().GetArray(arrayname)
|
|
385
|
-
cardinality = an.GetComponent(0, 4)
|
|
386
|
-
range = list()
|
|
387
|
-
range.append(an.GetComponent(0, 0))
|
|
388
|
-
range.append(an.GetComponent(0, 2))
|
|
389
|
-
average = an.GetComponent(0, 1)
|
|
390
|
-
stdDev = math.sqrt(math.fabs(an.GetComponent(0, 3)))
|
|
391
|
-
outStr = '%s%g%s%g\n%s%g%s%g' % (
|
|
392
|
-
' range: ', range[0], ' - ', range[1],
|
|
393
|
-
' average: ', average, ' , standard deviation: ', stdDev)
|
|
394
|
-
return outStr
|
|
395
|
-
|
|
396
|
-
# Here we define the various mesh types and labels for output.
|
|
397
|
-
meshTypes = [
|
|
398
|
-
['Triangle', 'Triangle',
|
|
399
|
-
[['QualityMeasureToArea', ' Area Ratio:'],
|
|
400
|
-
['QualityMeasureToEdgeRatio', ' Edge Ratio:'],
|
|
401
|
-
['QualityMeasureToAspectRatio', ' Aspect Ratio:'],
|
|
402
|
-
['QualityMeasureToRadiusRatio', ' Radius Ratio:'],
|
|
403
|
-
['QualityMeasureToAspectFrobenius', ' Frobenius Norm:'],
|
|
404
|
-
['QualityMeasureToMinAngle', ' Minimal Angle:']
|
|
405
|
-
]
|
|
406
|
-
],
|
|
407
|
-
|
|
408
|
-
['Quad', 'Quadrilateral',
|
|
409
|
-
[['QualityMeasureToArea', ' Area Ratio:'],
|
|
410
|
-
['QualityMeasureToEdgeRatio', ' Edge Ratio:'],
|
|
411
|
-
['QualityMeasureToAspectRatio', ' Aspect Ratio:'],
|
|
412
|
-
['QualityMeasureToRadiusRatio', ' Radius Ratio:'],
|
|
413
|
-
['QualityMeasureToMedAspectFrobenius',
|
|
414
|
-
' Average Frobenius Norm:'],
|
|
415
|
-
['QualityMeasureToMaxAspectFrobenius',
|
|
416
|
-
' Maximal Frobenius Norm:'],
|
|
417
|
-
['QualityMeasureToMinAngle', ' Minimal Angle:']
|
|
418
|
-
]
|
|
419
|
-
]
|
|
420
|
-
]
|
|
421
|
-
res = ''
|
|
422
|
-
if polydata.GetNumberOfCells() > 0:
|
|
423
|
-
for meshType in meshTypes:
|
|
424
|
-
res += '\n%s%s' % (meshType[1], ' quality of the mesh ')
|
|
425
|
-
quality.Update()
|
|
426
|
-
an = quality.GetOutput().GetFieldData().GetArray('Mesh ' + meshType[1] + ' Quality')
|
|
427
|
-
cardinality = an.GetComponent(0, 4)
|
|
428
|
-
|
|
429
|
-
res = ''.join((res, '(%u elements):\n' % cardinality))
|
|
430
|
-
|
|
431
|
-
# res += '('+str(cardinality) +meshType[1]+'):\n'
|
|
432
|
-
|
|
433
|
-
for measure in meshType[2]:
|
|
434
|
-
eval('quality.Set' + meshType[0] + measure[0] + '()')
|
|
435
|
-
quality.Update()
|
|
436
|
-
res += '\n%s\n%s' % (
|
|
437
|
-
measure[1],
|
|
438
|
-
DumpQualityStats(quality, 'Mesh ' + meshType[1] + ' Quality')
|
|
439
|
-
)
|
|
440
|
-
res += '\n'
|
|
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
|
|
441
155
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
http://www.vtk.org/Wiki/images/6/6b/VerdictManual-revA.pdf\n"""
|
|
156
|
+
if min(all_idx) < 0 or max(all_idx) >= len(vertices):
|
|
157
|
+
return False
|
|
445
158
|
|
|
446
|
-
|
|
447
|
-
print(res)
|
|
448
|
-
return
|
|
159
|
+
return True
|
|
@@ -1,63 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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.
|
|
4
14
|
|
|
5
15
|
from abc import ABC
|
|
16
|
+
from typing import Tuple, Union
|
|
17
|
+
|
|
6
18
|
import numpy as np
|
|
7
19
|
|
|
8
20
|
class SurfaceIntegralsMixin(ABC):
|
|
9
|
-
#
|
|
10
|
-
#
|
|
21
|
+
# Defines some methods inherited by AbstractMesh.
|
|
22
|
+
# There are located in this other module just to make the code more tidy.
|
|
11
23
|
|
|
12
24
|
def surface_integral(self, data, **kwargs):
|
|
13
25
|
"""Returns integral of given data along wet surface area."""
|
|
14
26
|
return np.sum(data * self.faces_areas, **kwargs)
|
|
15
27
|
|
|
16
|
-
def waterplane_integral(self, data, **kwargs):
|
|
17
|
-
"""Returns integral of given data along water plane area."""
|
|
18
|
-
return self.surface_integral(self.faces_normals[:,2] * data, **kwargs)
|
|
19
|
-
|
|
20
28
|
@property
|
|
21
|
-
def wet_surface_area(self):
|
|
29
|
+
def wet_surface_area(self) -> float:
|
|
22
30
|
"""Returns wet surface area."""
|
|
23
|
-
return self.surface_integral(1)
|
|
31
|
+
return self.immersed_part().surface_integral(1)
|
|
24
32
|
|
|
25
33
|
@property
|
|
26
|
-
def volumes(self):
|
|
27
|
-
"""Returns volumes using x, y, z components of the mesh.
|
|
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."""
|
|
28
37
|
norm_coord = self.faces_normals * self.faces_centers
|
|
29
|
-
return self.surface_integral(norm_coord.T, axis=1)
|
|
38
|
+
return tuple(self.surface_integral(norm_coord.T, axis=1))
|
|
30
39
|
|
|
31
40
|
@property
|
|
32
|
-
def volume(self):
|
|
41
|
+
def volume(self) -> float:
|
|
33
42
|
"""Returns volume of the mesh."""
|
|
34
43
|
return np.mean(self.volumes)
|
|
35
44
|
|
|
36
|
-
def
|
|
37
|
-
|
|
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
|
|
38
56
|
|
|
39
57
|
@property
|
|
40
|
-
def center_of_buoyancy(self):
|
|
58
|
+
def center_of_buoyancy(self) -> np.ndarray:
|
|
41
59
|
"""Returns center of buoyancy of the mesh."""
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
44
63
|
|
|
45
64
|
@property
|
|
46
|
-
def waterplane_area(self):
|
|
65
|
+
def waterplane_area(self) -> float:
|
|
47
66
|
"""Returns water plane area of the mesh."""
|
|
48
|
-
|
|
49
|
-
return
|
|
67
|
+
immersed_self = self.immersed_part()
|
|
68
|
+
return -immersed_self.waterplane_integral(1)
|
|
50
69
|
|
|
51
70
|
@property
|
|
52
|
-
def waterplane_center(self):
|
|
71
|
+
def waterplane_center(self) -> Union[None, np.ndarray]:
|
|
53
72
|
"""Returns water plane center of the mesh.
|
|
54
|
-
|
|
55
|
-
Note: Returns None if the mesh is full submerged.
|
|
73
|
+
Returns None if the mesh is full submerged.
|
|
56
74
|
"""
|
|
57
|
-
|
|
75
|
+
immersed_self = self.immersed_part()
|
|
76
|
+
waterplane_area = immersed_self.waterplane_area
|
|
58
77
|
if abs(waterplane_area) < 1e-10:
|
|
59
78
|
return None
|
|
60
79
|
else:
|
|
61
|
-
waterplane_center = -
|
|
62
|
-
|
|
80
|
+
waterplane_center = -immersed_self.waterplane_integral(
|
|
81
|
+
immersed_self.faces_centers.T, axis=1) / waterplane_area
|
|
63
82
|
return waterplane_center[:-1]
|