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.
Files changed (109) hide show
  1. mmgpy/__init__.py +296 -0
  2. mmgpy/__main__.py +13 -0
  3. mmgpy/_io.py +535 -0
  4. mmgpy/_logging.py +290 -0
  5. mmgpy/_mesh.py +2286 -0
  6. mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
  7. mmgpy/_mmgpy.pyi +2140 -0
  8. mmgpy/_options.py +304 -0
  9. mmgpy/_progress.py +850 -0
  10. mmgpy/_pyvista.py +410 -0
  11. mmgpy/_result.py +143 -0
  12. mmgpy/_transfer.py +273 -0
  13. mmgpy/_validation.py +669 -0
  14. mmgpy/_version.py +3 -0
  15. mmgpy/_version.py.in +3 -0
  16. mmgpy/bin/mmg2d_O3 +0 -0
  17. mmgpy/bin/mmg3d_O3 +0 -0
  18. mmgpy/bin/mmgs_O3 +0 -0
  19. mmgpy/interactive/__init__.py +24 -0
  20. mmgpy/interactive/sizing_editor.py +790 -0
  21. mmgpy/lagrangian.py +394 -0
  22. mmgpy/lib/libmmg2d.so +0 -0
  23. mmgpy/lib/libmmg2d.so.5 +0 -0
  24. mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
  25. mmgpy/lib/libmmg3d.so +0 -0
  26. mmgpy/lib/libmmg3d.so.5 +0 -0
  27. mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
  28. mmgpy/lib/libmmgs.so +0 -0
  29. mmgpy/lib/libmmgs.so.5 +0 -0
  30. mmgpy/lib/libmmgs.so.5.8.0 +0 -0
  31. mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
  32. mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
  33. mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
  34. mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
  35. mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
  36. mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
  37. mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
  38. mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
  39. mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
  40. mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
  41. mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
  42. mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
  43. mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
  44. mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
  45. mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
  46. mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
  47. mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
  48. mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
  49. mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
  50. mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
  51. mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
  52. mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
  53. mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
  54. mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
  55. mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
  56. mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
  57. mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
  58. mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
  59. mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
  60. mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
  61. mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
  62. mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
  63. mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
  64. mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
  65. mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
  66. mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
  67. mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
  68. mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
  69. mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
  70. mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
  71. mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
  72. mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
  73. mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
  74. mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
  75. mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
  76. mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
  77. mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
  78. mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
  79. mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
  80. mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
  81. mmgpy/lib/libvtksys-9.5.so.1 +0 -0
  82. mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
  83. mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
  84. mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
  85. mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
  86. mmgpy/metrics.py +596 -0
  87. mmgpy/progress.py +69 -0
  88. mmgpy/py.typed +0 -0
  89. mmgpy/repair/__init__.py +37 -0
  90. mmgpy/repair/_core.py +226 -0
  91. mmgpy/repair/_elements.py +241 -0
  92. mmgpy/repair/_vertices.py +219 -0
  93. mmgpy/sizing.py +370 -0
  94. mmgpy/ui/__init__.py +97 -0
  95. mmgpy/ui/__main__.py +87 -0
  96. mmgpy/ui/app.py +1837 -0
  97. mmgpy/ui/parsers.py +501 -0
  98. mmgpy/ui/remeshing.py +448 -0
  99. mmgpy/ui/samples.py +249 -0
  100. mmgpy/ui/utils.py +280 -0
  101. mmgpy/ui/viewer.py +587 -0
  102. mmgpy-0.5.0.dist-info/METADATA +186 -0
  103. mmgpy-0.5.0.dist-info/RECORD +109 -0
  104. mmgpy-0.5.0.dist-info/WHEEL +6 -0
  105. mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
  106. mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
  107. share/man/man1/mmg2d.1.gz +0 -0
  108. share/man/man1/mmg3d.1.gz +0 -0
  109. 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
+ ]