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/_validation.py ADDED
@@ -0,0 +1,669 @@
1
+ """Mesh validation module for mmgpy.
2
+
3
+ This module provides mesh validation capabilities including geometry checks,
4
+ topology checks, and quality assessment.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import TYPE_CHECKING
12
+
13
+ import numpy as np
14
+ from scipy.spatial import cKDTree
15
+
16
+ if TYPE_CHECKING:
17
+ from numpy.typing import NDArray
18
+
19
+ from ._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
20
+
21
+ # Tolerance for geometry checks (volume/area near zero)
22
+ _GEOMETRY_TOLERANCE = 1e-15
23
+
24
+ # Maximum number of issues to display in summary
25
+ _MAX_DISPLAYED_ISSUES = 10
26
+
27
+ # Maximum faces per edge for manifold mesh
28
+ _MAX_FACES_PER_MANIFOLD_EDGE = 2
29
+
30
+ # Minimum vertices for duplicate check
31
+ _MIN_VERTICES_FOR_DUPLICATE_CHECK = 2
32
+
33
+
34
+ class IssueSeverity(Enum):
35
+ """Severity level for validation issues."""
36
+
37
+ ERROR = "error"
38
+ WARNING = "warning"
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class ValidationIssue:
43
+ """A single validation issue found in the mesh.
44
+
45
+ Attributes
46
+ ----------
47
+ severity : IssueSeverity
48
+ Whether this is an error (mesh unusable) or warning (may cause issues).
49
+ check_name : str
50
+ Name of the validation check that found this issue.
51
+ message : str
52
+ Human-readable description of the issue.
53
+ element_ids : tuple[int, ...]
54
+ Indices of affected elements (empty for global issues).
55
+
56
+ """
57
+
58
+ severity: IssueSeverity
59
+ check_name: str
60
+ message: str
61
+ element_ids: tuple[int, ...] = field(default_factory=tuple)
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class QualityStats:
66
+ """Statistics about mesh element quality.
67
+
68
+ Quality values are normalized to [0, 1] where 1 is a perfect element.
69
+
70
+ Attributes
71
+ ----------
72
+ min : float
73
+ Minimum element quality.
74
+ max : float
75
+ Maximum element quality.
76
+ mean : float
77
+ Mean element quality.
78
+ std : float
79
+ Standard deviation of element quality.
80
+ histogram : tuple[tuple[str, int], ...]
81
+ Quality distribution as (bin_label, count) pairs.
82
+
83
+ """
84
+
85
+ min: float
86
+ max: float
87
+ mean: float
88
+ std: float
89
+ histogram: tuple[tuple[str, int], ...]
90
+
91
+ def below_threshold(self, threshold: float) -> int:
92
+ """Count elements below a quality threshold.
93
+
94
+ Parameters
95
+ ----------
96
+ threshold : float
97
+ Quality threshold (0-1).
98
+
99
+ Returns
100
+ -------
101
+ int
102
+ Number of elements with quality below threshold.
103
+
104
+ """
105
+ total = 0
106
+ for bin_label, count in self.histogram:
107
+ bin_upper = float(bin_label.split("-")[1])
108
+ if bin_upper <= threshold:
109
+ total += count
110
+ return total
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class ValidationReport:
115
+ """Complete validation report for a mesh.
116
+
117
+ Attributes
118
+ ----------
119
+ is_valid : bool
120
+ True if no errors were found (warnings are OK).
121
+ issues : tuple[ValidationIssue, ...]
122
+ All validation issues found.
123
+ quality : QualityStats | None
124
+ Quality statistics (None if quality check was skipped).
125
+ mesh_type : str
126
+ Type of mesh that was validated.
127
+
128
+ """
129
+
130
+ is_valid: bool
131
+ issues: tuple[ValidationIssue, ...]
132
+ quality: QualityStats | None
133
+ mesh_type: str
134
+
135
+ @property
136
+ def errors(self) -> list[ValidationIssue]:
137
+ """Get all error-level issues."""
138
+ return [i for i in self.issues if i.severity == IssueSeverity.ERROR]
139
+
140
+ @property
141
+ def warnings(self) -> list[ValidationIssue]:
142
+ """Get all warning-level issues."""
143
+ return [i for i in self.issues if i.severity == IssueSeverity.WARNING]
144
+
145
+ def __str__(self) -> str:
146
+ """Return a human-readable summary."""
147
+ lines = [
148
+ f"ValidationReport({self.mesh_type}):",
149
+ f" Valid: {self.is_valid}",
150
+ f" Errors: {len(self.errors)}",
151
+ f" Warnings: {len(self.warnings)}",
152
+ ]
153
+ if self.quality:
154
+ lines.extend(
155
+ [
156
+ " Quality:",
157
+ f" Min: {self.quality.min:.3f}",
158
+ f" Max: {self.quality.max:.3f}",
159
+ f" Mean: {self.quality.mean:.3f}",
160
+ ],
161
+ )
162
+ if self.issues:
163
+ lines.append(" Issues:")
164
+ for issue in self.issues[:_MAX_DISPLAYED_ISSUES]:
165
+ severity = "ERROR" if issue.severity == IssueSeverity.ERROR else "WARN"
166
+ lines.append(f" [{severity}] {issue.message}")
167
+ if len(self.issues) > _MAX_DISPLAYED_ISSUES:
168
+ remaining = len(self.issues) - _MAX_DISPLAYED_ISSUES
169
+ lines.append(f" ... and {remaining} more")
170
+ return "\n".join(lines)
171
+
172
+
173
+ class ValidationError(Exception):
174
+ """Exception raised when strict validation fails."""
175
+
176
+ def __init__(self, report: ValidationReport) -> None:
177
+ """Initialize with a validation report."""
178
+ self.report = report
179
+ issues = report.errors if report.errors else report.warnings
180
+ messages = [i.message for i in issues[:3]]
181
+ super().__init__(f"Mesh validation failed: {'; '.join(messages)}")
182
+
183
+
184
+ def _compute_quality_stats(qualities: NDArray[np.float64]) -> QualityStats:
185
+ """Compute quality statistics from an array of quality values."""
186
+ if len(qualities) == 0:
187
+ return QualityStats(
188
+ min=0.0,
189
+ max=0.0,
190
+ mean=0.0,
191
+ std=0.0,
192
+ histogram=(),
193
+ )
194
+
195
+ # Compute histogram with fixed bins
196
+ bins = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
197
+ hist_counts, _ = np.histogram(qualities, bins=bins)
198
+
199
+ histogram = []
200
+ for i, count in enumerate(hist_counts):
201
+ bin_label = f"{bins[i]:.1f}-{bins[i + 1]:.1f}"
202
+ histogram.append((bin_label, int(count)))
203
+
204
+ return QualityStats(
205
+ min=float(np.min(qualities)),
206
+ max=float(np.max(qualities)),
207
+ mean=float(np.mean(qualities)),
208
+ std=float(np.std(qualities)),
209
+ histogram=tuple(histogram),
210
+ )
211
+
212
+
213
+ def _check_geometry_3d(
214
+ mesh: MmgMesh3D,
215
+ issues: list[ValidationIssue],
216
+ ) -> None:
217
+ """Check 3D mesh geometry (positive volumes, valid areas)."""
218
+ vertices = mesh.get_vertices()
219
+ elements = mesh.get_tetrahedra()
220
+
221
+ if len(elements) == 0:
222
+ return
223
+
224
+ # Vectorized volume computation for all tetrahedra
225
+ # Get vertices for each tetrahedron: shape (n_elements, 4, 3)
226
+ tet_verts = vertices[elements]
227
+ v0, v1, v2, v3 = tet_verts[:, 0], tet_verts[:, 1], tet_verts[:, 2], tet_verts[:, 3]
228
+
229
+ # Build edge vectors: shape (n_elements, 3, 3)
230
+ edge_matrices = np.stack([v1 - v0, v2 - v0, v3 - v0], axis=-1)
231
+
232
+ # Compute signed volumes using determinant
233
+ volumes = np.linalg.det(edge_matrices) / 6.0
234
+
235
+ # Find inverted and degenerate elements
236
+ inverted_mask = volumes < -_GEOMETRY_TOLERANCE
237
+ degenerate_mask = np.abs(volumes) < _GEOMETRY_TOLERANCE
238
+
239
+ inverted_indices = np.where(inverted_mask)[0]
240
+ degenerate_indices = np.where(degenerate_mask)[0]
241
+
242
+ if len(inverted_indices) > 0:
243
+ issues.append(
244
+ ValidationIssue(
245
+ severity=IssueSeverity.ERROR,
246
+ check_name="inverted_elements",
247
+ message=f"Found {len(inverted_indices)} inverted tetrahedra "
248
+ f"with negative volume",
249
+ element_ids=tuple(int(i) for i in inverted_indices[:100]),
250
+ ),
251
+ )
252
+
253
+ if len(degenerate_indices) > 0:
254
+ issues.append(
255
+ ValidationIssue(
256
+ severity=IssueSeverity.WARNING,
257
+ check_name="degenerate_elements",
258
+ message=f"Found {len(degenerate_indices)} degenerate tetrahedra "
259
+ f"with near-zero volume",
260
+ element_ids=tuple(int(i) for i in degenerate_indices[:100]),
261
+ ),
262
+ )
263
+
264
+
265
+ def _check_geometry_2d(
266
+ mesh: MmgMesh2D,
267
+ issues: list[ValidationIssue],
268
+ ) -> None:
269
+ """Check 2D mesh geometry (positive areas)."""
270
+ vertices = mesh.get_vertices()
271
+ triangles = mesh.get_triangles()
272
+
273
+ if len(triangles) == 0:
274
+ return
275
+
276
+ # Vectorized area computation for all triangles
277
+ # Get vertices for each triangle: shape (n_elements, 3, 2)
278
+ tri_verts = vertices[triangles]
279
+ v0, v1, v2 = tri_verts[:, 0], tri_verts[:, 1], tri_verts[:, 2]
280
+
281
+ # Compute signed area using cross product (2D)
282
+ cross_z = (v1[:, 0] - v0[:, 0]) * (v2[:, 1] - v0[:, 1]) - (v2[:, 0] - v0[:, 0]) * (
283
+ v1[:, 1] - v0[:, 1]
284
+ )
285
+ areas = 0.5 * cross_z
286
+
287
+ # Find inverted and degenerate elements
288
+ inverted_mask = areas < -_GEOMETRY_TOLERANCE
289
+ degenerate_mask = np.abs(areas) < _GEOMETRY_TOLERANCE
290
+
291
+ inverted_indices = np.where(inverted_mask)[0]
292
+ degenerate_indices = np.where(degenerate_mask)[0]
293
+
294
+ if len(inverted_indices) > 0:
295
+ issues.append(
296
+ ValidationIssue(
297
+ severity=IssueSeverity.ERROR,
298
+ check_name="inverted_elements",
299
+ message=f"Found {len(inverted_indices)} inverted triangles "
300
+ f"with negative area",
301
+ element_ids=tuple(int(i) for i in inverted_indices[:100]),
302
+ ),
303
+ )
304
+
305
+ if len(degenerate_indices) > 0:
306
+ issues.append(
307
+ ValidationIssue(
308
+ severity=IssueSeverity.WARNING,
309
+ check_name="degenerate_elements",
310
+ message=f"Found {len(degenerate_indices)} degenerate triangles "
311
+ f"with near-zero area",
312
+ element_ids=tuple(int(i) for i in degenerate_indices[:100]),
313
+ ),
314
+ )
315
+
316
+
317
+ def _check_geometry_surface(
318
+ mesh: MmgMeshS,
319
+ issues: list[ValidationIssue],
320
+ ) -> None:
321
+ """Check surface mesh geometry (positive areas)."""
322
+ vertices = mesh.get_vertices()
323
+ triangles = mesh.get_triangles()
324
+
325
+ if len(triangles) == 0:
326
+ return
327
+
328
+ # Vectorized area computation for all triangles
329
+ # Get vertices for each triangle: shape (n_elements, 3, 3)
330
+ tri_verts = vertices[triangles]
331
+ v0, v1, v2 = tri_verts[:, 0], tri_verts[:, 1], tri_verts[:, 2]
332
+
333
+ # Compute area using cross product (3D)
334
+ edge1 = v1 - v0
335
+ edge2 = v2 - v0
336
+ cross = np.cross(edge1, edge2)
337
+ areas = 0.5 * np.linalg.norm(cross, axis=1)
338
+
339
+ # Find degenerate elements
340
+ degenerate_mask = areas < _GEOMETRY_TOLERANCE
341
+ degenerate_indices = np.where(degenerate_mask)[0]
342
+
343
+ if len(degenerate_indices) > 0:
344
+ issues.append(
345
+ ValidationIssue(
346
+ severity=IssueSeverity.WARNING,
347
+ check_name="degenerate_elements",
348
+ message=f"Found {len(degenerate_indices)} degenerate triangles "
349
+ f"with near-zero area",
350
+ element_ids=tuple(int(i) for i in degenerate_indices[:100]),
351
+ ),
352
+ )
353
+
354
+
355
+ def _check_topology_surface(
356
+ mesh: MmgMeshS | MmgMesh2D,
357
+ issues: list[ValidationIssue],
358
+ mesh_type: str,
359
+ ) -> None:
360
+ """Check surface/2D mesh topology (manifold edges, watertight)."""
361
+ triangles = mesh.get_triangles()
362
+
363
+ if len(triangles) == 0:
364
+ return
365
+
366
+ # Build edge-to-face mapping
367
+ edge_faces: dict[tuple[int, int], list[int]] = {}
368
+ for face_idx, tri in enumerate(triangles):
369
+ for j in range(3):
370
+ v1, v2 = int(tri[j]), int(tri[(j + 1) % 3])
371
+ edge = (min(v1, v2), max(v1, v2))
372
+ if edge not in edge_faces:
373
+ edge_faces[edge] = []
374
+ edge_faces[edge].append(face_idx)
375
+
376
+ # Check for non-manifold edges (shared by >2 faces)
377
+ non_manifold_edges = []
378
+ boundary_edges = []
379
+
380
+ for edge, faces in edge_faces.items():
381
+ if len(faces) > _MAX_FACES_PER_MANIFOLD_EDGE:
382
+ non_manifold_edges.append(edge)
383
+ elif len(faces) == 1:
384
+ boundary_edges.append(edge)
385
+
386
+ if non_manifold_edges:
387
+ issues.append(
388
+ ValidationIssue(
389
+ severity=IssueSeverity.ERROR,
390
+ check_name="non_manifold_edges",
391
+ message=f"Found {len(non_manifold_edges)} non-manifold edges "
392
+ f"(shared by more than 2 faces)",
393
+ element_ids=(),
394
+ ),
395
+ )
396
+
397
+ if boundary_edges and mesh_type == "surface":
398
+ issues.append(
399
+ ValidationIssue(
400
+ severity=IssueSeverity.WARNING,
401
+ check_name="open_boundary",
402
+ message=f"Found {len(boundary_edges)} boundary edges "
403
+ f"(mesh is not watertight)",
404
+ element_ids=(),
405
+ ),
406
+ )
407
+
408
+
409
+ def _check_orphan_vertices(
410
+ vertices: NDArray[np.float64],
411
+ elements: NDArray[np.int32],
412
+ issues: list[ValidationIssue],
413
+ ) -> None:
414
+ """Check for vertices not referenced by any element."""
415
+ if len(vertices) == 0 or len(elements) == 0:
416
+ return
417
+
418
+ used_vertices = set(elements.flatten())
419
+ all_vertices = set(range(len(vertices)))
420
+ orphan_vertices = all_vertices - used_vertices
421
+
422
+ if orphan_vertices:
423
+ issues.append(
424
+ ValidationIssue(
425
+ severity=IssueSeverity.WARNING,
426
+ check_name="orphan_vertices",
427
+ message=f"Found {len(orphan_vertices)} orphan vertices "
428
+ f"not referenced by any element",
429
+ element_ids=tuple(sorted(orphan_vertices)[:100]),
430
+ ),
431
+ )
432
+
433
+
434
+ def _check_quality(
435
+ qualities: NDArray[np.float64],
436
+ min_quality: float,
437
+ issues: list[ValidationIssue],
438
+ ) -> None:
439
+ """Check element quality against threshold."""
440
+ if len(qualities) == 0:
441
+ return
442
+
443
+ low_quality_mask = qualities < min_quality
444
+ low_quality_count = int(np.sum(low_quality_mask))
445
+
446
+ if low_quality_count > 0:
447
+ low_quality_indices = np.where(low_quality_mask)[0]
448
+ issues.append(
449
+ ValidationIssue(
450
+ severity=IssueSeverity.WARNING,
451
+ check_name="low_quality",
452
+ message=f"Found {low_quality_count} elements with quality "
453
+ f"below {min_quality:.2f}",
454
+ element_ids=tuple(int(i) for i in low_quality_indices[:100]),
455
+ ),
456
+ )
457
+
458
+
459
+ def _check_duplicate_vertices(
460
+ vertices: NDArray[np.float64],
461
+ issues: list[ValidationIssue],
462
+ tolerance: float = 1e-10,
463
+ ) -> None:
464
+ """Check for duplicate (coincident) vertices using KD-tree.
465
+
466
+ Time complexity: O(n log n) for tree construction, O(n) expected for queries.
467
+ Space complexity: O(n)
468
+ """
469
+ if len(vertices) < _MIN_VERTICES_FOR_DUPLICATE_CHECK:
470
+ return
471
+
472
+ tree = cKDTree(vertices)
473
+ pairs = tree.query_pairs(r=tolerance, output_type="ndarray")
474
+
475
+ if len(pairs) > 0:
476
+ issues.append(
477
+ ValidationIssue(
478
+ severity=IssueSeverity.WARNING,
479
+ check_name="duplicate_vertices",
480
+ message=f"Found {len(pairs)} duplicate vertex pairs "
481
+ f"within tolerance {tolerance}",
482
+ element_ids=(),
483
+ ),
484
+ )
485
+
486
+
487
+ def validate_mesh_3d(
488
+ mesh: MmgMesh3D,
489
+ *,
490
+ check_geometry: bool = True,
491
+ check_topology: bool = True,
492
+ check_quality: bool = True,
493
+ min_quality: float = 0.1,
494
+ ) -> ValidationReport:
495
+ """Validate a 3D tetrahedral mesh.
496
+
497
+ Parameters
498
+ ----------
499
+ mesh : MmgMesh3D
500
+ The mesh to validate.
501
+ check_geometry : bool
502
+ Check for geometric issues (inverted/degenerate elements).
503
+ check_topology : bool
504
+ Check for topological issues (orphan vertices).
505
+ check_quality : bool
506
+ Check element quality against threshold.
507
+ min_quality : float
508
+ Minimum acceptable element quality (0-1).
509
+
510
+ Returns
511
+ -------
512
+ ValidationReport
513
+ Complete validation report.
514
+
515
+ """
516
+ issues: list[ValidationIssue] = []
517
+ quality_stats: QualityStats | None = None
518
+
519
+ vertices = mesh.get_vertices()
520
+ elements = mesh.get_tetrahedra()
521
+
522
+ if check_geometry:
523
+ _check_geometry_3d(mesh, issues)
524
+ _check_duplicate_vertices(vertices, issues)
525
+
526
+ if check_topology:
527
+ _check_orphan_vertices(vertices, elements, issues)
528
+
529
+ if check_quality and len(elements) > 0:
530
+ qualities = mesh.get_element_qualities()
531
+ quality_stats = _compute_quality_stats(qualities)
532
+ _check_quality(qualities, min_quality, issues)
533
+
534
+ has_errors = any(i.severity == IssueSeverity.ERROR for i in issues)
535
+
536
+ return ValidationReport(
537
+ is_valid=not has_errors,
538
+ issues=tuple(issues),
539
+ quality=quality_stats,
540
+ mesh_type="MmgMesh3D",
541
+ )
542
+
543
+
544
+ def validate_mesh_2d(
545
+ mesh: MmgMesh2D,
546
+ *,
547
+ check_geometry: bool = True,
548
+ check_topology: bool = True,
549
+ check_quality: bool = True,
550
+ min_quality: float = 0.1,
551
+ ) -> ValidationReport:
552
+ """Validate a 2D planar mesh.
553
+
554
+ Parameters
555
+ ----------
556
+ mesh : MmgMesh2D
557
+ The mesh to validate.
558
+ check_geometry : bool
559
+ Check for geometric issues (inverted/degenerate elements).
560
+ check_topology : bool
561
+ Check for topological issues (orphan vertices, non-manifold edges).
562
+ check_quality : bool
563
+ Check element quality against threshold.
564
+ min_quality : float
565
+ Minimum acceptable element quality (0-1).
566
+
567
+ Returns
568
+ -------
569
+ ValidationReport
570
+ Complete validation report.
571
+
572
+ """
573
+ issues: list[ValidationIssue] = []
574
+ quality_stats: QualityStats | None = None
575
+
576
+ vertices = mesh.get_vertices()
577
+ triangles = mesh.get_triangles()
578
+
579
+ if check_geometry:
580
+ _check_geometry_2d(mesh, issues)
581
+ _check_duplicate_vertices(vertices, issues)
582
+
583
+ if check_topology:
584
+ _check_orphan_vertices(vertices, triangles, issues)
585
+ _check_topology_surface(mesh, issues, "2d")
586
+
587
+ if check_quality and len(triangles) > 0:
588
+ qualities = mesh.get_element_qualities()
589
+ quality_stats = _compute_quality_stats(qualities)
590
+ _check_quality(qualities, min_quality, issues)
591
+
592
+ has_errors = any(i.severity == IssueSeverity.ERROR for i in issues)
593
+
594
+ return ValidationReport(
595
+ is_valid=not has_errors,
596
+ issues=tuple(issues),
597
+ quality=quality_stats,
598
+ mesh_type="MmgMesh2D",
599
+ )
600
+
601
+
602
+ def validate_mesh_surface(
603
+ mesh: MmgMeshS,
604
+ *,
605
+ check_geometry: bool = True,
606
+ check_topology: bool = True,
607
+ check_quality: bool = True,
608
+ min_quality: float = 0.1,
609
+ ) -> ValidationReport:
610
+ """Validate a surface mesh.
611
+
612
+ Parameters
613
+ ----------
614
+ mesh : MmgMeshS
615
+ The mesh to validate.
616
+ check_geometry : bool
617
+ Check for geometric issues (degenerate elements).
618
+ check_topology : bool
619
+ Check for topological issues (orphan vertices, non-manifold edges).
620
+ check_quality : bool
621
+ Check element quality against threshold.
622
+ min_quality : float
623
+ Minimum acceptable element quality (0-1).
624
+
625
+ Returns
626
+ -------
627
+ ValidationReport
628
+ Complete validation report.
629
+
630
+ """
631
+ issues: list[ValidationIssue] = []
632
+ quality_stats: QualityStats | None = None
633
+
634
+ vertices = mesh.get_vertices()
635
+ triangles = mesh.get_triangles()
636
+
637
+ if check_geometry:
638
+ _check_geometry_surface(mesh, issues)
639
+ _check_duplicate_vertices(vertices, issues)
640
+
641
+ if check_topology:
642
+ _check_orphan_vertices(vertices, triangles, issues)
643
+ _check_topology_surface(mesh, issues, "surface")
644
+
645
+ if check_quality and len(triangles) > 0:
646
+ qualities = mesh.get_element_qualities()
647
+ quality_stats = _compute_quality_stats(qualities)
648
+ _check_quality(qualities, min_quality, issues)
649
+
650
+ has_errors = any(i.severity == IssueSeverity.ERROR for i in issues)
651
+
652
+ return ValidationReport(
653
+ is_valid=not has_errors,
654
+ issues=tuple(issues),
655
+ quality=quality_stats,
656
+ mesh_type="MmgMeshS",
657
+ )
658
+
659
+
660
+ __all__ = [
661
+ "IssueSeverity",
662
+ "QualityStats",
663
+ "ValidationError",
664
+ "ValidationIssue",
665
+ "ValidationReport",
666
+ "validate_mesh_2d",
667
+ "validate_mesh_3d",
668
+ "validate_mesh_surface",
669
+ ]
mmgpy/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Version information."""
2
+
3
+ __version__ = "0.5.0"
mmgpy/_version.py.in ADDED
@@ -0,0 +1,3 @@
1
+ """Version information."""
2
+
3
+ __version__ = "@MMGPY_VERSION@"
mmgpy/bin/mmg2d_O3 ADDED
Binary file
mmgpy/bin/mmg3d_O3 ADDED
Binary file
mmgpy/bin/mmgs_O3 ADDED
Binary file
@@ -0,0 +1,24 @@
1
+ """Interactive sizing tools for mmgpy.
2
+
3
+ This module provides PyVista-based interactive tools for defining local
4
+ mesh sizing constraints through visual interaction.
5
+
6
+ Example:
7
+ -------
8
+ >>> from mmgpy import Mesh
9
+ >>> from mmgpy.interactive import SizingEditor
10
+ >>>
11
+ >>> mesh = Mesh("model.mesh")
12
+ >>> editor = SizingEditor(mesh)
13
+ >>> editor.add_sphere_tool()
14
+ >>> editor.run() # Opens PyVista window
15
+ >>>
16
+ >>> # After editing, apply constraints
17
+ >>> editor.apply_to_mesh()
18
+ >>> mesh.remesh()
19
+
20
+ """
21
+
22
+ from mmgpy.interactive.sizing_editor import SizingEditor
23
+
24
+ __all__ = ["SizingEditor"]