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