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/_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"]
|