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/_io.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""Unified mesh I/O for mmgpy.
|
|
2
|
+
|
|
3
|
+
This module provides a unified `read()` function that can load meshes from
|
|
4
|
+
any file format supported by meshio, or directly from PyVista objects.
|
|
5
|
+
|
|
6
|
+
For Medit format (.mesh) files, native MMG loading is used to preserve
|
|
7
|
+
MMG-specific keywords like Ridges, RequiredVertices, Tangents, and reference
|
|
8
|
+
markers that meshio does not understand.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> import mmgpy
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Read from various file formats
|
|
14
|
+
>>> mesh = mmgpy.read("mesh.vtk")
|
|
15
|
+
>>> mesh = mmgpy.read("mesh.msh") # Gmsh
|
|
16
|
+
>>> mesh = mmgpy.read("mesh.stl") # STL surface
|
|
17
|
+
>>> mesh = mmgpy.read("mesh.mesh") # Medit (native MMG loading)
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Read from PyVista object
|
|
20
|
+
>>> import pyvista as pv
|
|
21
|
+
>>> pv_mesh = pv.read("mesh.vtk")
|
|
22
|
+
>>> mesh = mmgpy.read(pv_mesh)
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import re
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
import meshio
|
|
34
|
+
import numpy as np
|
|
35
|
+
import pyvista as pv
|
|
36
|
+
|
|
37
|
+
from mmgpy._mesh import _DIMS_2D, _DIMS_3D, MeshKind, _is_2d_points
|
|
38
|
+
from mmgpy._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger("mmgpy")
|
|
41
|
+
|
|
42
|
+
_MEDIT_KEYWORD_ONLY_PATTERN = re.compile(r"^\s*(\w+)\s*$", re.IGNORECASE)
|
|
43
|
+
_MEDIT_DIMENSION_INLINE_PATTERN = re.compile(
|
|
44
|
+
r"^\s*dimension\s+(\d+)\s*$",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
_MEDIT_DIMENSION_VALUE_PATTERN = re.compile(r"^\s*(\d+)\s*$")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_medit_header(path: Path) -> tuple[int | None, bool, bool]:
|
|
51
|
+
"""Parse Medit file header to extract dimension and element types.
|
|
52
|
+
|
|
53
|
+
Handles both inline format ("Dimension 3") and multi-line format.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
tuple[int | None, bool, bool]
|
|
58
|
+
(dimension, has_tetrahedra, has_triangles)
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
dimension = None
|
|
62
|
+
has_tetrahedra = False
|
|
63
|
+
has_triangles = False
|
|
64
|
+
|
|
65
|
+
with path.open(encoding="utf-8", errors="replace") as f:
|
|
66
|
+
for raw_line in f:
|
|
67
|
+
stripped = raw_line.strip()
|
|
68
|
+
if not stripped or stripped.startswith("#"):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# Check for inline "Dimension N" format first
|
|
72
|
+
dim_inline = _MEDIT_DIMENSION_INLINE_PATTERN.match(stripped)
|
|
73
|
+
if dim_inline:
|
|
74
|
+
dimension = int(dim_inline.group(1))
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# Check for keyword-only format
|
|
78
|
+
keyword_match = _MEDIT_KEYWORD_ONLY_PATTERN.match(stripped)
|
|
79
|
+
if not keyword_match:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
keyword = keyword_match.group(1).lower()
|
|
83
|
+
if keyword == "dimension":
|
|
84
|
+
next_line = next(f, "").strip()
|
|
85
|
+
val_match = _MEDIT_DIMENSION_VALUE_PATTERN.match(next_line)
|
|
86
|
+
if val_match:
|
|
87
|
+
dimension = int(val_match.group(1))
|
|
88
|
+
elif keyword == "tetrahedra":
|
|
89
|
+
has_tetrahedra = True
|
|
90
|
+
elif keyword == "triangles":
|
|
91
|
+
has_triangles = True
|
|
92
|
+
|
|
93
|
+
# Stop early only if we found tetrahedra (volumetric mesh)
|
|
94
|
+
# Continue scanning if only triangles found (might have tetrahedra later)
|
|
95
|
+
if dimension is not None and has_tetrahedra:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
return dimension, has_tetrahedra, has_triangles
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _detect_medit_mesh_kind(path: Path) -> MeshKind:
|
|
102
|
+
"""Detect mesh kind from Medit (.mesh) file header.
|
|
103
|
+
|
|
104
|
+
Parses the file to find the Dimension keyword and check for element types.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
path : Path
|
|
109
|
+
Path to the .mesh file.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
MeshKind
|
|
114
|
+
Detected mesh kind.
|
|
115
|
+
|
|
116
|
+
Raises
|
|
117
|
+
------
|
|
118
|
+
ValueError
|
|
119
|
+
If mesh kind cannot be determined.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
dimension, has_tetrahedra, has_triangles = _parse_medit_header(path)
|
|
123
|
+
|
|
124
|
+
if dimension == _DIMS_3D and has_tetrahedra:
|
|
125
|
+
return MeshKind.TETRAHEDRAL
|
|
126
|
+
if dimension == _DIMS_2D and has_triangles:
|
|
127
|
+
return MeshKind.TRIANGULAR_2D
|
|
128
|
+
if dimension == _DIMS_3D and has_triangles:
|
|
129
|
+
return MeshKind.TRIANGULAR_SURFACE
|
|
130
|
+
|
|
131
|
+
msg = f"Cannot determine mesh kind from file: {path}"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_medit_native(
|
|
136
|
+
path: Path,
|
|
137
|
+
mesh_kind: MeshKind | None,
|
|
138
|
+
) -> MmgMesh3D | MmgMesh2D | MmgMeshS:
|
|
139
|
+
"""Load a Medit (.mesh) file using native MMG loading.
|
|
140
|
+
|
|
141
|
+
Uses the native MMG*_loadMesh functions which preserve MMG-specific
|
|
142
|
+
keywords like Ridges, RequiredVertices, Tangents, and reference markers.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
path : Path
|
|
147
|
+
Path to the .mesh file.
|
|
148
|
+
mesh_kind : MeshKind | None
|
|
149
|
+
Force a specific mesh kind, or None for auto-detection.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
154
|
+
The loaded mesh implementation.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
ValueError
|
|
159
|
+
If mesh kind cannot be determined.
|
|
160
|
+
RuntimeError
|
|
161
|
+
If loading fails.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
if mesh_kind is None:
|
|
165
|
+
mesh_kind = _detect_medit_mesh_kind(path)
|
|
166
|
+
|
|
167
|
+
path_str = str(path)
|
|
168
|
+
|
|
169
|
+
if mesh_kind == MeshKind.TETRAHEDRAL:
|
|
170
|
+
return MmgMesh3D(path_str)
|
|
171
|
+
if mesh_kind == MeshKind.TRIANGULAR_2D:
|
|
172
|
+
return MmgMesh2D(path_str)
|
|
173
|
+
if mesh_kind == MeshKind.TRIANGULAR_SURFACE:
|
|
174
|
+
return MmgMeshS(path_str)
|
|
175
|
+
|
|
176
|
+
msg = f"Unknown mesh_kind: {mesh_kind}"
|
|
177
|
+
raise ValueError(msg)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if TYPE_CHECKING:
|
|
181
|
+
from mmgpy._mesh import Mesh
|
|
182
|
+
|
|
183
|
+
# Element types that indicate volumetric 3D meshes (only tetrahedra supported by MMG)
|
|
184
|
+
_VOLUME_CELL_TYPES = frozenset(
|
|
185
|
+
{
|
|
186
|
+
"tetra",
|
|
187
|
+
"tetra10",
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Element types that would be volumetric but are NOT supported by MMG
|
|
192
|
+
_UNSUPPORTED_VOLUME_TYPES = frozenset(
|
|
193
|
+
{
|
|
194
|
+
"hexahedron",
|
|
195
|
+
"hexahedron20",
|
|
196
|
+
"hexahedron27",
|
|
197
|
+
"wedge",
|
|
198
|
+
"wedge15",
|
|
199
|
+
"pyramid",
|
|
200
|
+
"pyramid13",
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Element types that indicate surface meshes
|
|
205
|
+
_SURFACE_CELL_TYPES = frozenset(
|
|
206
|
+
{
|
|
207
|
+
"triangle",
|
|
208
|
+
"triangle6",
|
|
209
|
+
"quad",
|
|
210
|
+
"quad8",
|
|
211
|
+
"quad9",
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Non-triangular surface cell types that need triangulation
|
|
216
|
+
_NON_TRIANGLE_SURFACE_TYPES = frozenset(
|
|
217
|
+
{
|
|
218
|
+
"quad",
|
|
219
|
+
"quad8",
|
|
220
|
+
"quad9",
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
_TRIANGULATION_WARNING = (
|
|
225
|
+
"Input mesh contains non-triangular elements (quads, polygons). "
|
|
226
|
+
"Converting to triangles. Note: output will always be triangular "
|
|
227
|
+
"as MMG only supports triangular elements."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _has_non_triangle_cells(mesh: meshio.Mesh) -> bool:
|
|
232
|
+
"""Check if meshio mesh has non-triangular surface cells."""
|
|
233
|
+
cell_types = {block.type for block in mesh.cells}
|
|
234
|
+
return bool(cell_types & _NON_TRIANGLE_SURFACE_TYPES)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _meshio_to_pyvista_polydata(mesh: meshio.Mesh) -> pv.PolyData:
|
|
238
|
+
"""Convert meshio mesh to PyVista PolyData for triangulation."""
|
|
239
|
+
# Ensure points are in native byte order (meshio may use big-endian)
|
|
240
|
+
points = np.ascontiguousarray(mesh.points, dtype=np.float64)
|
|
241
|
+
cells = []
|
|
242
|
+
for block in mesh.cells:
|
|
243
|
+
if block.type in _SURFACE_CELL_TYPES:
|
|
244
|
+
n_verts = block.data.shape[1]
|
|
245
|
+
for cell in block.data:
|
|
246
|
+
cells.extend([n_verts, *cell])
|
|
247
|
+
if not cells:
|
|
248
|
+
msg = "No surface cells found in mesh"
|
|
249
|
+
raise ValueError(msg)
|
|
250
|
+
return pv.PolyData(points, faces=cells)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _detect_mesh_kind(mesh: meshio.Mesh) -> MeshKind:
|
|
254
|
+
"""Detect mesh kind from meshio mesh based on cell types and point dimensions.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
MeshKind.TETRAHEDRAL for volumetric meshes with tetrahedra
|
|
258
|
+
MeshKind.TRIANGULAR_SURFACE for 3D surface meshes (triangles in 3D space)
|
|
259
|
+
MeshKind.TRIANGULAR_2D for planar 2D meshes (triangles with z~=0)
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
ValueError: If mesh contains unsupported element types (hexahedra, etc.)
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
cell_types = {block.type for block in mesh.cells}
|
|
266
|
+
|
|
267
|
+
# Check for unsupported volumetric elements
|
|
268
|
+
unsupported = cell_types & _UNSUPPORTED_VOLUME_TYPES
|
|
269
|
+
if unsupported:
|
|
270
|
+
msg = (
|
|
271
|
+
f"Unsupported element types: {unsupported}. "
|
|
272
|
+
"MMG only supports tetrahedral (3D), triangular (2D/surface) meshes."
|
|
273
|
+
)
|
|
274
|
+
raise ValueError(msg)
|
|
275
|
+
|
|
276
|
+
# Check for supported volumetric elements (tetrahedra)
|
|
277
|
+
if cell_types & _VOLUME_CELL_TYPES:
|
|
278
|
+
return MeshKind.TETRAHEDRAL
|
|
279
|
+
|
|
280
|
+
# Check for surface elements
|
|
281
|
+
if cell_types & _SURFACE_CELL_TYPES:
|
|
282
|
+
if _is_2d_points(mesh.points):
|
|
283
|
+
return MeshKind.TRIANGULAR_2D
|
|
284
|
+
return MeshKind.TRIANGULAR_SURFACE
|
|
285
|
+
|
|
286
|
+
msg = f"Cannot determine mesh kind from cell types: {cell_types}"
|
|
287
|
+
raise ValueError(msg)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _meshio_to_mmg3d(mesh: meshio.Mesh) -> MmgMesh3D:
|
|
291
|
+
"""Convert meshio mesh to MmgMesh3D."""
|
|
292
|
+
vertices = np.ascontiguousarray(mesh.points, dtype=np.float64)
|
|
293
|
+
|
|
294
|
+
# Collect all tetrahedra blocks
|
|
295
|
+
tetra_blocks = [block.data for block in mesh.cells if block.type == "tetra"]
|
|
296
|
+
|
|
297
|
+
if not tetra_blocks:
|
|
298
|
+
msg = "No tetrahedra found in mesh"
|
|
299
|
+
raise ValueError(msg)
|
|
300
|
+
|
|
301
|
+
# Concatenate all tetrahedra blocks
|
|
302
|
+
tetrahedra = np.ascontiguousarray(
|
|
303
|
+
np.vstack(tetra_blocks) if len(tetra_blocks) > 1 else tetra_blocks[0],
|
|
304
|
+
dtype=np.int32,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return MmgMesh3D(vertices, tetrahedra)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _extract_triangles_from_polydata(pv_mesh: pv.PolyData) -> np.ndarray:
|
|
311
|
+
"""Extract triangle connectivity from triangulated PolyData."""
|
|
312
|
+
faces = pv_mesh.faces
|
|
313
|
+
n_triangles = pv_mesh.n_cells
|
|
314
|
+
triangles = np.empty((n_triangles, 3), dtype=np.int32)
|
|
315
|
+
idx = 0
|
|
316
|
+
for i in range(n_triangles):
|
|
317
|
+
n_verts = faces[idx]
|
|
318
|
+
triangles[i] = faces[idx + 1 : idx + 4]
|
|
319
|
+
idx += n_verts + 1
|
|
320
|
+
return triangles
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _meshio_to_mmg2d(mesh: meshio.Mesh) -> MmgMesh2D:
|
|
324
|
+
"""Convert meshio mesh to MmgMesh2D."""
|
|
325
|
+
if _has_non_triangle_cells(mesh):
|
|
326
|
+
logger.warning(_TRIANGULATION_WARNING)
|
|
327
|
+
pv_mesh = _meshio_to_pyvista_polydata(mesh)
|
|
328
|
+
pv_mesh = pv_mesh.triangulate()
|
|
329
|
+
points = np.array(pv_mesh.points, dtype=np.float64)
|
|
330
|
+
if points.shape[1] == _DIMS_3D:
|
|
331
|
+
vertices = np.ascontiguousarray(points[:, :2])
|
|
332
|
+
else:
|
|
333
|
+
vertices = np.ascontiguousarray(points)
|
|
334
|
+
triangles = _extract_triangles_from_polydata(pv_mesh)
|
|
335
|
+
return MmgMesh2D(vertices, triangles)
|
|
336
|
+
|
|
337
|
+
points = mesh.points
|
|
338
|
+
|
|
339
|
+
# Extract 2D vertices (drop z if present)
|
|
340
|
+
if points.shape[1] == _DIMS_3D:
|
|
341
|
+
vertices = np.ascontiguousarray(points[:, :2], dtype=np.float64)
|
|
342
|
+
else:
|
|
343
|
+
vertices = np.ascontiguousarray(points, dtype=np.float64)
|
|
344
|
+
|
|
345
|
+
# Collect all triangle blocks
|
|
346
|
+
tri_blocks = [block.data for block in mesh.cells if block.type == "triangle"]
|
|
347
|
+
|
|
348
|
+
if not tri_blocks:
|
|
349
|
+
msg = "No triangles found in mesh"
|
|
350
|
+
raise ValueError(msg)
|
|
351
|
+
|
|
352
|
+
# Concatenate all triangle blocks
|
|
353
|
+
triangles = np.ascontiguousarray(
|
|
354
|
+
np.vstack(tri_blocks) if len(tri_blocks) > 1 else tri_blocks[0],
|
|
355
|
+
dtype=np.int32,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return MmgMesh2D(vertices, triangles)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _meshio_to_mmgs(mesh: meshio.Mesh) -> MmgMeshS:
|
|
362
|
+
"""Convert meshio mesh to MmgMeshS."""
|
|
363
|
+
if _has_non_triangle_cells(mesh):
|
|
364
|
+
logger.warning(_TRIANGULATION_WARNING)
|
|
365
|
+
pv_mesh = _meshio_to_pyvista_polydata(mesh)
|
|
366
|
+
pv_mesh = pv_mesh.triangulate()
|
|
367
|
+
vertices = np.array(pv_mesh.points, dtype=np.float64)
|
|
368
|
+
triangles = _extract_triangles_from_polydata(pv_mesh)
|
|
369
|
+
return MmgMeshS(vertices, triangles)
|
|
370
|
+
|
|
371
|
+
vertices = np.ascontiguousarray(mesh.points, dtype=np.float64)
|
|
372
|
+
|
|
373
|
+
# Collect all triangle blocks
|
|
374
|
+
tri_blocks = [block.data for block in mesh.cells if block.type == "triangle"]
|
|
375
|
+
|
|
376
|
+
if not tri_blocks:
|
|
377
|
+
msg = "No triangles found in mesh"
|
|
378
|
+
raise ValueError(msg)
|
|
379
|
+
|
|
380
|
+
# Concatenate all triangle blocks
|
|
381
|
+
triangles = np.ascontiguousarray(
|
|
382
|
+
np.vstack(tri_blocks) if len(tri_blocks) > 1 else tri_blocks[0],
|
|
383
|
+
dtype=np.int32,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return MmgMeshS(vertices, triangles)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _convert_meshio(
|
|
390
|
+
mesh: meshio.Mesh,
|
|
391
|
+
mesh_kind: MeshKind | None,
|
|
392
|
+
) -> MmgMesh3D | MmgMesh2D | MmgMeshS:
|
|
393
|
+
"""Convert meshio mesh to appropriate mmgpy mesh type."""
|
|
394
|
+
if mesh_kind is None:
|
|
395
|
+
mesh_kind = _detect_mesh_kind(mesh)
|
|
396
|
+
|
|
397
|
+
if mesh_kind == MeshKind.TETRAHEDRAL:
|
|
398
|
+
return _meshio_to_mmg3d(mesh)
|
|
399
|
+
if mesh_kind == MeshKind.TRIANGULAR_2D:
|
|
400
|
+
return _meshio_to_mmg2d(mesh)
|
|
401
|
+
if mesh_kind == MeshKind.TRIANGULAR_SURFACE:
|
|
402
|
+
return _meshio_to_mmgs(mesh)
|
|
403
|
+
|
|
404
|
+
msg = f"Unknown mesh_kind: {mesh_kind}"
|
|
405
|
+
raise ValueError(msg)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def read(
|
|
409
|
+
source: str | Path | pv.UnstructuredGrid | pv.PolyData,
|
|
410
|
+
mesh_kind: MeshKind | None = None,
|
|
411
|
+
) -> Mesh:
|
|
412
|
+
"""Read a mesh from a file or PyVista object.
|
|
413
|
+
|
|
414
|
+
This function provides unified mesh loading from any format supported by
|
|
415
|
+
meshio (40+ formats including VTK, Gmsh, STL, OBJ, etc.) or directly from
|
|
416
|
+
PyVista mesh objects.
|
|
417
|
+
|
|
418
|
+
For Medit format (.mesh) files, native MMG loading is used to preserve
|
|
419
|
+
MMG-specific keywords like Ridges, RequiredVertices, Tangents, and
|
|
420
|
+
reference markers that meshio does not understand.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
source: File path (str or Path) or PyVista mesh object.
|
|
424
|
+
mesh_kind: Force a specific mesh kind instead of auto-detection.
|
|
425
|
+
- MeshKind.TETRAHEDRAL: 3D volumetric mesh
|
|
426
|
+
- MeshKind.TRIANGULAR_2D: 2D planar mesh
|
|
427
|
+
- MeshKind.TRIANGULAR_SURFACE: 3D surface mesh
|
|
428
|
+
- None: Auto-detect based on element types and coordinates
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
A Mesh instance with the appropriate kind.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
ValueError: If mesh kind cannot be determined or file cannot be read.
|
|
435
|
+
TypeError: If source type is not supported.
|
|
436
|
+
FileNotFoundError: If file does not exist.
|
|
437
|
+
|
|
438
|
+
Auto-detection logic:
|
|
439
|
+
- Has tetrahedra → TETRAHEDRAL
|
|
440
|
+
- Has triangles + 3D coords → TRIANGULAR_SURFACE
|
|
441
|
+
- Has triangles + 2D coords (or z~=0) -> TRIANGULAR_2D
|
|
442
|
+
|
|
443
|
+
Supported file formats:
|
|
444
|
+
- Medit: .mesh (native MMG loading, preserves all MMG keywords)
|
|
445
|
+
- VTK: .vtk, .vtu, .vtp (via meshio)
|
|
446
|
+
- Gmsh: .msh (via meshio)
|
|
447
|
+
- STL: .stl (via meshio)
|
|
448
|
+
- OBJ: .obj (via meshio)
|
|
449
|
+
- PLY: .ply (via meshio)
|
|
450
|
+
- And many more via meshio...
|
|
451
|
+
|
|
452
|
+
Example:
|
|
453
|
+
>>> import mmgpy
|
|
454
|
+
>>>
|
|
455
|
+
>>> # Auto-detect mesh kind from file
|
|
456
|
+
>>> mesh = mmgpy.read("tetra_mesh.vtk")
|
|
457
|
+
>>> mesh.kind # MeshKind.TETRAHEDRAL
|
|
458
|
+
>>>
|
|
459
|
+
>>> # Force specific mesh kind
|
|
460
|
+
>>> mesh = mmgpy.read("mesh.vtk", mesh_kind=MeshKind.TRIANGULAR_SURFACE)
|
|
461
|
+
>>>
|
|
462
|
+
>>> # Read Medit file with native loading (preserves Ridges, etc.)
|
|
463
|
+
>>> mesh = mmgpy.read("mesh.mesh")
|
|
464
|
+
>>>
|
|
465
|
+
>>> # Read from PyVista object
|
|
466
|
+
>>> import pyvista as pv
|
|
467
|
+
>>> grid = pv.read("mesh.vtk")
|
|
468
|
+
>>> mesh = mmgpy.read(grid)
|
|
469
|
+
|
|
470
|
+
"""
|
|
471
|
+
# Import here to avoid circular imports
|
|
472
|
+
from mmgpy._mesh import Mesh # noqa: PLC0415
|
|
473
|
+
from mmgpy._pyvista import from_pyvista # noqa: PLC0415
|
|
474
|
+
|
|
475
|
+
# Handle PyVista objects
|
|
476
|
+
if isinstance(source, pv.UnstructuredGrid | pv.PolyData):
|
|
477
|
+
mesh_class = _mesh_kind_to_class(mesh_kind) if mesh_kind else None
|
|
478
|
+
impl = from_pyvista(source, mesh_class)
|
|
479
|
+
kind = _impl_to_kind(impl)
|
|
480
|
+
return Mesh._from_impl(impl, kind) # noqa: SLF001
|
|
481
|
+
|
|
482
|
+
# Handle file paths
|
|
483
|
+
if isinstance(source, str | Path):
|
|
484
|
+
path = Path(source)
|
|
485
|
+
if not path.exists():
|
|
486
|
+
msg = f"File not found: {path}"
|
|
487
|
+
raise FileNotFoundError(msg)
|
|
488
|
+
|
|
489
|
+
# Use native MMG loading for Medit format to preserve MMG-specific
|
|
490
|
+
# keywords (Ridges, RequiredVertices, Tangents, reference markers)
|
|
491
|
+
suffix = path.suffix.lower()
|
|
492
|
+
if suffix == ".mesh":
|
|
493
|
+
impl = _load_medit_native(path, mesh_kind)
|
|
494
|
+
kind = _impl_to_kind(impl)
|
|
495
|
+
return Mesh._from_impl(impl, kind) # noqa: SLF001
|
|
496
|
+
|
|
497
|
+
# Use meshio for other formats
|
|
498
|
+
meshio_mesh = meshio.read(path)
|
|
499
|
+
impl = _convert_meshio(meshio_mesh, mesh_kind)
|
|
500
|
+
kind = _impl_to_kind(impl)
|
|
501
|
+
return Mesh._from_impl(impl, kind) # noqa: SLF001
|
|
502
|
+
|
|
503
|
+
msg = f"Unsupported source type: {type(source)}"
|
|
504
|
+
raise TypeError(msg)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _mesh_kind_to_class(
|
|
508
|
+
mesh_kind: MeshKind,
|
|
509
|
+
) -> type[MmgMesh3D | MmgMesh2D | MmgMeshS]:
|
|
510
|
+
"""Convert MeshKind enum to mesh class."""
|
|
511
|
+
if mesh_kind == MeshKind.TETRAHEDRAL:
|
|
512
|
+
return MmgMesh3D
|
|
513
|
+
if mesh_kind == MeshKind.TRIANGULAR_2D:
|
|
514
|
+
return MmgMesh2D
|
|
515
|
+
if mesh_kind == MeshKind.TRIANGULAR_SURFACE:
|
|
516
|
+
return MmgMeshS
|
|
517
|
+
msg = f"Unknown mesh_kind: {mesh_kind}"
|
|
518
|
+
raise ValueError(msg)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _impl_to_kind(
|
|
522
|
+
impl: MmgMesh3D | MmgMesh2D | MmgMeshS,
|
|
523
|
+
) -> MeshKind:
|
|
524
|
+
"""Convert implementation type to MeshKind."""
|
|
525
|
+
if isinstance(impl, MmgMesh3D):
|
|
526
|
+
return MeshKind.TETRAHEDRAL
|
|
527
|
+
if isinstance(impl, MmgMesh2D):
|
|
528
|
+
return MeshKind.TRIANGULAR_2D
|
|
529
|
+
if isinstance(impl, MmgMeshS):
|
|
530
|
+
return MeshKind.TRIANGULAR_SURFACE
|
|
531
|
+
msg = f"Unknown implementation type: {type(impl)}"
|
|
532
|
+
raise TypeError(msg)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
__all__ = ["read"]
|