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/_pyvista.py ADDED
@@ -0,0 +1,410 @@
1
+ """PyVista integration for mmgpy mesh classes.
2
+
3
+ This module provides conversion functions between PyVista mesh types
4
+ and mmgpy mesh classes.
5
+
6
+ Example:
7
+ >>> import pyvista as pv
8
+ >>> from mmgpy import Mesh
9
+ >>>
10
+ >>> # Load mesh and convert to mmgpy
11
+ >>> grid = pv.read("mesh.vtk")
12
+ >>> mesh = Mesh(grid)
13
+ >>>
14
+ >>> # Remesh and convert back
15
+ >>> mesh.remesh(hmax=0.1)
16
+ >>> result = mesh.to_pyvista()
17
+
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import TYPE_CHECKING, overload
24
+
25
+ import numpy as np
26
+ import pyvista as pv
27
+
28
+ from mmgpy._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
29
+
30
+ logger = logging.getLogger("mmgpy")
31
+
32
+ if TYPE_CHECKING:
33
+ from numpy.typing import NDArray
34
+
35
+ _DIMS_2D = 2
36
+ _DIMS_3D = 3
37
+ _TRIANGLE_VERTS = 3
38
+ _2D_DETECTION_TOLERANCE = 1e-8
39
+
40
+ _TRIANGULATION_WARNING = (
41
+ "Input mesh contains non-triangular elements (quads, polygons). "
42
+ "Converting to triangles. Note: output will always be triangular "
43
+ "as MMG only supports triangular elements."
44
+ )
45
+
46
+
47
+ def _triangulate_if_needed(mesh: pv.PolyData) -> tuple[pv.PolyData, bool]:
48
+ """Triangulate mesh if it contains non-triangular faces.
49
+
50
+ Parameters
51
+ ----------
52
+ mesh : pv.PolyData
53
+ Input mesh that may contain quads or other polygons.
54
+
55
+ Returns
56
+ -------
57
+ tuple[pv.PolyData, bool]
58
+ Tuple of (triangulated_mesh, was_triangulated).
59
+ If mesh was already all triangles, returns (mesh, False).
60
+
61
+ """
62
+ if mesh.n_cells == 0:
63
+ return mesh, False
64
+
65
+ if mesh.is_all_triangles:
66
+ return mesh, False
67
+
68
+ triangulated = mesh.triangulate()
69
+ return triangulated, True
70
+
71
+
72
+ def _is_2d_mesh(points: NDArray[np.floating]) -> bool:
73
+ """Check if points are essentially 2D (z coordinates are zero or near-zero)."""
74
+ if points.shape[1] == _DIMS_2D:
75
+ return True
76
+ if points.shape[1] == _DIMS_3D:
77
+ z_coords = points[:, 2]
78
+ return bool(np.allclose(z_coords, 0, atol=_2D_DETECTION_TOLERANCE))
79
+ return False
80
+
81
+
82
+ def _extract_triangles_from_polydata(mesh: pv.PolyData) -> NDArray[np.int32]:
83
+ """Extract triangle connectivity from PolyData faces array."""
84
+ if hasattr(mesh, "cells_dict") and pv.CellType.TRIANGLE in mesh.cells_dict:
85
+ return mesh.cells_dict[pv.CellType.TRIANGLE].astype(np.int32)
86
+
87
+ faces = mesh.faces
88
+ if len(faces) == 0:
89
+ msg = "PolyData has no faces"
90
+ raise ValueError(msg)
91
+
92
+ triangles = []
93
+ i = 0
94
+ while i < len(faces):
95
+ n_verts = faces[i]
96
+ if n_verts != _TRIANGLE_VERTS:
97
+ msg = f"Expected triangles (3 vertices), got {n_verts}-vertex polygon"
98
+ raise ValueError(msg)
99
+ triangles.append(faces[i + 1 : i + 4])
100
+ i += n_verts + 1
101
+
102
+ return np.array(triangles, dtype=np.int32)
103
+
104
+
105
+ def _from_pyvista_to_mmg3d(mesh: pv.UnstructuredGrid) -> MmgMesh3D:
106
+ """Convert UnstructuredGrid with tetrahedra to MmgMesh3D."""
107
+ if pv.CellType.TETRA not in mesh.cells_dict:
108
+ msg = "UnstructuredGrid must contain tetrahedra (CellType.TETRA)"
109
+ raise ValueError(msg)
110
+
111
+ vertices = np.array(mesh.points, dtype=np.float64)
112
+ elements = mesh.cells_dict[pv.CellType.TETRA].astype(np.int32)
113
+
114
+ mmg_mesh = MmgMesh3D(vertices, elements)
115
+
116
+ # Preserve element refs from cell_data if present
117
+ if "refs" in mesh.cell_data:
118
+ refs = np.asarray(mesh.cell_data["refs"], dtype=np.int32)
119
+ if len(refs) == len(elements):
120
+ mmg_mesh.set_tetrahedra(elements, refs)
121
+
122
+ return mmg_mesh
123
+
124
+
125
+ def _from_pyvista_to_mmg2d(mesh: pv.PolyData) -> MmgMesh2D:
126
+ """Convert PolyData with 2D triangles to MmgMesh2D."""
127
+ mesh, was_triangulated = _triangulate_if_needed(mesh)
128
+ if was_triangulated:
129
+ logger.warning(_TRIANGULATION_WARNING)
130
+
131
+ points = np.array(mesh.points, dtype=np.float64)
132
+ if points.shape[1] == _DIMS_3D:
133
+ vertices = np.ascontiguousarray(points[:, :2])
134
+ else:
135
+ vertices = points
136
+ triangles = _extract_triangles_from_polydata(mesh)
137
+
138
+ mmg_mesh = MmgMesh2D(vertices, triangles)
139
+
140
+ # Preserve triangle refs from cell_data if present
141
+ if "refs" in mesh.cell_data:
142
+ refs = np.asarray(mesh.cell_data["refs"], dtype=np.int32)
143
+ if len(refs) == len(triangles):
144
+ mmg_mesh.set_triangles(triangles, refs)
145
+
146
+ return mmg_mesh
147
+
148
+
149
+ def _from_pyvista_to_mmgs(mesh: pv.PolyData) -> MmgMeshS:
150
+ """Convert PolyData with 3D surface triangles to MmgMeshS."""
151
+ mesh, was_triangulated = _triangulate_if_needed(mesh)
152
+ if was_triangulated:
153
+ logger.warning(_TRIANGULATION_WARNING)
154
+
155
+ vertices = np.array(mesh.points, dtype=np.float64)
156
+ triangles = _extract_triangles_from_polydata(mesh)
157
+
158
+ mmg_mesh = MmgMeshS(vertices, triangles)
159
+
160
+ # Preserve triangle refs from cell_data if present
161
+ if "refs" in mesh.cell_data:
162
+ refs = np.asarray(mesh.cell_data["refs"], dtype=np.int32)
163
+ if len(refs) == len(triangles):
164
+ mmg_mesh.set_triangles(triangles, refs)
165
+
166
+ return mmg_mesh
167
+
168
+
169
+ def _from_pyvista_with_explicit_type(
170
+ mesh: pv.UnstructuredGrid | pv.PolyData,
171
+ mesh_type: type[MmgMesh3D | MmgMesh2D | MmgMeshS],
172
+ ) -> MmgMesh3D | MmgMesh2D | MmgMeshS:
173
+ """Convert PyVista mesh to mmgpy mesh with explicit type."""
174
+ if mesh_type is MmgMesh3D:
175
+ if not isinstance(mesh, pv.UnstructuredGrid):
176
+ msg = "MmgMesh3D requires UnstructuredGrid input"
177
+ raise ValueError(msg)
178
+ return _from_pyvista_to_mmg3d(mesh)
179
+
180
+ if mesh_type is MmgMesh2D:
181
+ if not isinstance(mesh, pv.PolyData):
182
+ msg = "MmgMesh2D requires PolyData input"
183
+ raise ValueError(msg)
184
+ return _from_pyvista_to_mmg2d(mesh)
185
+
186
+ if mesh_type is MmgMeshS:
187
+ if not isinstance(mesh, pv.PolyData):
188
+ msg = "MmgMeshS requires PolyData input"
189
+ raise ValueError(msg)
190
+ return _from_pyvista_to_mmgs(mesh)
191
+
192
+ msg = f"Unknown mesh type: {mesh_type}"
193
+ raise ValueError(msg)
194
+
195
+
196
+ def _from_pyvista_auto_detect(
197
+ mesh: pv.UnstructuredGrid | pv.PolyData,
198
+ ) -> MmgMesh3D | MmgMesh2D | MmgMeshS:
199
+ """Convert PyVista mesh to mmgpy mesh with auto-detection."""
200
+ if isinstance(mesh, pv.UnstructuredGrid):
201
+ if pv.CellType.TETRA in mesh.cells_dict:
202
+ return _from_pyvista_to_mmg3d(mesh)
203
+ msg = "UnstructuredGrid must contain tetrahedra for auto-detection"
204
+ raise ValueError(msg)
205
+
206
+ if isinstance(mesh, pv.PolyData):
207
+ if _is_2d_mesh(mesh.points):
208
+ return _from_pyvista_to_mmg2d(mesh)
209
+ return _from_pyvista_to_mmgs(mesh)
210
+
211
+ msg = f"Unsupported PyVista mesh type: {type(mesh)}"
212
+ raise TypeError(msg)
213
+
214
+
215
+ @overload
216
+ def from_pyvista(
217
+ mesh: pv.UnstructuredGrid | pv.PolyData,
218
+ mesh_type: type[MmgMesh3D],
219
+ ) -> MmgMesh3D: ...
220
+
221
+
222
+ @overload
223
+ def from_pyvista(
224
+ mesh: pv.UnstructuredGrid | pv.PolyData,
225
+ mesh_type: type[MmgMesh2D],
226
+ ) -> MmgMesh2D: ...
227
+
228
+
229
+ @overload
230
+ def from_pyvista(
231
+ mesh: pv.UnstructuredGrid | pv.PolyData,
232
+ mesh_type: type[MmgMeshS],
233
+ ) -> MmgMeshS: ...
234
+
235
+
236
+ @overload
237
+ def from_pyvista(
238
+ mesh: pv.UnstructuredGrid | pv.PolyData,
239
+ mesh_type: None = None,
240
+ ) -> MmgMesh3D | MmgMesh2D | MmgMeshS: ...
241
+
242
+
243
+ def from_pyvista(
244
+ mesh: pv.UnstructuredGrid | pv.PolyData,
245
+ mesh_type: type[MmgMesh3D | MmgMesh2D | MmgMeshS] | None = None,
246
+ ) -> MmgMesh3D | MmgMesh2D | MmgMeshS:
247
+ """Convert a PyVista mesh to an mmgpy mesh.
248
+
249
+ Args:
250
+ mesh: PyVista mesh (UnstructuredGrid or PolyData).
251
+ mesh_type: Target mesh class. If None, auto-detects based on:
252
+ - UnstructuredGrid with tetrahedra → MmgMesh3D
253
+ - PolyData with 2D points (z~=0) -> MmgMesh2D
254
+ - PolyData with 3D points → MmgMeshS
255
+
256
+ Returns:
257
+ The appropriate mmgpy mesh instance.
258
+
259
+ Raises:
260
+ ValueError: If mesh type cannot be determined or is incompatible.
261
+
262
+ Note:
263
+ When auto-detecting mesh type for PolyData, a mesh is considered 2D
264
+ (and converted to MmgMesh2D) if all z-coordinates are within 1e-8 of zero.
265
+ For thin 3D meshes near z=0, explicitly specify ``mesh_type=MmgMeshS``.
266
+
267
+ Example:
268
+ >>> import pyvista as pv
269
+ >>> from mmgpy import from_pyvista, MmgMeshS
270
+ >>>
271
+ >>> # Auto-detect mesh type
272
+ >>> grid = pv.read("tetra_mesh.vtk")
273
+ >>> mesh3d = from_pyvista(grid)
274
+ >>>
275
+ >>> # Explicit mesh type for thin 3D surfaces
276
+ >>> surface = pv.read("surface.stl")
277
+ >>> mesh_s = from_pyvista(surface, MmgMeshS)
278
+
279
+ """
280
+ if mesh_type is not None:
281
+ return _from_pyvista_with_explicit_type(mesh, mesh_type)
282
+ return _from_pyvista_auto_detect(mesh)
283
+
284
+
285
+ @overload
286
+ def to_pyvista(
287
+ mesh: MmgMesh3D,
288
+ *,
289
+ include_refs: bool = True,
290
+ ) -> pv.UnstructuredGrid: ...
291
+
292
+
293
+ @overload
294
+ def to_pyvista(
295
+ mesh: MmgMesh2D,
296
+ *,
297
+ include_refs: bool = True,
298
+ ) -> pv.PolyData: ...
299
+
300
+
301
+ @overload
302
+ def to_pyvista(
303
+ mesh: MmgMeshS,
304
+ *,
305
+ include_refs: bool = True,
306
+ ) -> pv.PolyData: ...
307
+
308
+
309
+ def to_pyvista(
310
+ mesh: MmgMesh3D | MmgMesh2D | MmgMeshS,
311
+ *,
312
+ include_refs: bool = True,
313
+ ) -> pv.UnstructuredGrid | pv.PolyData:
314
+ """Convert an mmgpy mesh to a PyVista mesh.
315
+
316
+ Args:
317
+ mesh: mmgpy mesh instance (MmgMesh3D, MmgMesh2D, or MmgMeshS).
318
+ include_refs: If True, include element references as cell_data.
319
+
320
+ Returns:
321
+ PyVista mesh:
322
+ - MmgMesh3D → UnstructuredGrid with tetrahedra
323
+ - MmgMesh2D → PolyData with triangular faces (z=0)
324
+ - MmgMeshS → PolyData with triangular faces
325
+
326
+ Raises:
327
+ TypeError: If mesh is not an mmgpy mesh type.
328
+
329
+ Example:
330
+ >>> from mmgpy import MmgMesh3D, to_pyvista
331
+ >>>
332
+ >>> mesh = MmgMesh3D(vertices, elements)
333
+ >>> mesh.remesh(hmax=0.1)
334
+ >>> grid = to_pyvista(mesh)
335
+ >>> grid.plot()
336
+
337
+ """
338
+ if isinstance(mesh, MmgMesh3D):
339
+ return _mmg3d_to_pyvista(mesh, include_refs=include_refs)
340
+ if isinstance(mesh, MmgMesh2D):
341
+ return _mmg2d_to_pyvista(mesh, include_refs=include_refs)
342
+ if isinstance(mesh, MmgMeshS):
343
+ return _mmgs_to_pyvista(mesh, include_refs=include_refs)
344
+
345
+ msg = f"Unsupported mesh type: {type(mesh)}"
346
+ raise TypeError(msg)
347
+
348
+
349
+ def _mmg3d_to_pyvista(mesh: MmgMesh3D, *, include_refs: bool) -> pv.UnstructuredGrid:
350
+ """Convert MmgMesh3D to PyVista UnstructuredGrid."""
351
+ vertices = mesh.get_vertices()
352
+
353
+ if include_refs:
354
+ elements, refs = mesh.get_elements_with_refs()
355
+ else:
356
+ elements = mesh.get_elements()
357
+ refs = None
358
+
359
+ grid = pv.UnstructuredGrid({pv.CellType.TETRA: elements}, vertices)
360
+
361
+ if refs is not None:
362
+ grid.cell_data["refs"] = refs
363
+
364
+ return grid
365
+
366
+
367
+ def _mmg2d_to_pyvista(mesh: MmgMesh2D, *, include_refs: bool) -> pv.PolyData:
368
+ """Convert MmgMesh2D to PyVista PolyData."""
369
+ vertices_2d = mesh.get_vertices()
370
+ vertices_3d = np.column_stack([vertices_2d, np.zeros(len(vertices_2d))])
371
+
372
+ if include_refs:
373
+ triangles, refs = mesh.get_triangles_with_refs()
374
+ else:
375
+ triangles = mesh.get_triangles()
376
+ refs = None
377
+
378
+ faces = np.hstack(
379
+ [np.full((len(triangles), 1), _TRIANGLE_VERTS), triangles],
380
+ ).ravel()
381
+ polydata = pv.PolyData(vertices_3d, faces=faces)
382
+
383
+ if refs is not None:
384
+ polydata.cell_data["refs"] = refs
385
+
386
+ return polydata
387
+
388
+
389
+ def _mmgs_to_pyvista(mesh: MmgMeshS, *, include_refs: bool) -> pv.PolyData:
390
+ """Convert MmgMeshS to PyVista PolyData."""
391
+ vertices = mesh.get_vertices()
392
+
393
+ if include_refs:
394
+ triangles, refs = mesh.get_triangles_with_refs()
395
+ else:
396
+ triangles = mesh.get_triangles()
397
+ refs = None
398
+
399
+ faces = np.hstack(
400
+ [np.full((len(triangles), 1), _TRIANGLE_VERTS), triangles],
401
+ ).ravel()
402
+ polydata = pv.PolyData(vertices, faces=faces)
403
+
404
+ if refs is not None:
405
+ polydata.cell_data["refs"] = refs
406
+
407
+ return polydata
408
+
409
+
410
+ __all__ = ["from_pyvista", "to_pyvista"]
mmgpy/_result.py ADDED
@@ -0,0 +1,143 @@
1
+ """RemeshResult dataclass for capturing remeshing statistics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class RemeshResult:
10
+ """Statistics from a remeshing operation.
11
+
12
+ This class captures mesh topology changes, quality metrics, timing,
13
+ and any warnings from the remeshing operation.
14
+
15
+ Attributes
16
+ ----------
17
+ vertices_before : int
18
+ Number of vertices before remeshing.
19
+ vertices_after : int
20
+ Number of vertices after remeshing.
21
+ elements_before : int
22
+ Number of primary elements (tetrahedra for 3D, triangles for 2D/surface).
23
+ elements_after : int
24
+ Number of primary elements after remeshing.
25
+ triangles_before : int
26
+ Number of triangles (boundary for 3D, all for 2D/surface).
27
+ triangles_after : int
28
+ Number of triangles after remeshing.
29
+ edges_before : int
30
+ Number of edges before remeshing.
31
+ edges_after : int
32
+ Number of edges after remeshing.
33
+ quality_min_before : float
34
+ Minimum element quality before remeshing (0-1 scale).
35
+ quality_min_after : float
36
+ Minimum element quality after remeshing.
37
+ quality_mean_before : float
38
+ Mean element quality before remeshing.
39
+ quality_mean_after : float
40
+ Mean element quality after remeshing.
41
+ duration_seconds : float
42
+ Wall-clock time for the remeshing operation in seconds.
43
+ Measures only the MMG library call, excluding stats collection overhead.
44
+ warnings : tuple[str, ...]
45
+ Any warnings from MMG (non-fatal issues). Contains warning messages
46
+ captured from MMG's stderr output during remeshing. Common warnings
47
+ include edge size clamping, geometric constraint violations, etc.
48
+ return_code : int
49
+ MMG return code (0 = success). Note: If remeshing fails, an exception
50
+ is raised before RemeshResult is created, so this will always be 0
51
+ for successfully returned results. Included for completeness and
52
+ potential future use with partial failure modes.
53
+
54
+ Examples
55
+ --------
56
+ >>> mesh = MmgMesh3D(vertices, tetrahedra)
57
+ >>> result = mesh.remesh(hmax=0.1)
58
+ >>> print(result)
59
+ RemeshResult(
60
+ vertices: 100 -> 250 (+150)
61
+ elements: 400 -> 1200 (+800)
62
+ quality: 0.450 -> 0.780 (173.3%)
63
+ duration: 0.15s
64
+ )
65
+ >>> result.success
66
+ True
67
+ >>> result.quality_improvement
68
+ 1.733...
69
+
70
+ """
71
+
72
+ vertices_before: int
73
+ vertices_after: int
74
+ elements_before: int
75
+ elements_after: int
76
+ triangles_before: int
77
+ triangles_after: int
78
+ edges_before: int
79
+ edges_after: int
80
+ quality_min_before: float
81
+ quality_min_after: float
82
+ quality_mean_before: float
83
+ quality_mean_after: float
84
+ duration_seconds: float
85
+ warnings: tuple[str, ...]
86
+ return_code: int
87
+
88
+ @property
89
+ def vertex_change(self) -> int:
90
+ """Net change in vertex count."""
91
+ return self.vertices_after - self.vertices_before
92
+
93
+ @property
94
+ def element_change(self) -> int:
95
+ """Net change in element count."""
96
+ return self.elements_after - self.elements_before
97
+
98
+ @property
99
+ def triangle_change(self) -> int:
100
+ """Net change in triangle count."""
101
+ return self.triangles_after - self.triangles_before
102
+
103
+ @property
104
+ def edge_change(self) -> int:
105
+ """Net change in edge count."""
106
+ return self.edges_after - self.edges_before
107
+
108
+ @property
109
+ def quality_improvement(self) -> float:
110
+ """Quality improvement ratio (mean_after / mean_before).
111
+
112
+ Returns 1.0 if both values are zero (no change), inf if only
113
+ before is zero, and the actual ratio otherwise.
114
+ """
115
+ if self.quality_mean_before == 0:
116
+ if self.quality_mean_after == 0:
117
+ return 1.0 # No change (both zero)
118
+ return float("inf") # Improved from zero
119
+ return self.quality_mean_after / self.quality_mean_before
120
+
121
+ @property
122
+ def success(self) -> bool:
123
+ """Whether remeshing completed successfully."""
124
+ return self.return_code == 0
125
+
126
+ def __str__(self) -> str:
127
+ """Return a readable string representation."""
128
+ quality_pct = self.quality_improvement * 100
129
+ q_before = self.quality_mean_before
130
+ q_after = self.quality_mean_after
131
+ return (
132
+ f"RemeshResult(\n"
133
+ f" vertices: {self.vertices_before} -> {self.vertices_after} "
134
+ f"({self.vertex_change:+d})\n"
135
+ f" elements: {self.elements_before} -> {self.elements_after} "
136
+ f"({self.element_change:+d})\n"
137
+ f" quality: {q_before:.3f} -> {q_after:.3f} ({quality_pct:.1f}%)\n"
138
+ f" duration: {self.duration_seconds:.2f}s\n"
139
+ f")"
140
+ )
141
+
142
+
143
+ __all__ = ["RemeshResult"]