mmgpy 0.5.0__cp311-cp311-manylinux2014_x86_64.manylinux_2_17_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.
- mmgpy/__init__.py +296 -0
- mmgpy/__main__.py +13 -0
- mmgpy/_io.py +535 -0
- mmgpy/_logging.py +290 -0
- mmgpy/_mesh.py +2286 -0
- mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
- mmgpy/_mmgpy.pyi +2140 -0
- mmgpy/_options.py +304 -0
- mmgpy/_progress.py +850 -0
- mmgpy/_pyvista.py +410 -0
- mmgpy/_result.py +143 -0
- mmgpy/_transfer.py +273 -0
- mmgpy/_validation.py +669 -0
- mmgpy/_version.py +3 -0
- mmgpy/_version.py.in +3 -0
- mmgpy/bin/mmg2d_O3 +0 -0
- mmgpy/bin/mmg3d_O3 +0 -0
- mmgpy/bin/mmgs_O3 +0 -0
- mmgpy/interactive/__init__.py +24 -0
- mmgpy/interactive/sizing_editor.py +790 -0
- mmgpy/lagrangian.py +394 -0
- mmgpy/lib/libmmg2d.so +0 -0
- mmgpy/lib/libmmg2d.so.5 +0 -0
- mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
- mmgpy/lib/libmmg3d.so +0 -0
- mmgpy/lib/libmmg3d.so.5 +0 -0
- mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
- mmgpy/lib/libmmgs.so +0 -0
- mmgpy/lib/libmmgs.so.5 +0 -0
- mmgpy/lib/libmmgs.so.5.8.0 +0 -0
- mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
- mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
- mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
- mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
- mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
- mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
- mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
- mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
- mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
- mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
- mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
- mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
- mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
- mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
- mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
- mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
- mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
- mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
- mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
- mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
- mmgpy/lib/libvtksys-9.5.so.1 +0 -0
- mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
- mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
- mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
- mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
- mmgpy/metrics.py +596 -0
- mmgpy/progress.py +69 -0
- mmgpy/py.typed +0 -0
- mmgpy/repair/__init__.py +37 -0
- mmgpy/repair/_core.py +226 -0
- mmgpy/repair/_elements.py +241 -0
- mmgpy/repair/_vertices.py +219 -0
- mmgpy/sizing.py +370 -0
- mmgpy/ui/__init__.py +97 -0
- mmgpy/ui/__main__.py +87 -0
- mmgpy/ui/app.py +1837 -0
- mmgpy/ui/parsers.py +501 -0
- mmgpy/ui/remeshing.py +448 -0
- mmgpy/ui/samples.py +249 -0
- mmgpy/ui/utils.py +280 -0
- mmgpy/ui/viewer.py +587 -0
- mmgpy-0.5.0.dist-info/METADATA +186 -0
- mmgpy-0.5.0.dist-info/RECORD +109 -0
- mmgpy-0.5.0.dist-info/WHEEL +6 -0
- mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
- mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
- share/man/man1/mmg2d.1.gz +0 -0
- share/man/man1/mmg3d.1.gz +0 -0
- share/man/man1/mmgs.1.gz +0 -0
mmgpy/repair/_core.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Core repair functionality for mmgpy.
|
|
2
|
+
|
|
3
|
+
This module provides the auto_repair convenience function and the RepairReport
|
|
4
|
+
dataclass for summarizing repair operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from mmgpy import Mesh
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class RepairReport:
|
|
18
|
+
"""Report summarizing mesh repair operations.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
duplicate_vertices_removed : int
|
|
23
|
+
Number of duplicate vertices removed.
|
|
24
|
+
orphan_vertices_removed : int
|
|
25
|
+
Number of orphan vertices removed.
|
|
26
|
+
degenerate_elements_removed : int
|
|
27
|
+
Number of degenerate elements removed.
|
|
28
|
+
inverted_elements_fixed : int
|
|
29
|
+
Number of inverted elements fixed.
|
|
30
|
+
duplicate_elements_removed : int
|
|
31
|
+
Number of duplicate elements removed.
|
|
32
|
+
vertices_before : int
|
|
33
|
+
Number of vertices before repair.
|
|
34
|
+
vertices_after : int
|
|
35
|
+
Number of vertices after repair.
|
|
36
|
+
elements_before : int
|
|
37
|
+
Number of elements before repair.
|
|
38
|
+
elements_after : int
|
|
39
|
+
Number of elements after repair.
|
|
40
|
+
operations_applied : tuple[str, ...]
|
|
41
|
+
Names of repair operations that were applied.
|
|
42
|
+
|
|
43
|
+
Examples
|
|
44
|
+
--------
|
|
45
|
+
>>> from mmgpy.repair import auto_repair
|
|
46
|
+
>>> mesh, report = auto_repair(mesh)
|
|
47
|
+
>>> print(report)
|
|
48
|
+
RepairReport:
|
|
49
|
+
Duplicate vertices removed: 12
|
|
50
|
+
Orphan vertices removed: 3
|
|
51
|
+
Degenerate elements removed: 2
|
|
52
|
+
Inverted elements fixed: 5
|
|
53
|
+
>>> print(f"Vertices: {report.vertices_before} -> {report.vertices_after}")
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
duplicate_vertices_removed: int = 0
|
|
58
|
+
orphan_vertices_removed: int = 0
|
|
59
|
+
degenerate_elements_removed: int = 0
|
|
60
|
+
inverted_elements_fixed: int = 0
|
|
61
|
+
duplicate_elements_removed: int = 0
|
|
62
|
+
vertices_before: int = 0
|
|
63
|
+
vertices_after: int = 0
|
|
64
|
+
elements_before: int = 0
|
|
65
|
+
elements_after: int = 0
|
|
66
|
+
operations_applied: tuple[str, ...] = field(default_factory=tuple)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def total_repairs(self) -> int:
|
|
70
|
+
"""Total number of repairs made."""
|
|
71
|
+
return (
|
|
72
|
+
self.duplicate_vertices_removed
|
|
73
|
+
+ self.orphan_vertices_removed
|
|
74
|
+
+ self.degenerate_elements_removed
|
|
75
|
+
+ self.inverted_elements_fixed
|
|
76
|
+
+ self.duplicate_elements_removed
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def was_modified(self) -> bool:
|
|
81
|
+
"""True if any repairs were made."""
|
|
82
|
+
return self.total_repairs > 0
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
"""Return a human-readable summary."""
|
|
86
|
+
lines = ["RepairReport:"]
|
|
87
|
+
|
|
88
|
+
if self.duplicate_vertices_removed > 0:
|
|
89
|
+
lines.append(
|
|
90
|
+
f" Duplicate vertices removed: {self.duplicate_vertices_removed}",
|
|
91
|
+
)
|
|
92
|
+
if self.orphan_vertices_removed > 0:
|
|
93
|
+
lines.append(f" Orphan vertices removed: {self.orphan_vertices_removed}")
|
|
94
|
+
if self.degenerate_elements_removed > 0:
|
|
95
|
+
lines.append(
|
|
96
|
+
f" Degenerate elements removed: {self.degenerate_elements_removed}",
|
|
97
|
+
)
|
|
98
|
+
if self.inverted_elements_fixed > 0:
|
|
99
|
+
lines.append(f" Inverted elements fixed: {self.inverted_elements_fixed}")
|
|
100
|
+
if self.duplicate_elements_removed > 0:
|
|
101
|
+
lines.append(
|
|
102
|
+
f" Duplicate elements removed: {self.duplicate_elements_removed}",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if len(lines) == 1:
|
|
106
|
+
lines.append(" No repairs needed")
|
|
107
|
+
else:
|
|
108
|
+
lines.append(
|
|
109
|
+
f" Vertices: {self.vertices_before} -> {self.vertices_after}",
|
|
110
|
+
)
|
|
111
|
+
lines.append(f" Elements: {self.elements_before} -> {self.elements_after}")
|
|
112
|
+
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def auto_repair(
|
|
117
|
+
mesh: Mesh,
|
|
118
|
+
*,
|
|
119
|
+
duplicate_tolerance: float = 1e-10,
|
|
120
|
+
degenerate_tolerance: float = 1e-10,
|
|
121
|
+
) -> tuple[Mesh, RepairReport]:
|
|
122
|
+
"""Apply all safe repair operations to the mesh.
|
|
123
|
+
|
|
124
|
+
This convenience function applies the following repairs in order:
|
|
125
|
+
1. Remove duplicate vertices
|
|
126
|
+
2. Remove degenerate elements
|
|
127
|
+
3. Fix inverted elements
|
|
128
|
+
4. Remove duplicate elements
|
|
129
|
+
5. Remove orphan vertices (cleanup)
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
mesh : Mesh
|
|
134
|
+
The mesh to repair.
|
|
135
|
+
duplicate_tolerance : float, optional
|
|
136
|
+
Tolerance for duplicate vertex detection. Default is 1e-10.
|
|
137
|
+
degenerate_tolerance : float, optional
|
|
138
|
+
Tolerance for degenerate element detection. Default is 1e-10.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
tuple[Mesh, RepairReport]
|
|
143
|
+
The repaired mesh and a report summarizing the operations.
|
|
144
|
+
|
|
145
|
+
Examples
|
|
146
|
+
--------
|
|
147
|
+
>>> from mmgpy import Mesh
|
|
148
|
+
>>> from mmgpy.repair import auto_repair
|
|
149
|
+
>>> mesh = Mesh(vertices, cells)
|
|
150
|
+
>>> mesh, report = auto_repair(mesh)
|
|
151
|
+
>>> if report.was_modified:
|
|
152
|
+
... print(f"Applied {report.total_repairs} repairs")
|
|
153
|
+
... print(report)
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
from mmgpy import MeshKind # noqa: PLC0415
|
|
157
|
+
from mmgpy.repair._elements import ( # noqa: PLC0415
|
|
158
|
+
fix_inverted_elements,
|
|
159
|
+
remove_degenerate_elements,
|
|
160
|
+
remove_duplicate_elements,
|
|
161
|
+
)
|
|
162
|
+
from mmgpy.repair._vertices import ( # noqa: PLC0415
|
|
163
|
+
remove_duplicate_vertices,
|
|
164
|
+
remove_orphan_vertices,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
vertices_before = len(mesh.get_vertices())
|
|
168
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
169
|
+
elements_before = len(mesh.get_tetrahedra())
|
|
170
|
+
else:
|
|
171
|
+
elements_before = len(mesh.get_triangles())
|
|
172
|
+
|
|
173
|
+
operations_applied: list[str] = []
|
|
174
|
+
|
|
175
|
+
mesh, duplicate_verts = remove_duplicate_vertices(
|
|
176
|
+
mesh,
|
|
177
|
+
tolerance=duplicate_tolerance,
|
|
178
|
+
)
|
|
179
|
+
if duplicate_verts > 0:
|
|
180
|
+
operations_applied.append("remove_duplicate_vertices")
|
|
181
|
+
|
|
182
|
+
mesh, degenerate_elems = remove_degenerate_elements(
|
|
183
|
+
mesh,
|
|
184
|
+
tolerance=degenerate_tolerance,
|
|
185
|
+
)
|
|
186
|
+
if degenerate_elems > 0:
|
|
187
|
+
operations_applied.append("remove_degenerate_elements")
|
|
188
|
+
|
|
189
|
+
mesh, inverted_elems = fix_inverted_elements(mesh)
|
|
190
|
+
if inverted_elems > 0:
|
|
191
|
+
operations_applied.append("fix_inverted_elements")
|
|
192
|
+
|
|
193
|
+
mesh, duplicate_elems = remove_duplicate_elements(mesh)
|
|
194
|
+
if duplicate_elems > 0:
|
|
195
|
+
operations_applied.append("remove_duplicate_elements")
|
|
196
|
+
|
|
197
|
+
mesh, orphan_verts = remove_orphan_vertices(mesh)
|
|
198
|
+
if orphan_verts > 0:
|
|
199
|
+
operations_applied.append("remove_orphan_vertices")
|
|
200
|
+
|
|
201
|
+
vertices_after = len(mesh.get_vertices())
|
|
202
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
203
|
+
elements_after = len(mesh.get_tetrahedra())
|
|
204
|
+
else:
|
|
205
|
+
elements_after = len(mesh.get_triangles())
|
|
206
|
+
|
|
207
|
+
report = RepairReport(
|
|
208
|
+
duplicate_vertices_removed=duplicate_verts,
|
|
209
|
+
orphan_vertices_removed=orphan_verts,
|
|
210
|
+
degenerate_elements_removed=degenerate_elems,
|
|
211
|
+
inverted_elements_fixed=inverted_elems,
|
|
212
|
+
duplicate_elements_removed=duplicate_elems,
|
|
213
|
+
vertices_before=vertices_before,
|
|
214
|
+
vertices_after=vertices_after,
|
|
215
|
+
elements_before=elements_before,
|
|
216
|
+
elements_after=elements_after,
|
|
217
|
+
operations_applied=tuple(operations_applied),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return mesh, report
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
__all__ = [
|
|
224
|
+
"RepairReport",
|
|
225
|
+
"auto_repair",
|
|
226
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Element repair operations for mmgpy.
|
|
2
|
+
|
|
3
|
+
This module provides functions for repairing element-related mesh issues
|
|
4
|
+
including degenerate elements, inverted elements, and duplicate elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from mmgpy import Mesh
|
|
15
|
+
|
|
16
|
+
_GEOMETRY_TOLERANCE = 1e-10
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _compute_tetra_volumes(
|
|
20
|
+
vertices: np.ndarray,
|
|
21
|
+
tetrahedra: np.ndarray,
|
|
22
|
+
) -> np.ndarray:
|
|
23
|
+
"""Compute signed volumes for tetrahedra."""
|
|
24
|
+
tet_verts = vertices[tetrahedra]
|
|
25
|
+
v0, v1, v2, v3 = tet_verts[:, 0], tet_verts[:, 1], tet_verts[:, 2], tet_verts[:, 3]
|
|
26
|
+
edge_matrices = np.stack([v1 - v0, v2 - v0, v3 - v0], axis=-1)
|
|
27
|
+
return np.linalg.det(edge_matrices) / 6.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _compute_triangle_areas_2d(
|
|
31
|
+
vertices: np.ndarray,
|
|
32
|
+
triangles: np.ndarray,
|
|
33
|
+
) -> np.ndarray:
|
|
34
|
+
"""Compute signed areas for 2D triangles."""
|
|
35
|
+
tri_verts = vertices[triangles]
|
|
36
|
+
v0, v1, v2 = tri_verts[:, 0], tri_verts[:, 1], tri_verts[:, 2]
|
|
37
|
+
cross_z = (v1[:, 0] - v0[:, 0]) * (v2[:, 1] - v0[:, 1]) - (v2[:, 0] - v0[:, 0]) * (
|
|
38
|
+
v1[:, 1] - v0[:, 1]
|
|
39
|
+
)
|
|
40
|
+
return 0.5 * cross_z
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _compute_triangle_areas_3d(
|
|
44
|
+
vertices: np.ndarray,
|
|
45
|
+
triangles: np.ndarray,
|
|
46
|
+
) -> np.ndarray:
|
|
47
|
+
"""Compute areas for 3D triangles (unsigned)."""
|
|
48
|
+
tri_verts = vertices[triangles]
|
|
49
|
+
v0, v1, v2 = tri_verts[:, 0], tri_verts[:, 1], tri_verts[:, 2]
|
|
50
|
+
edge1 = v1 - v0
|
|
51
|
+
edge2 = v2 - v0
|
|
52
|
+
cross = np.cross(edge1, edge2)
|
|
53
|
+
return 0.5 * np.linalg.norm(cross, axis=1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def remove_degenerate_elements(
|
|
57
|
+
mesh: Mesh,
|
|
58
|
+
tolerance: float = 1e-10,
|
|
59
|
+
) -> tuple[Mesh, int]:
|
|
60
|
+
"""Remove elements with zero or near-zero volume/area.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
mesh : Mesh
|
|
65
|
+
The mesh to repair.
|
|
66
|
+
tolerance : float, optional
|
|
67
|
+
Tolerance for considering an element as degenerate.
|
|
68
|
+
Default is 1e-10.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
tuple[Mesh, int]
|
|
73
|
+
The repaired mesh and the number of elements removed.
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> from mmgpy import Mesh
|
|
78
|
+
>>> from mmgpy.repair import remove_degenerate_elements
|
|
79
|
+
>>> mesh = Mesh(vertices, cells)
|
|
80
|
+
>>> mesh, removed_count = remove_degenerate_elements(mesh)
|
|
81
|
+
>>> print(f"Removed {removed_count} degenerate elements")
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
from mmgpy import Mesh, MeshKind # noqa: PLC0415
|
|
85
|
+
from mmgpy.repair._vertices import remove_orphan_vertices # noqa: PLC0415
|
|
86
|
+
|
|
87
|
+
vertices = mesh.get_vertices()
|
|
88
|
+
|
|
89
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
90
|
+
elements = mesh.get_tetrahedra()
|
|
91
|
+
if len(elements) == 0:
|
|
92
|
+
return mesh, 0
|
|
93
|
+
volumes = _compute_tetra_volumes(vertices, elements)
|
|
94
|
+
valid_mask = np.abs(volumes) >= tolerance
|
|
95
|
+
elif mesh.kind == MeshKind.TRIANGULAR_2D:
|
|
96
|
+
elements = mesh.get_triangles()
|
|
97
|
+
if len(elements) == 0:
|
|
98
|
+
return mesh, 0
|
|
99
|
+
areas = _compute_triangle_areas_2d(vertices, elements)
|
|
100
|
+
valid_mask = np.abs(areas) >= tolerance
|
|
101
|
+
else: # TRIANGULAR_SURFACE
|
|
102
|
+
elements = mesh.get_triangles()
|
|
103
|
+
if len(elements) == 0:
|
|
104
|
+
return mesh, 0
|
|
105
|
+
areas = _compute_triangle_areas_3d(vertices, elements)
|
|
106
|
+
valid_mask = areas >= tolerance
|
|
107
|
+
|
|
108
|
+
removed_count = int(np.sum(~valid_mask))
|
|
109
|
+
|
|
110
|
+
if removed_count == 0:
|
|
111
|
+
return mesh, 0
|
|
112
|
+
|
|
113
|
+
new_elements = elements[valid_mask]
|
|
114
|
+
new_mesh = Mesh(vertices.copy(), new_elements)
|
|
115
|
+
new_mesh, _ = remove_orphan_vertices(new_mesh)
|
|
116
|
+
|
|
117
|
+
return new_mesh, removed_count
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def fix_inverted_elements(mesh: Mesh) -> tuple[Mesh, int]:
|
|
121
|
+
"""Fix inverted elements by reversing their vertex order.
|
|
122
|
+
|
|
123
|
+
Inverted elements have negative volume (tetrahedra) or negative area
|
|
124
|
+
(2D triangles). This function flips the orientation to make them positive.
|
|
125
|
+
|
|
126
|
+
Note
|
|
127
|
+
----
|
|
128
|
+
For surface meshes (TRIANGULAR_SURFACE), this function returns immediately
|
|
129
|
+
without making changes. Surface mesh orientation is determined by normal
|
|
130
|
+
direction which requires additional context (e.g., inside/outside knowledge)
|
|
131
|
+
that cannot be inferred from geometry alone. Use mesh-specific tools for
|
|
132
|
+
surface normal correction if needed.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
mesh : Mesh
|
|
137
|
+
The mesh to repair.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
tuple[Mesh, int]
|
|
142
|
+
The repaired mesh and the number of elements fixed.
|
|
143
|
+
|
|
144
|
+
Examples
|
|
145
|
+
--------
|
|
146
|
+
>>> from mmgpy import Mesh
|
|
147
|
+
>>> from mmgpy.repair import fix_inverted_elements
|
|
148
|
+
>>> mesh = Mesh(vertices, cells)
|
|
149
|
+
>>> mesh, fixed_count = fix_inverted_elements(mesh)
|
|
150
|
+
>>> print(f"Fixed {fixed_count} inverted elements")
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
from mmgpy import Mesh, MeshKind # noqa: PLC0415
|
|
154
|
+
|
|
155
|
+
vertices = mesh.get_vertices()
|
|
156
|
+
|
|
157
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
158
|
+
elements = mesh.get_tetrahedra().copy()
|
|
159
|
+
if len(elements) == 0:
|
|
160
|
+
return mesh, 0
|
|
161
|
+
volumes = _compute_tetra_volumes(vertices, elements)
|
|
162
|
+
inverted_mask = volumes < -_GEOMETRY_TOLERANCE
|
|
163
|
+
elements[inverted_mask] = elements[inverted_mask][:, [0, 2, 1, 3]]
|
|
164
|
+
elif mesh.kind == MeshKind.TRIANGULAR_2D:
|
|
165
|
+
elements = mesh.get_triangles().copy()
|
|
166
|
+
if len(elements) == 0:
|
|
167
|
+
return mesh, 0
|
|
168
|
+
areas = _compute_triangle_areas_2d(vertices, elements)
|
|
169
|
+
inverted_mask = areas < -_GEOMETRY_TOLERANCE
|
|
170
|
+
elements[inverted_mask] = elements[inverted_mask][:, [0, 2, 1]]
|
|
171
|
+
else: # TRIANGULAR_SURFACE
|
|
172
|
+
return mesh, 0
|
|
173
|
+
|
|
174
|
+
fixed_count = int(np.sum(inverted_mask))
|
|
175
|
+
|
|
176
|
+
if fixed_count == 0:
|
|
177
|
+
return mesh, 0
|
|
178
|
+
|
|
179
|
+
new_mesh = Mesh(vertices.copy(), elements)
|
|
180
|
+
return new_mesh, fixed_count
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def remove_duplicate_elements(mesh: Mesh) -> tuple[Mesh, int]:
|
|
184
|
+
"""Remove duplicate elements (same vertices, any order).
|
|
185
|
+
|
|
186
|
+
Elements are considered duplicates if they reference the same set
|
|
187
|
+
of vertices, regardless of the vertex ordering.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
mesh : Mesh
|
|
192
|
+
The mesh to repair.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
tuple[Mesh, int]
|
|
197
|
+
The repaired mesh and the number of elements removed.
|
|
198
|
+
|
|
199
|
+
Examples
|
|
200
|
+
--------
|
|
201
|
+
>>> from mmgpy import Mesh
|
|
202
|
+
>>> from mmgpy.repair import remove_duplicate_elements
|
|
203
|
+
>>> mesh = Mesh(vertices, cells)
|
|
204
|
+
>>> mesh, removed_count = remove_duplicate_elements(mesh)
|
|
205
|
+
>>> print(f"Removed {removed_count} duplicate elements")
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
from mmgpy import Mesh, MeshKind # noqa: PLC0415
|
|
209
|
+
from mmgpy.repair._vertices import remove_orphan_vertices # noqa: PLC0415
|
|
210
|
+
|
|
211
|
+
vertices = mesh.get_vertices()
|
|
212
|
+
|
|
213
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
214
|
+
elements = mesh.get_tetrahedra()
|
|
215
|
+
else:
|
|
216
|
+
elements = mesh.get_triangles()
|
|
217
|
+
|
|
218
|
+
if len(elements) == 0:
|
|
219
|
+
return mesh, 0
|
|
220
|
+
|
|
221
|
+
sorted_elements = np.sort(elements, axis=1)
|
|
222
|
+
_, unique_indices = np.unique(sorted_elements, axis=0, return_index=True)
|
|
223
|
+
unique_indices = np.sort(unique_indices)
|
|
224
|
+
|
|
225
|
+
removed_count = len(elements) - len(unique_indices)
|
|
226
|
+
|
|
227
|
+
if removed_count == 0:
|
|
228
|
+
return mesh, 0
|
|
229
|
+
|
|
230
|
+
new_elements = elements[unique_indices]
|
|
231
|
+
new_mesh = Mesh(vertices.copy(), new_elements)
|
|
232
|
+
new_mesh, _ = remove_orphan_vertices(new_mesh)
|
|
233
|
+
|
|
234
|
+
return new_mesh, removed_count
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
__all__ = [
|
|
238
|
+
"fix_inverted_elements",
|
|
239
|
+
"remove_degenerate_elements",
|
|
240
|
+
"remove_duplicate_elements",
|
|
241
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Vertex repair operations for mmgpy.
|
|
2
|
+
|
|
3
|
+
This module provides functions for repairing vertex-related mesh issues
|
|
4
|
+
including duplicate vertices, orphan vertices, and close vertices.
|
|
5
|
+
|
|
6
|
+
Note:
|
|
7
|
+
Choosing between ``remove_duplicate_vertices`` and ``merge_close_vertices``:
|
|
8
|
+
|
|
9
|
+
- Use ``remove_duplicate_vertices`` (default tolerance=1e-10) for exact or
|
|
10
|
+
near-exact duplicates that result from numerical precision issues or
|
|
11
|
+
mesh generation artifacts.
|
|
12
|
+
|
|
13
|
+
- Use ``merge_close_vertices`` (default tolerance=1e-6) when you want to
|
|
14
|
+
merge vertices that are geometrically close but not necessarily duplicates,
|
|
15
|
+
such as when simplifying meshes or cleaning up imported geometry.
|
|
16
|
+
|
|
17
|
+
The ``auto_repair`` function uses ``remove_duplicate_vertices`` (strict
|
|
18
|
+
tolerance) to avoid unintentionally merging distinct vertices. Call
|
|
19
|
+
``merge_close_vertices`` separately if aggressive vertex merging is desired.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
from scipy.spatial import cKDTree
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from mmgpy import Mesh
|
|
33
|
+
|
|
34
|
+
_logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
_LARGE_PAIR_THRESHOLD = 100_000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def remove_duplicate_vertices(
|
|
40
|
+
mesh: Mesh,
|
|
41
|
+
tolerance: float = 1e-10,
|
|
42
|
+
) -> tuple[Mesh, int]:
|
|
43
|
+
"""Remove exact duplicate vertices from the mesh.
|
|
44
|
+
|
|
45
|
+
Uses KD-tree for O(n log n) duplicate detection. Vertices within
|
|
46
|
+
the tolerance distance are considered duplicates.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
mesh : Mesh
|
|
51
|
+
The mesh to repair.
|
|
52
|
+
tolerance : float, optional
|
|
53
|
+
Distance tolerance for considering vertices as duplicates.
|
|
54
|
+
Default is 1e-10.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
tuple[Mesh, int]
|
|
59
|
+
The repaired mesh and the number of vertices removed.
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
>>> from mmgpy import Mesh
|
|
64
|
+
>>> from mmgpy.repair import remove_duplicate_vertices
|
|
65
|
+
>>> mesh = Mesh(vertices, cells)
|
|
66
|
+
>>> mesh, removed_count = remove_duplicate_vertices(mesh)
|
|
67
|
+
>>> print(f"Removed {removed_count} duplicate vertices")
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
from mmgpy import Mesh, MeshKind # noqa: PLC0415
|
|
71
|
+
|
|
72
|
+
vertices = mesh.get_vertices()
|
|
73
|
+
n_vertices = len(vertices)
|
|
74
|
+
min_vertices_for_duplicates = 2
|
|
75
|
+
|
|
76
|
+
if n_vertices < min_vertices_for_duplicates:
|
|
77
|
+
return mesh, 0
|
|
78
|
+
|
|
79
|
+
tree = cKDTree(vertices)
|
|
80
|
+
pairs = tree.query_pairs(r=tolerance, output_type="ndarray")
|
|
81
|
+
|
|
82
|
+
if len(pairs) == 0:
|
|
83
|
+
return mesh, 0
|
|
84
|
+
|
|
85
|
+
if len(pairs) > _LARGE_PAIR_THRESHOLD:
|
|
86
|
+
_logger.warning(
|
|
87
|
+
"Found %d vertex pairs within tolerance %g. This may indicate "
|
|
88
|
+
"the tolerance is too large or the mesh has many near-coincident "
|
|
89
|
+
"vertices. Consider using a smaller tolerance.",
|
|
90
|
+
len(pairs),
|
|
91
|
+
tolerance,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
mapping = np.arange(n_vertices)
|
|
95
|
+
for i, j in pairs:
|
|
96
|
+
canonical = min(mapping[i], mapping[j])
|
|
97
|
+
mapping[i] = canonical
|
|
98
|
+
mapping[j] = canonical
|
|
99
|
+
|
|
100
|
+
for i in range(n_vertices):
|
|
101
|
+
root = i
|
|
102
|
+
while mapping[root] != root:
|
|
103
|
+
root = mapping[root]
|
|
104
|
+
mapping[i] = root
|
|
105
|
+
|
|
106
|
+
unique_indices = np.where(mapping == np.arange(n_vertices))[0]
|
|
107
|
+
new_vertices = vertices[unique_indices]
|
|
108
|
+
|
|
109
|
+
old_to_new = np.zeros(n_vertices, dtype=np.int32)
|
|
110
|
+
for new_idx, old_idx in enumerate(unique_indices):
|
|
111
|
+
old_to_new[old_idx] = new_idx
|
|
112
|
+
|
|
113
|
+
final_mapping = old_to_new[mapping]
|
|
114
|
+
|
|
115
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
116
|
+
elements = mesh.get_tetrahedra()
|
|
117
|
+
new_elements = final_mapping[elements]
|
|
118
|
+
else:
|
|
119
|
+
elements = mesh.get_triangles()
|
|
120
|
+
new_elements = final_mapping[elements]
|
|
121
|
+
|
|
122
|
+
new_mesh = Mesh(new_vertices, new_elements)
|
|
123
|
+
removed_count = n_vertices - len(new_vertices)
|
|
124
|
+
|
|
125
|
+
return new_mesh, removed_count
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def remove_orphan_vertices(mesh: Mesh) -> tuple[Mesh, int]:
|
|
129
|
+
"""Remove vertices not referenced by any element.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
mesh : Mesh
|
|
134
|
+
The mesh to repair.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
tuple[Mesh, int]
|
|
139
|
+
The repaired mesh and the number of vertices removed.
|
|
140
|
+
|
|
141
|
+
Examples
|
|
142
|
+
--------
|
|
143
|
+
>>> from mmgpy import Mesh
|
|
144
|
+
>>> from mmgpy.repair import remove_orphan_vertices
|
|
145
|
+
>>> mesh = Mesh(vertices, cells)
|
|
146
|
+
>>> mesh, removed_count = remove_orphan_vertices(mesh)
|
|
147
|
+
>>> print(f"Removed {removed_count} orphan vertices")
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
from mmgpy import Mesh, MeshKind # noqa: PLC0415
|
|
151
|
+
|
|
152
|
+
vertices = mesh.get_vertices()
|
|
153
|
+
n_vertices = len(vertices)
|
|
154
|
+
|
|
155
|
+
if mesh.kind == MeshKind.TETRAHEDRAL:
|
|
156
|
+
elements = mesh.get_tetrahedra()
|
|
157
|
+
else:
|
|
158
|
+
elements = mesh.get_triangles()
|
|
159
|
+
|
|
160
|
+
if len(elements) == 0:
|
|
161
|
+
return mesh, 0
|
|
162
|
+
|
|
163
|
+
used_vertices = np.unique(elements.flatten())
|
|
164
|
+
|
|
165
|
+
if len(used_vertices) == n_vertices:
|
|
166
|
+
return mesh, 0
|
|
167
|
+
|
|
168
|
+
new_vertices = vertices[used_vertices]
|
|
169
|
+
|
|
170
|
+
old_to_new = np.full(n_vertices, -1, dtype=np.int32)
|
|
171
|
+
old_to_new[used_vertices] = np.arange(len(used_vertices), dtype=np.int32)
|
|
172
|
+
|
|
173
|
+
new_elements = old_to_new[elements]
|
|
174
|
+
|
|
175
|
+
new_mesh = Mesh(new_vertices, new_elements)
|
|
176
|
+
removed_count = n_vertices - len(new_vertices)
|
|
177
|
+
|
|
178
|
+
return new_mesh, removed_count
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def merge_close_vertices(
|
|
182
|
+
mesh: Mesh,
|
|
183
|
+
tolerance: float = 1e-6,
|
|
184
|
+
) -> tuple[Mesh, int]:
|
|
185
|
+
"""Merge vertices that are within tolerance distance of each other.
|
|
186
|
+
|
|
187
|
+
Similar to remove_duplicate_vertices but with a larger default tolerance.
|
|
188
|
+
This is useful for meshes with vertices that are nearly coincident but
|
|
189
|
+
not exactly duplicate.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
mesh : Mesh
|
|
194
|
+
The mesh to repair.
|
|
195
|
+
tolerance : float, optional
|
|
196
|
+
Distance tolerance for merging vertices. Default is 1e-6.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
tuple[Mesh, int]
|
|
201
|
+
The repaired mesh and the number of vertices merged.
|
|
202
|
+
|
|
203
|
+
Examples
|
|
204
|
+
--------
|
|
205
|
+
>>> from mmgpy import Mesh
|
|
206
|
+
>>> from mmgpy.repair import merge_close_vertices
|
|
207
|
+
>>> mesh = Mesh(vertices, cells)
|
|
208
|
+
>>> mesh, merged_count = merge_close_vertices(mesh, tolerance=1e-6)
|
|
209
|
+
>>> print(f"Merged {merged_count} close vertices")
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
return remove_duplicate_vertices(mesh, tolerance=tolerance)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
__all__ = [
|
|
216
|
+
"merge_close_vertices",
|
|
217
|
+
"remove_duplicate_vertices",
|
|
218
|
+
"remove_orphan_vertices",
|
|
219
|
+
]
|