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/_mesh.py
ADDED
|
@@ -0,0 +1,2286 @@
|
|
|
1
|
+
"""Unified Mesh class for mmgpy.
|
|
2
|
+
|
|
3
|
+
This module provides a single `Mesh` class that wraps the underlying
|
|
4
|
+
MmgMesh3D, MmgMesh2D, and MmgMeshS implementations with auto-detection
|
|
5
|
+
of mesh type.
|
|
6
|
+
|
|
7
|
+
The Mesh class is the primary public API for mmgpy. The underlying C++
|
|
8
|
+
bindings (MmgMesh3D, MmgMesh2D, MmgMeshS) are implementation details
|
|
9
|
+
and should not be used directly.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from mmgpy import Mesh, MeshKind
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Auto-detect mesh type from data
|
|
15
|
+
>>> mesh = Mesh(vertices, cells)
|
|
16
|
+
>>> mesh.kind # MeshKind.TETRAHEDRAL
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Remesh and save
|
|
19
|
+
>>> mesh.remesh(hmax=0.1)
|
|
20
|
+
>>> mesh.save("output.vtk")
|
|
21
|
+
|
|
22
|
+
>>> # Context manager usage
|
|
23
|
+
>>> with Mesh(vertices, cells) as mesh:
|
|
24
|
+
... mesh.remesh(hmax=0.1)
|
|
25
|
+
... mesh.save("output.vtk")
|
|
26
|
+
|
|
27
|
+
>>> # Transactional modifications with checkpoint
|
|
28
|
+
>>> mesh = Mesh(vertices, cells)
|
|
29
|
+
>>> with mesh.checkpoint() as snapshot:
|
|
30
|
+
... mesh.remesh(hmax=0.01)
|
|
31
|
+
... if mesh.validate():
|
|
32
|
+
... snapshot.commit()
|
|
33
|
+
... else:
|
|
34
|
+
... snapshot.rollback()
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from contextlib import contextmanager
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from enum import Enum
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
import pyvista as pv
|
|
48
|
+
|
|
49
|
+
from mmgpy._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from collections.abc import Callable, Generator, Sequence
|
|
53
|
+
from types import TracebackType
|
|
54
|
+
|
|
55
|
+
from numpy.typing import NDArray
|
|
56
|
+
|
|
57
|
+
from mmgpy._options import Mmg2DOptions, Mmg3DOptions, MmgSOptions
|
|
58
|
+
from mmgpy._progress import (
|
|
59
|
+
ProgressCallback,
|
|
60
|
+
ProgressEvent,
|
|
61
|
+
RichProgressReporter,
|
|
62
|
+
)
|
|
63
|
+
from mmgpy._result import RemeshResult
|
|
64
|
+
from mmgpy._validation import ValidationReport
|
|
65
|
+
from mmgpy.sizing import SizingConstraint
|
|
66
|
+
|
|
67
|
+
# Progress can be True (default rich), False (disabled), or a callback
|
|
68
|
+
ProgressParam = bool | Callable[[ProgressEvent], bool] | None
|
|
69
|
+
|
|
70
|
+
# Field transfer can be True (all fields), False (no transfer), or list of names
|
|
71
|
+
FieldTransferParam = bool | Sequence[str] | None
|
|
72
|
+
|
|
73
|
+
_DIMS_2D = 2
|
|
74
|
+
_DIMS_3D = 3
|
|
75
|
+
_TETRA_VERTS = 4
|
|
76
|
+
_TRI_VERTS = 3
|
|
77
|
+
_2D_DETECTION_TOLERANCE = 1e-8
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_interactive_terminal() -> bool: # pragma: no cover
|
|
81
|
+
"""Check if we're running in an interactive terminal.
|
|
82
|
+
|
|
83
|
+
Returns False in CI environments, pytest, or when stdout is not a TTY.
|
|
84
|
+
"""
|
|
85
|
+
import os # noqa: PLC0415
|
|
86
|
+
import sys # noqa: PLC0415
|
|
87
|
+
|
|
88
|
+
# Check for common CI environment variables
|
|
89
|
+
ci_vars = ("CI", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS", "CIRCLECI", "JENKINS_URL")
|
|
90
|
+
if any(os.environ.get(var) for var in ci_vars):
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Check if running under pytest
|
|
94
|
+
if "pytest" in sys.modules:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Check if stdout is a TTY
|
|
98
|
+
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_progress_callback(
|
|
102
|
+
progress: ProgressParam,
|
|
103
|
+
) -> tuple[ProgressCallback | None, RichProgressReporter | None]:
|
|
104
|
+
"""Resolve progress parameter to callback and optional context manager.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
progress : bool | Callable | None
|
|
109
|
+
The progress parameter from remesh methods.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
tuple[ProgressCallback | None, RichProgressReporter | None]
|
|
114
|
+
A tuple of (callback, reporter_ctx). If reporter_ctx is not None,
|
|
115
|
+
it must be used as a context manager to manage the Rich display.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
from mmgpy._progress import RichProgressReporter # noqa: PLC0415
|
|
119
|
+
|
|
120
|
+
if progress is True:
|
|
121
|
+
# Only show progress bar in interactive terminals
|
|
122
|
+
if _is_interactive_terminal(): # pragma: no cover
|
|
123
|
+
reporter = RichProgressReporter(transient=True)
|
|
124
|
+
return reporter, reporter
|
|
125
|
+
return None, None
|
|
126
|
+
if callable(progress):
|
|
127
|
+
return progress, None
|
|
128
|
+
return None, None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _dict_to_remesh_result(stats: dict[str, Any]) -> RemeshResult:
|
|
132
|
+
"""Convert C++ remesh statistics dict to RemeshResult dataclass."""
|
|
133
|
+
from mmgpy._result import RemeshResult as _RemeshResult # noqa: PLC0415
|
|
134
|
+
|
|
135
|
+
return _RemeshResult(
|
|
136
|
+
vertices_before=stats["vertices_before"],
|
|
137
|
+
vertices_after=stats["vertices_after"],
|
|
138
|
+
elements_before=stats["elements_before"],
|
|
139
|
+
elements_after=stats["elements_after"],
|
|
140
|
+
triangles_before=stats["triangles_before"],
|
|
141
|
+
triangles_after=stats["triangles_after"],
|
|
142
|
+
edges_before=stats["edges_before"],
|
|
143
|
+
edges_after=stats["edges_after"],
|
|
144
|
+
quality_min_before=stats["quality_min_before"],
|
|
145
|
+
quality_min_after=stats["quality_min_after"],
|
|
146
|
+
quality_mean_before=stats["quality_mean_before"],
|
|
147
|
+
quality_mean_after=stats["quality_mean_after"],
|
|
148
|
+
duration_seconds=stats["duration_seconds"],
|
|
149
|
+
warnings=tuple(stats["warnings"]),
|
|
150
|
+
return_code=stats["return_code"],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class MeshKind(Enum):
|
|
155
|
+
"""Enumeration of mesh types.
|
|
156
|
+
|
|
157
|
+
Attributes
|
|
158
|
+
----------
|
|
159
|
+
TETRAHEDRAL
|
|
160
|
+
3D volumetric mesh with tetrahedral elements.
|
|
161
|
+
TRIANGULAR_2D
|
|
162
|
+
2D planar mesh with triangular elements.
|
|
163
|
+
TRIANGULAR_SURFACE
|
|
164
|
+
3D surface mesh with triangular elements.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
TETRAHEDRAL = "tetrahedral"
|
|
169
|
+
TRIANGULAR_2D = "triangular_2d"
|
|
170
|
+
TRIANGULAR_SURFACE = "triangular_surface"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _is_2d_points(points: NDArray[np.floating]) -> bool:
|
|
174
|
+
"""Check if points are essentially 2D (z coordinates are zero or near-zero)."""
|
|
175
|
+
if points.shape[1] == _DIMS_2D:
|
|
176
|
+
return True
|
|
177
|
+
if points.shape[1] == _DIMS_3D:
|
|
178
|
+
z_coords = points[:, 2]
|
|
179
|
+
return bool(np.allclose(z_coords, 0, atol=_2D_DETECTION_TOLERANCE))
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _detect_mesh_kind(
|
|
184
|
+
vertices: NDArray[np.floating],
|
|
185
|
+
cells: NDArray[np.integer],
|
|
186
|
+
) -> MeshKind:
|
|
187
|
+
"""Detect mesh kind from vertices and cells arrays.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
vertices : ndarray
|
|
192
|
+
Vertex coordinates (Nx2 or Nx3).
|
|
193
|
+
cells : ndarray
|
|
194
|
+
Cell connectivity (NxM where M is vertices per cell).
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
MeshKind
|
|
199
|
+
Detected mesh kind.
|
|
200
|
+
|
|
201
|
+
Raises
|
|
202
|
+
------
|
|
203
|
+
ValueError
|
|
204
|
+
If mesh type cannot be determined.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
n_cell_verts = cells.shape[1]
|
|
208
|
+
|
|
209
|
+
if n_cell_verts == _TETRA_VERTS:
|
|
210
|
+
return MeshKind.TETRAHEDRAL
|
|
211
|
+
|
|
212
|
+
if n_cell_verts == _TRI_VERTS:
|
|
213
|
+
if _is_2d_points(vertices):
|
|
214
|
+
return MeshKind.TRIANGULAR_2D
|
|
215
|
+
return MeshKind.TRIANGULAR_SURFACE
|
|
216
|
+
|
|
217
|
+
msg = f"Cannot determine mesh type from cells with {n_cell_verts} vertices per cell"
|
|
218
|
+
raise ValueError(msg)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _create_impl(
|
|
222
|
+
vertices: NDArray[np.floating],
|
|
223
|
+
cells: NDArray[np.integer],
|
|
224
|
+
kind: MeshKind,
|
|
225
|
+
) -> MmgMesh3D | MmgMesh2D | MmgMeshS:
|
|
226
|
+
"""Create the appropriate mesh implementation.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
vertices : ndarray
|
|
231
|
+
Vertex coordinates.
|
|
232
|
+
cells : ndarray
|
|
233
|
+
Cell connectivity.
|
|
234
|
+
kind : MeshKind
|
|
235
|
+
Mesh kind to create.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
240
|
+
The mesh implementation.
|
|
241
|
+
|
|
242
|
+
"""
|
|
243
|
+
vertices = np.ascontiguousarray(vertices, dtype=np.float64)
|
|
244
|
+
cells = np.ascontiguousarray(cells, dtype=np.int32)
|
|
245
|
+
|
|
246
|
+
if kind == MeshKind.TETRAHEDRAL:
|
|
247
|
+
return MmgMesh3D(vertices, cells)
|
|
248
|
+
|
|
249
|
+
if kind == MeshKind.TRIANGULAR_2D:
|
|
250
|
+
# Ensure 2D vertices
|
|
251
|
+
if vertices.shape[1] == _DIMS_3D:
|
|
252
|
+
vertices = np.ascontiguousarray(vertices[:, :2])
|
|
253
|
+
return MmgMesh2D(vertices, cells)
|
|
254
|
+
|
|
255
|
+
if kind == MeshKind.TRIANGULAR_SURFACE:
|
|
256
|
+
return MmgMeshS(vertices, cells)
|
|
257
|
+
|
|
258
|
+
msg = f"Unknown mesh kind: {kind}"
|
|
259
|
+
raise ValueError(msg)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class MeshCheckpoint:
|
|
264
|
+
"""Snapshot of mesh state for rollback.
|
|
265
|
+
|
|
266
|
+
This class is returned by `Mesh.checkpoint()` and provides transactional
|
|
267
|
+
semantics for mesh modifications. Changes are automatically rolled back
|
|
268
|
+
on context exit unless `commit()` is called.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
mesh : Mesh
|
|
273
|
+
The mesh to checkpoint.
|
|
274
|
+
|
|
275
|
+
Notes
|
|
276
|
+
-----
|
|
277
|
+
The checkpoint stores a complete copy of the mesh data including vertices,
|
|
278
|
+
elements, reference markers, and solution fields (metric, displacement,
|
|
279
|
+
levelset). For large meshes, this may consume significant memory.
|
|
280
|
+
|
|
281
|
+
Note: The tensor field is not saved because it shares memory with metric
|
|
282
|
+
in MMG's internal representation. Only one of metric or tensor can be
|
|
283
|
+
set at a time.
|
|
284
|
+
|
|
285
|
+
Examples
|
|
286
|
+
--------
|
|
287
|
+
>>> mesh = Mesh(vertices, cells)
|
|
288
|
+
>>> with mesh.checkpoint() as snapshot:
|
|
289
|
+
... mesh.remesh(hmax=0.01)
|
|
290
|
+
... if mesh.validate():
|
|
291
|
+
... snapshot.commit() # Keep changes
|
|
292
|
+
... # Otherwise, changes are automatically rolled back
|
|
293
|
+
|
|
294
|
+
>>> # Automatic rollback on exception
|
|
295
|
+
>>> with mesh.checkpoint():
|
|
296
|
+
... mesh.remesh(hmax=0.01)
|
|
297
|
+
... raise ValueError("Something went wrong")
|
|
298
|
+
>>> # mesh is restored to original state
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
_mesh: Mesh
|
|
303
|
+
_vertices: NDArray[np.float64] = field(repr=False)
|
|
304
|
+
_vertex_refs: NDArray[np.int64] = field(repr=False)
|
|
305
|
+
_triangles: NDArray[np.int32] = field(repr=False)
|
|
306
|
+
_triangle_refs: NDArray[np.int64] = field(repr=False)
|
|
307
|
+
_edges: NDArray[np.int32] = field(repr=False)
|
|
308
|
+
_edge_refs: NDArray[np.int64] = field(repr=False)
|
|
309
|
+
_tetrahedra: NDArray[np.int32] | None = field(default=None, repr=False)
|
|
310
|
+
_tetrahedra_refs: NDArray[np.int64] | None = field(default=None, repr=False)
|
|
311
|
+
_fields: dict[str, NDArray[np.float64]] = field(default_factory=dict, repr=False)
|
|
312
|
+
_committed: bool = field(default=False, repr=False)
|
|
313
|
+
|
|
314
|
+
def commit(self) -> None:
|
|
315
|
+
"""Keep the current mesh state.
|
|
316
|
+
|
|
317
|
+
Call this method to prevent rollback when the context manager exits.
|
|
318
|
+
"""
|
|
319
|
+
self._committed = True
|
|
320
|
+
|
|
321
|
+
def rollback(self) -> None:
|
|
322
|
+
"""Restore the mesh to its checkpoint state.
|
|
323
|
+
|
|
324
|
+
This is called automatically on context exit if `commit()` was not called,
|
|
325
|
+
or if an exception occurred. Can also be called manually.
|
|
326
|
+
"""
|
|
327
|
+
mesh = self._mesh
|
|
328
|
+
kind = mesh._kind # noqa: SLF001
|
|
329
|
+
|
|
330
|
+
if kind == MeshKind.TETRAHEDRAL:
|
|
331
|
+
if self._tetrahedra is None or self._tetrahedra_refs is None:
|
|
332
|
+
msg = "Tetrahedra data missing in checkpoint"
|
|
333
|
+
raise RuntimeError(msg)
|
|
334
|
+
impl = cast("MmgMesh3D", mesh._impl) # noqa: SLF001
|
|
335
|
+
impl.set_mesh_size(
|
|
336
|
+
vertices=len(self._vertices),
|
|
337
|
+
tetrahedra=len(self._tetrahedra),
|
|
338
|
+
triangles=len(self._triangles),
|
|
339
|
+
edges=len(self._edges),
|
|
340
|
+
)
|
|
341
|
+
impl.set_vertices(self._vertices, self._vertex_refs)
|
|
342
|
+
impl.set_tetrahedra(self._tetrahedra, self._tetrahedra_refs)
|
|
343
|
+
if len(self._triangles) > 0:
|
|
344
|
+
impl.set_triangles(self._triangles, self._triangle_refs)
|
|
345
|
+
if len(self._edges) > 0:
|
|
346
|
+
impl.set_edges(self._edges, self._edge_refs)
|
|
347
|
+
elif kind == MeshKind.TRIANGULAR_2D:
|
|
348
|
+
impl_2d = cast("MmgMesh2D", mesh._impl) # noqa: SLF001
|
|
349
|
+
impl_2d.set_mesh_size(
|
|
350
|
+
vertices=len(self._vertices),
|
|
351
|
+
triangles=len(self._triangles),
|
|
352
|
+
edges=len(self._edges),
|
|
353
|
+
)
|
|
354
|
+
impl_2d.set_vertices(self._vertices, self._vertex_refs)
|
|
355
|
+
impl_2d.set_triangles(self._triangles, self._triangle_refs)
|
|
356
|
+
if len(self._edges) > 0:
|
|
357
|
+
impl_2d.set_edges(self._edges, self._edge_refs)
|
|
358
|
+
else: # TRIANGULAR_SURFACE
|
|
359
|
+
impl_s = cast("MmgMeshS", mesh._impl) # noqa: SLF001
|
|
360
|
+
impl_s.set_mesh_size(
|
|
361
|
+
vertices=len(self._vertices),
|
|
362
|
+
triangles=len(self._triangles),
|
|
363
|
+
edges=len(self._edges),
|
|
364
|
+
)
|
|
365
|
+
impl_s.set_vertices(self._vertices, self._vertex_refs)
|
|
366
|
+
impl_s.set_triangles(self._triangles, self._triangle_refs)
|
|
367
|
+
if len(self._edges) > 0:
|
|
368
|
+
impl_s.set_edges(self._edges, self._edge_refs)
|
|
369
|
+
|
|
370
|
+
# Restore solution fields
|
|
371
|
+
for field_name, field_data in self._fields.items():
|
|
372
|
+
mesh._impl.set_field(field_name, field_data) # noqa: SLF001
|
|
373
|
+
|
|
374
|
+
def __enter__(self) -> MeshCheckpoint: # noqa: PYI034
|
|
375
|
+
"""Enter the context manager."""
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
def __exit__(
|
|
379
|
+
self,
|
|
380
|
+
exc_type: type[BaseException] | None,
|
|
381
|
+
exc_val: BaseException | None,
|
|
382
|
+
exc_tb: TracebackType | None,
|
|
383
|
+
) -> bool:
|
|
384
|
+
"""Exit the context manager, rolling back if not committed or on exception."""
|
|
385
|
+
if exc_type is not None or not self._committed:
|
|
386
|
+
self.rollback()
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class Mesh:
|
|
391
|
+
"""Unified mesh class with auto-detection of mesh type.
|
|
392
|
+
|
|
393
|
+
This class provides a single interface for working with 2D planar,
|
|
394
|
+
3D volumetric, and 3D surface meshes. The mesh type is automatically
|
|
395
|
+
detected from the input data.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
source : ndarray | str | Path | pv.UnstructuredGrid | pv.PolyData
|
|
400
|
+
Either:
|
|
401
|
+
- Vertex coordinates array (requires `cells` parameter)
|
|
402
|
+
- File path to load mesh from
|
|
403
|
+
- PyVista mesh object
|
|
404
|
+
cells : ndarray, optional
|
|
405
|
+
Cell connectivity array. Required when `source` is vertices.
|
|
406
|
+
|
|
407
|
+
Attributes
|
|
408
|
+
----------
|
|
409
|
+
kind : MeshKind
|
|
410
|
+
The type of mesh (TETRAHEDRAL, TRIANGULAR_2D, or TRIANGULAR_SURFACE).
|
|
411
|
+
|
|
412
|
+
Examples
|
|
413
|
+
--------
|
|
414
|
+
Create a mesh from vertices and cells:
|
|
415
|
+
|
|
416
|
+
>>> vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]])
|
|
417
|
+
>>> cells = np.array([[0, 1, 2, 3]])
|
|
418
|
+
>>> mesh = Mesh(vertices, cells)
|
|
419
|
+
>>> mesh.kind
|
|
420
|
+
MeshKind.TETRAHEDRAL
|
|
421
|
+
|
|
422
|
+
Load a mesh from file:
|
|
423
|
+
|
|
424
|
+
>>> mesh = Mesh("mesh.vtk")
|
|
425
|
+
|
|
426
|
+
Create from PyVista:
|
|
427
|
+
|
|
428
|
+
>>> pv_mesh = pv.read("mesh.vtk")
|
|
429
|
+
>>> mesh = Mesh(pv_mesh)
|
|
430
|
+
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
__slots__ = ("_impl", "_kind", "_sizing_constraints", "_user_fields")
|
|
434
|
+
|
|
435
|
+
_impl: MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
436
|
+
_kind: MeshKind
|
|
437
|
+
_sizing_constraints: list[SizingConstraint]
|
|
438
|
+
_user_fields: dict[str, NDArray[np.float64]]
|
|
439
|
+
|
|
440
|
+
def __init__(
|
|
441
|
+
self,
|
|
442
|
+
source: NDArray[np.floating] | str | Path | pv.UnstructuredGrid | pv.PolyData,
|
|
443
|
+
cells: NDArray[np.integer] | None = None,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Initialize a Mesh from various sources."""
|
|
446
|
+
# Import here to avoid circular imports
|
|
447
|
+
from mmgpy._io import read as _read_mesh # noqa: PLC0415
|
|
448
|
+
|
|
449
|
+
self._sizing_constraints = []
|
|
450
|
+
self._user_fields = {}
|
|
451
|
+
|
|
452
|
+
# Handle PyVista objects
|
|
453
|
+
if isinstance(source, pv.UnstructuredGrid | pv.PolyData):
|
|
454
|
+
result = _read_mesh(source)
|
|
455
|
+
self._impl = result._impl # noqa: SLF001
|
|
456
|
+
self._kind = result._kind # noqa: SLF001
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
# Handle file paths
|
|
460
|
+
if isinstance(source, str | Path):
|
|
461
|
+
result = _read_mesh(source)
|
|
462
|
+
self._impl = result._impl # noqa: SLF001
|
|
463
|
+
self._kind = result._kind # noqa: SLF001
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# Handle vertices + cells
|
|
467
|
+
if cells is None:
|
|
468
|
+
msg = "cells parameter is required when source is a vertices array"
|
|
469
|
+
raise ValueError(msg)
|
|
470
|
+
|
|
471
|
+
vertices = np.asarray(source)
|
|
472
|
+
cells = np.asarray(cells)
|
|
473
|
+
|
|
474
|
+
self._kind = _detect_mesh_kind(vertices, cells)
|
|
475
|
+
self._impl = _create_impl(vertices, cells, self._kind)
|
|
476
|
+
|
|
477
|
+
@classmethod
|
|
478
|
+
def _from_impl(
|
|
479
|
+
cls,
|
|
480
|
+
impl: MmgMesh3D | MmgMesh2D | MmgMeshS,
|
|
481
|
+
kind: MeshKind,
|
|
482
|
+
) -> Mesh:
|
|
483
|
+
"""Create a Mesh from an existing implementation (internal use).
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
impl : MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
488
|
+
The underlying mesh implementation.
|
|
489
|
+
kind : MeshKind
|
|
490
|
+
The mesh kind.
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
Mesh
|
|
495
|
+
A new Mesh wrapping the implementation.
|
|
496
|
+
|
|
497
|
+
"""
|
|
498
|
+
mesh = object.__new__(cls)
|
|
499
|
+
mesh._impl = impl # noqa: SLF001
|
|
500
|
+
mesh._kind = kind # noqa: SLF001
|
|
501
|
+
mesh._sizing_constraints = [] # noqa: SLF001
|
|
502
|
+
mesh._user_fields = {} # noqa: SLF001
|
|
503
|
+
return mesh
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def kind(self) -> MeshKind:
|
|
507
|
+
"""Get the mesh kind.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
MeshKind
|
|
512
|
+
The type of mesh.
|
|
513
|
+
|
|
514
|
+
"""
|
|
515
|
+
return self._kind
|
|
516
|
+
|
|
517
|
+
# =========================================================================
|
|
518
|
+
# Vertex operations
|
|
519
|
+
# =========================================================================
|
|
520
|
+
|
|
521
|
+
def get_vertices(self) -> NDArray[np.float64]:
|
|
522
|
+
"""Get vertex coordinates.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
ndarray
|
|
527
|
+
Vertex coordinates (Nx2 for 2D, Nx3 for 3D).
|
|
528
|
+
|
|
529
|
+
"""
|
|
530
|
+
return self._impl.get_vertices()
|
|
531
|
+
|
|
532
|
+
def get_vertices_with_refs(self) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
|
|
533
|
+
"""Get vertex coordinates and reference markers.
|
|
534
|
+
|
|
535
|
+
Returns
|
|
536
|
+
-------
|
|
537
|
+
vertices : ndarray
|
|
538
|
+
Vertex coordinates.
|
|
539
|
+
refs : ndarray
|
|
540
|
+
Reference markers for each vertex.
|
|
541
|
+
|
|
542
|
+
"""
|
|
543
|
+
return self._impl.get_vertices_with_refs()
|
|
544
|
+
|
|
545
|
+
def set_vertices(
|
|
546
|
+
self,
|
|
547
|
+
vertices: NDArray[np.float64],
|
|
548
|
+
refs: NDArray[np.int64] | None = None,
|
|
549
|
+
) -> None:
|
|
550
|
+
"""Set vertex coordinates.
|
|
551
|
+
|
|
552
|
+
Parameters
|
|
553
|
+
----------
|
|
554
|
+
vertices : ndarray
|
|
555
|
+
Vertex coordinates.
|
|
556
|
+
refs : ndarray, optional
|
|
557
|
+
Reference markers for each vertex.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
self._impl.set_vertices(vertices, refs)
|
|
561
|
+
|
|
562
|
+
# =========================================================================
|
|
563
|
+
# Triangle operations (shared by all types)
|
|
564
|
+
# =========================================================================
|
|
565
|
+
|
|
566
|
+
def get_triangles(self) -> NDArray[np.int32]:
|
|
567
|
+
"""Get triangle connectivity.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
ndarray
|
|
572
|
+
Triangle connectivity (Nx3).
|
|
573
|
+
|
|
574
|
+
"""
|
|
575
|
+
return self._impl.get_triangles()
|
|
576
|
+
|
|
577
|
+
def get_triangles_with_refs(self) -> tuple[NDArray[np.int32], NDArray[np.int64]]:
|
|
578
|
+
"""Get triangle connectivity and reference markers.
|
|
579
|
+
|
|
580
|
+
Returns
|
|
581
|
+
-------
|
|
582
|
+
triangles : ndarray
|
|
583
|
+
Triangle connectivity.
|
|
584
|
+
refs : ndarray
|
|
585
|
+
Reference markers for each triangle.
|
|
586
|
+
|
|
587
|
+
"""
|
|
588
|
+
return self._impl.get_triangles_with_refs()
|
|
589
|
+
|
|
590
|
+
def set_triangles(
|
|
591
|
+
self,
|
|
592
|
+
triangles: NDArray[np.int32],
|
|
593
|
+
refs: NDArray[np.int64] | None = None,
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Set triangle connectivity.
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
triangles : ndarray
|
|
600
|
+
Triangle connectivity (Nx3).
|
|
601
|
+
refs : ndarray, optional
|
|
602
|
+
Reference markers for each triangle.
|
|
603
|
+
|
|
604
|
+
"""
|
|
605
|
+
self._impl.set_triangles(triangles, refs)
|
|
606
|
+
|
|
607
|
+
# =========================================================================
|
|
608
|
+
# Edge operations
|
|
609
|
+
# =========================================================================
|
|
610
|
+
|
|
611
|
+
def get_edges(self) -> NDArray[np.int32]:
|
|
612
|
+
"""Get edge connectivity.
|
|
613
|
+
|
|
614
|
+
Returns
|
|
615
|
+
-------
|
|
616
|
+
ndarray
|
|
617
|
+
Edge connectivity (Nx2).
|
|
618
|
+
|
|
619
|
+
"""
|
|
620
|
+
return self._impl.get_edges()
|
|
621
|
+
|
|
622
|
+
def get_edges_with_refs(self) -> tuple[NDArray[np.int32], NDArray[np.int64]]:
|
|
623
|
+
"""Get edge connectivity and reference markers.
|
|
624
|
+
|
|
625
|
+
Returns
|
|
626
|
+
-------
|
|
627
|
+
edges : ndarray
|
|
628
|
+
Edge connectivity.
|
|
629
|
+
refs : ndarray
|
|
630
|
+
Reference markers for each edge.
|
|
631
|
+
|
|
632
|
+
"""
|
|
633
|
+
return self._impl.get_edges_with_refs()
|
|
634
|
+
|
|
635
|
+
def set_edges(
|
|
636
|
+
self,
|
|
637
|
+
edges: NDArray[np.int32],
|
|
638
|
+
refs: NDArray[np.int64] | None = None,
|
|
639
|
+
) -> None:
|
|
640
|
+
"""Set edge connectivity.
|
|
641
|
+
|
|
642
|
+
Parameters
|
|
643
|
+
----------
|
|
644
|
+
edges : ndarray
|
|
645
|
+
Edge connectivity (Nx2).
|
|
646
|
+
refs : ndarray, optional
|
|
647
|
+
Reference markers for each edge.
|
|
648
|
+
|
|
649
|
+
"""
|
|
650
|
+
self._impl.set_edges(edges, refs)
|
|
651
|
+
|
|
652
|
+
# =========================================================================
|
|
653
|
+
# Tetrahedra operations (TETRAHEDRAL only)
|
|
654
|
+
# =========================================================================
|
|
655
|
+
|
|
656
|
+
def get_tetrahedra(self) -> NDArray[np.int32]:
|
|
657
|
+
"""Get tetrahedra connectivity.
|
|
658
|
+
|
|
659
|
+
Only available for TETRAHEDRAL meshes.
|
|
660
|
+
|
|
661
|
+
Returns
|
|
662
|
+
-------
|
|
663
|
+
ndarray
|
|
664
|
+
Tetrahedra connectivity (Nx4).
|
|
665
|
+
|
|
666
|
+
Raises
|
|
667
|
+
------
|
|
668
|
+
TypeError
|
|
669
|
+
If mesh is not TETRAHEDRAL.
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
if self._kind != MeshKind.TETRAHEDRAL:
|
|
673
|
+
msg = "get_tetrahedra() is only available for TETRAHEDRAL meshes"
|
|
674
|
+
raise TypeError(msg)
|
|
675
|
+
return self._impl.get_tetrahedra() # type: ignore[union-attr]
|
|
676
|
+
|
|
677
|
+
def get_tetrahedra_with_refs(
|
|
678
|
+
self,
|
|
679
|
+
) -> tuple[NDArray[np.int32], NDArray[np.int64]]:
|
|
680
|
+
"""Get tetrahedra connectivity and reference markers.
|
|
681
|
+
|
|
682
|
+
Only available for TETRAHEDRAL meshes.
|
|
683
|
+
|
|
684
|
+
Returns
|
|
685
|
+
-------
|
|
686
|
+
tetrahedra : ndarray
|
|
687
|
+
Tetrahedra connectivity.
|
|
688
|
+
refs : ndarray
|
|
689
|
+
Reference markers for each tetrahedron.
|
|
690
|
+
|
|
691
|
+
Raises
|
|
692
|
+
------
|
|
693
|
+
TypeError
|
|
694
|
+
If mesh is not TETRAHEDRAL.
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
if self._kind != MeshKind.TETRAHEDRAL:
|
|
698
|
+
msg = "get_tetrahedra_with_refs() is only available for TETRAHEDRAL meshes"
|
|
699
|
+
raise TypeError(msg)
|
|
700
|
+
return self._impl.get_tetrahedra_with_refs() # type: ignore[union-attr]
|
|
701
|
+
|
|
702
|
+
def get_elements(self) -> NDArray[np.int32]:
|
|
703
|
+
"""Get primary element connectivity (alias for get_tetrahedra).
|
|
704
|
+
|
|
705
|
+
Only available for TETRAHEDRAL meshes.
|
|
706
|
+
|
|
707
|
+
Returns
|
|
708
|
+
-------
|
|
709
|
+
ndarray
|
|
710
|
+
Element connectivity (Nx4 tetrahedra).
|
|
711
|
+
|
|
712
|
+
Raises
|
|
713
|
+
------
|
|
714
|
+
TypeError
|
|
715
|
+
If mesh is not TETRAHEDRAL.
|
|
716
|
+
|
|
717
|
+
"""
|
|
718
|
+
if self._kind != MeshKind.TETRAHEDRAL:
|
|
719
|
+
msg = "get_elements() is only available for TETRAHEDRAL meshes"
|
|
720
|
+
raise TypeError(msg)
|
|
721
|
+
return self._impl.get_elements() # type: ignore[union-attr]
|
|
722
|
+
|
|
723
|
+
def get_elements_with_refs(self) -> tuple[NDArray[np.int32], NDArray[np.int64]]:
|
|
724
|
+
"""Get primary element connectivity and reference markers.
|
|
725
|
+
|
|
726
|
+
Only available for TETRAHEDRAL meshes.
|
|
727
|
+
|
|
728
|
+
Returns
|
|
729
|
+
-------
|
|
730
|
+
elements : ndarray
|
|
731
|
+
Element connectivity.
|
|
732
|
+
refs : ndarray
|
|
733
|
+
Reference markers for each element.
|
|
734
|
+
|
|
735
|
+
Raises
|
|
736
|
+
------
|
|
737
|
+
TypeError
|
|
738
|
+
If mesh is not TETRAHEDRAL.
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
if self._kind != MeshKind.TETRAHEDRAL:
|
|
742
|
+
msg = "get_elements_with_refs() is only available for TETRAHEDRAL meshes"
|
|
743
|
+
raise TypeError(msg)
|
|
744
|
+
return self._impl.get_elements_with_refs() # type: ignore[union-attr]
|
|
745
|
+
|
|
746
|
+
# =========================================================================
|
|
747
|
+
# Field operations (solution data)
|
|
748
|
+
# =========================================================================
|
|
749
|
+
|
|
750
|
+
def set_field(self, key: str, value: NDArray[np.float64]) -> None:
|
|
751
|
+
"""Set a solution field.
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
key : str
|
|
756
|
+
Field name.
|
|
757
|
+
value : ndarray
|
|
758
|
+
Field values (one per vertex).
|
|
759
|
+
|
|
760
|
+
"""
|
|
761
|
+
self._impl.set_field(key, value)
|
|
762
|
+
|
|
763
|
+
def get_field(self, key: str) -> NDArray[np.float64]:
|
|
764
|
+
"""Get a solution field.
|
|
765
|
+
|
|
766
|
+
Parameters
|
|
767
|
+
----------
|
|
768
|
+
key : str
|
|
769
|
+
Field name.
|
|
770
|
+
|
|
771
|
+
Returns
|
|
772
|
+
-------
|
|
773
|
+
ndarray
|
|
774
|
+
Field values.
|
|
775
|
+
|
|
776
|
+
"""
|
|
777
|
+
return self._impl.get_field(key)
|
|
778
|
+
|
|
779
|
+
def _try_get_field(self, key: str) -> NDArray[np.float64] | None:
|
|
780
|
+
"""Try to get a field, returning None if not set or contains garbage.
|
|
781
|
+
|
|
782
|
+
The underlying C++ bindings may return uninitialized memory for fields
|
|
783
|
+
that haven't been explicitly set. This method filters out such garbage
|
|
784
|
+
by checking for subnormal (denormalized) floating point values.
|
|
785
|
+
"""
|
|
786
|
+
try:
|
|
787
|
+
data = self._impl.get_field(key)
|
|
788
|
+
except RuntimeError:
|
|
789
|
+
return None
|
|
790
|
+
# Check for uninitialized memory: subnormal values indicate garbage
|
|
791
|
+
if np.any(~np.isfinite(data)) or np.any(
|
|
792
|
+
(data != 0) & (np.abs(data) < np.finfo(np.float64).tiny),
|
|
793
|
+
):
|
|
794
|
+
return None
|
|
795
|
+
return data
|
|
796
|
+
|
|
797
|
+
def __setitem__(self, key: str, value: NDArray[np.float64]) -> None:
|
|
798
|
+
"""Set a solution field using dictionary syntax."""
|
|
799
|
+
self._impl[key] = value
|
|
800
|
+
|
|
801
|
+
def __getitem__(self, key: str) -> NDArray[np.float64]:
|
|
802
|
+
"""Get a solution field using dictionary syntax."""
|
|
803
|
+
return self._impl[key]
|
|
804
|
+
|
|
805
|
+
# =========================================================================
|
|
806
|
+
# User field operations (arbitrary fields for transfer)
|
|
807
|
+
# =========================================================================
|
|
808
|
+
|
|
809
|
+
def set_user_field(self, name: str, values: NDArray[np.float64]) -> None:
|
|
810
|
+
"""Set a user-defined field for transfer during remeshing.
|
|
811
|
+
|
|
812
|
+
Unlike MMG's built-in fields (metric, displacement, levelset), user fields
|
|
813
|
+
are arbitrary data arrays that can be transferred to the new mesh after
|
|
814
|
+
remeshing via interpolation.
|
|
815
|
+
|
|
816
|
+
Parameters
|
|
817
|
+
----------
|
|
818
|
+
name : str
|
|
819
|
+
Field name (any string except reserved names like "metric").
|
|
820
|
+
values : ndarray
|
|
821
|
+
Field values, shape (n_vertices,) for scalars or
|
|
822
|
+
(n_vertices, n_components) for vectors/tensors.
|
|
823
|
+
|
|
824
|
+
Examples
|
|
825
|
+
--------
|
|
826
|
+
>>> mesh.set_user_field("temperature", temperature_array)
|
|
827
|
+
>>> mesh.set_user_field("velocity", velocity_array) # (N, 3) vector
|
|
828
|
+
>>> mesh.remesh(hmax=0.1, transfer_fields=True)
|
|
829
|
+
>>> new_temp = mesh.get_user_field("temperature")
|
|
830
|
+
|
|
831
|
+
"""
|
|
832
|
+
n_vertices = len(self.get_vertices())
|
|
833
|
+
values = np.asarray(values, dtype=np.float64)
|
|
834
|
+
if values.shape[0] != n_vertices:
|
|
835
|
+
msg = (
|
|
836
|
+
f"Field '{name}' has {values.shape[0]} values but mesh "
|
|
837
|
+
f"has {n_vertices} vertices"
|
|
838
|
+
)
|
|
839
|
+
raise ValueError(msg)
|
|
840
|
+
self._user_fields[name] = values
|
|
841
|
+
|
|
842
|
+
def get_user_field(self, name: str) -> NDArray[np.float64]:
|
|
843
|
+
"""Get a user-defined field.
|
|
844
|
+
|
|
845
|
+
Parameters
|
|
846
|
+
----------
|
|
847
|
+
name : str
|
|
848
|
+
Field name.
|
|
849
|
+
|
|
850
|
+
Returns
|
|
851
|
+
-------
|
|
852
|
+
ndarray
|
|
853
|
+
Field values at vertices.
|
|
854
|
+
|
|
855
|
+
Raises
|
|
856
|
+
------
|
|
857
|
+
KeyError
|
|
858
|
+
If the field does not exist.
|
|
859
|
+
|
|
860
|
+
"""
|
|
861
|
+
if name not in self._user_fields:
|
|
862
|
+
msg = f"User field '{name}' not found. Available: {list(self._user_fields)}"
|
|
863
|
+
raise KeyError(msg)
|
|
864
|
+
return self._user_fields[name]
|
|
865
|
+
|
|
866
|
+
def get_user_fields(self) -> dict[str, NDArray[np.float64]]:
|
|
867
|
+
"""Get all user-defined fields.
|
|
868
|
+
|
|
869
|
+
Returns
|
|
870
|
+
-------
|
|
871
|
+
dict[str, ndarray]
|
|
872
|
+
Dictionary mapping field names to values.
|
|
873
|
+
|
|
874
|
+
"""
|
|
875
|
+
return dict(self._user_fields)
|
|
876
|
+
|
|
877
|
+
def clear_user_fields(self) -> None:
|
|
878
|
+
"""Remove all user-defined fields."""
|
|
879
|
+
self._user_fields.clear()
|
|
880
|
+
|
|
881
|
+
def has_user_field(self, name: str) -> bool:
|
|
882
|
+
"""Check if a user field exists.
|
|
883
|
+
|
|
884
|
+
Parameters
|
|
885
|
+
----------
|
|
886
|
+
name : str
|
|
887
|
+
Field name.
|
|
888
|
+
|
|
889
|
+
Returns
|
|
890
|
+
-------
|
|
891
|
+
bool
|
|
892
|
+
True if the field exists.
|
|
893
|
+
|
|
894
|
+
"""
|
|
895
|
+
return name in self._user_fields
|
|
896
|
+
|
|
897
|
+
# =========================================================================
|
|
898
|
+
# Geometry operations
|
|
899
|
+
# =========================================================================
|
|
900
|
+
|
|
901
|
+
def _compute_tetrahedra_volumes(
|
|
902
|
+
self,
|
|
903
|
+
vertices: NDArray[np.float64],
|
|
904
|
+
tetrahedra: NDArray[np.int32],
|
|
905
|
+
) -> NDArray[np.float64]:
|
|
906
|
+
"""Compute volumes for an array of tetrahedra.
|
|
907
|
+
|
|
908
|
+
Parameters
|
|
909
|
+
----------
|
|
910
|
+
vertices : ndarray
|
|
911
|
+
Vertex coordinates, shape (N, 3).
|
|
912
|
+
tetrahedra : ndarray
|
|
913
|
+
Tetrahedra connectivity, shape (M, 4).
|
|
914
|
+
|
|
915
|
+
Returns
|
|
916
|
+
-------
|
|
917
|
+
ndarray
|
|
918
|
+
Volume of each tetrahedron, shape (M,).
|
|
919
|
+
|
|
920
|
+
"""
|
|
921
|
+
v0 = vertices[tetrahedra[:, 0]]
|
|
922
|
+
v1 = vertices[tetrahedra[:, 1]]
|
|
923
|
+
v2 = vertices[tetrahedra[:, 2]]
|
|
924
|
+
v3 = vertices[tetrahedra[:, 3]]
|
|
925
|
+
return np.abs(np.einsum("ij,ij->i", v1 - v0, np.cross(v2 - v0, v3 - v0))) / 6
|
|
926
|
+
|
|
927
|
+
def _compute_triangle_areas(
|
|
928
|
+
self,
|
|
929
|
+
vertices: NDArray[np.float64],
|
|
930
|
+
triangles: NDArray[np.int32],
|
|
931
|
+
) -> NDArray[np.float64]:
|
|
932
|
+
"""Compute areas for an array of triangles.
|
|
933
|
+
|
|
934
|
+
Automatically handles 2D (shoelace formula) and 3D (cross product) cases.
|
|
935
|
+
|
|
936
|
+
Parameters
|
|
937
|
+
----------
|
|
938
|
+
vertices : ndarray
|
|
939
|
+
Vertex coordinates, shape (N, 2) or (N, 3).
|
|
940
|
+
triangles : ndarray
|
|
941
|
+
Triangle connectivity, shape (M, 3).
|
|
942
|
+
|
|
943
|
+
Returns
|
|
944
|
+
-------
|
|
945
|
+
ndarray
|
|
946
|
+
Area of each triangle, shape (M,).
|
|
947
|
+
|
|
948
|
+
"""
|
|
949
|
+
v0 = vertices[triangles[:, 0]]
|
|
950
|
+
v1 = vertices[triangles[:, 1]]
|
|
951
|
+
v2 = vertices[triangles[:, 2]]
|
|
952
|
+
|
|
953
|
+
if self._kind == MeshKind.TRIANGULAR_2D:
|
|
954
|
+
# 2D: use shoelace formula
|
|
955
|
+
return 0.5 * np.abs(
|
|
956
|
+
(v1[:, 0] - v0[:, 0]) * (v2[:, 1] - v0[:, 1])
|
|
957
|
+
- (v2[:, 0] - v0[:, 0]) * (v1[:, 1] - v0[:, 1]),
|
|
958
|
+
)
|
|
959
|
+
# 3D: use cross product magnitude
|
|
960
|
+
return 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0), axis=1)
|
|
961
|
+
|
|
962
|
+
def get_bounds(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
963
|
+
"""Get the bounding box of the mesh.
|
|
964
|
+
|
|
965
|
+
Returns
|
|
966
|
+
-------
|
|
967
|
+
tuple[ndarray, ndarray]
|
|
968
|
+
Tuple of (min_coords, max_coords), each shape (3,) for 3D
|
|
969
|
+
or (2,) for 2D meshes.
|
|
970
|
+
|
|
971
|
+
Raises
|
|
972
|
+
------
|
|
973
|
+
ValueError
|
|
974
|
+
If the mesh has no vertices.
|
|
975
|
+
|
|
976
|
+
Examples
|
|
977
|
+
--------
|
|
978
|
+
>>> mesh = Mesh(vertices, cells)
|
|
979
|
+
>>> min_pt, max_pt = mesh.get_bounds()
|
|
980
|
+
>>> print(f"Size: {max_pt - min_pt}")
|
|
981
|
+
|
|
982
|
+
"""
|
|
983
|
+
vertices = self.get_vertices()
|
|
984
|
+
if len(vertices) == 0:
|
|
985
|
+
msg = "Cannot compute bounds for mesh with no vertices"
|
|
986
|
+
raise ValueError(msg)
|
|
987
|
+
return vertices.min(axis=0), vertices.max(axis=0)
|
|
988
|
+
|
|
989
|
+
def get_center_of_mass(self) -> NDArray[np.float64]:
|
|
990
|
+
"""Get the centroid (center of mass) of the mesh.
|
|
991
|
+
|
|
992
|
+
For volume meshes, computes volume-weighted centroid.
|
|
993
|
+
For surface meshes, computes area-weighted centroid.
|
|
994
|
+
For 2D meshes, computes area-weighted centroid.
|
|
995
|
+
|
|
996
|
+
Returns
|
|
997
|
+
-------
|
|
998
|
+
ndarray
|
|
999
|
+
Centroid coordinates, shape (3,) for 3D or (2,) for 2D.
|
|
1000
|
+
|
|
1001
|
+
Examples
|
|
1002
|
+
--------
|
|
1003
|
+
>>> mesh = Mesh(vertices, cells)
|
|
1004
|
+
>>> center = mesh.get_center_of_mass()
|
|
1005
|
+
>>> print(f"Center: {center}")
|
|
1006
|
+
|
|
1007
|
+
"""
|
|
1008
|
+
vertices = self.get_vertices()
|
|
1009
|
+
|
|
1010
|
+
if self._kind == MeshKind.TETRAHEDRAL:
|
|
1011
|
+
tetrahedra = self.get_tetrahedra()
|
|
1012
|
+
if len(tetrahedra) == 0:
|
|
1013
|
+
return vertices.mean(axis=0)
|
|
1014
|
+
|
|
1015
|
+
volumes = self._compute_tetrahedra_volumes(vertices, tetrahedra)
|
|
1016
|
+
v0 = vertices[tetrahedra[:, 0]]
|
|
1017
|
+
v1 = vertices[tetrahedra[:, 1]]
|
|
1018
|
+
v2 = vertices[tetrahedra[:, 2]]
|
|
1019
|
+
v3 = vertices[tetrahedra[:, 3]]
|
|
1020
|
+
centroids = (v0 + v1 + v2 + v3) / 4
|
|
1021
|
+
total_volume = volumes.sum()
|
|
1022
|
+
|
|
1023
|
+
if total_volume < np.finfo(np.float64).tiny:
|
|
1024
|
+
return vertices.mean(axis=0)
|
|
1025
|
+
|
|
1026
|
+
return (centroids * volumes[:, np.newaxis]).sum(axis=0) / total_volume
|
|
1027
|
+
|
|
1028
|
+
triangles = self.get_triangles()
|
|
1029
|
+
if len(triangles) == 0:
|
|
1030
|
+
return vertices.mean(axis=0)
|
|
1031
|
+
|
|
1032
|
+
areas = self._compute_triangle_areas(vertices, triangles)
|
|
1033
|
+
v0 = vertices[triangles[:, 0]]
|
|
1034
|
+
v1 = vertices[triangles[:, 1]]
|
|
1035
|
+
v2 = vertices[triangles[:, 2]]
|
|
1036
|
+
centroids = (v0 + v1 + v2) / 3
|
|
1037
|
+
total_area = areas.sum()
|
|
1038
|
+
|
|
1039
|
+
if total_area < np.finfo(np.float64).tiny:
|
|
1040
|
+
return vertices.mean(axis=0)
|
|
1041
|
+
|
|
1042
|
+
return (centroids * areas[:, np.newaxis]).sum(axis=0) / total_area
|
|
1043
|
+
|
|
1044
|
+
def compute_volume(self) -> float:
|
|
1045
|
+
"""Compute the total volume of the mesh.
|
|
1046
|
+
|
|
1047
|
+
Only available for 3D volume meshes (TETRAHEDRAL).
|
|
1048
|
+
|
|
1049
|
+
Returns
|
|
1050
|
+
-------
|
|
1051
|
+
float
|
|
1052
|
+
Total volume in mesh units cubed.
|
|
1053
|
+
|
|
1054
|
+
Raises
|
|
1055
|
+
------
|
|
1056
|
+
TypeError
|
|
1057
|
+
If mesh is not a 3D volume mesh.
|
|
1058
|
+
|
|
1059
|
+
Examples
|
|
1060
|
+
--------
|
|
1061
|
+
>>> mesh = Mesh(vertices, cells)
|
|
1062
|
+
>>> volume = mesh.compute_volume()
|
|
1063
|
+
>>> print(f"Volume: {volume:.2f} mm^3")
|
|
1064
|
+
|
|
1065
|
+
"""
|
|
1066
|
+
if self._kind != MeshKind.TETRAHEDRAL:
|
|
1067
|
+
msg = "compute_volume() is only available for TETRAHEDRAL meshes"
|
|
1068
|
+
raise TypeError(msg)
|
|
1069
|
+
|
|
1070
|
+
vertices = self.get_vertices()
|
|
1071
|
+
tetrahedra = self.get_tetrahedra()
|
|
1072
|
+
|
|
1073
|
+
if len(tetrahedra) == 0:
|
|
1074
|
+
return 0.0
|
|
1075
|
+
|
|
1076
|
+
volumes = self._compute_tetrahedra_volumes(vertices, tetrahedra)
|
|
1077
|
+
return float(volumes.sum())
|
|
1078
|
+
|
|
1079
|
+
def compute_surface_area(self) -> float:
|
|
1080
|
+
"""Compute the total surface area of the mesh.
|
|
1081
|
+
|
|
1082
|
+
For volume meshes (TETRAHEDRAL), computes boundary surface area
|
|
1083
|
+
(triangles stored in the mesh represent boundary faces).
|
|
1084
|
+
For surface meshes (TRIANGULAR_SURFACE), computes total area.
|
|
1085
|
+
For 2D meshes (TRIANGULAR_2D), computes total area.
|
|
1086
|
+
|
|
1087
|
+
Returns
|
|
1088
|
+
-------
|
|
1089
|
+
float
|
|
1090
|
+
Total surface area in mesh units squared.
|
|
1091
|
+
|
|
1092
|
+
Examples
|
|
1093
|
+
--------
|
|
1094
|
+
>>> mesh = Mesh(vertices, cells)
|
|
1095
|
+
>>> area = mesh.compute_surface_area()
|
|
1096
|
+
>>> print(f"Surface area: {area:.2f} mm^2")
|
|
1097
|
+
|
|
1098
|
+
"""
|
|
1099
|
+
vertices = self.get_vertices()
|
|
1100
|
+
triangles = self.get_triangles()
|
|
1101
|
+
|
|
1102
|
+
if len(triangles) == 0:
|
|
1103
|
+
return 0.0
|
|
1104
|
+
|
|
1105
|
+
areas = self._compute_triangle_areas(vertices, triangles)
|
|
1106
|
+
return float(areas.sum())
|
|
1107
|
+
|
|
1108
|
+
def get_diagonal(self) -> float:
|
|
1109
|
+
"""Get the diagonal length of the bounding box.
|
|
1110
|
+
|
|
1111
|
+
Returns
|
|
1112
|
+
-------
|
|
1113
|
+
float
|
|
1114
|
+
The diagonal length of the bounding box.
|
|
1115
|
+
|
|
1116
|
+
Raises
|
|
1117
|
+
------
|
|
1118
|
+
ValueError
|
|
1119
|
+
If the mesh has no vertices.
|
|
1120
|
+
|
|
1121
|
+
Examples
|
|
1122
|
+
--------
|
|
1123
|
+
>>> mesh = Mesh(vertices, cells)
|
|
1124
|
+
>>> diagonal = mesh.get_diagonal()
|
|
1125
|
+
>>> print(f"Bounding box diagonal: {diagonal:.2f}")
|
|
1126
|
+
|
|
1127
|
+
"""
|
|
1128
|
+
min_pt, max_pt = self.get_bounds()
|
|
1129
|
+
return float(np.linalg.norm(max_pt - min_pt))
|
|
1130
|
+
|
|
1131
|
+
# =========================================================================
|
|
1132
|
+
# Topology queries
|
|
1133
|
+
# =========================================================================
|
|
1134
|
+
|
|
1135
|
+
def get_adjacent_elements(self, idx: int) -> NDArray[np.int32]:
|
|
1136
|
+
"""Get indices of elements adjacent to a given element.
|
|
1137
|
+
|
|
1138
|
+
Parameters
|
|
1139
|
+
----------
|
|
1140
|
+
idx : int
|
|
1141
|
+
Element index (1-based for MMG).
|
|
1142
|
+
|
|
1143
|
+
Returns
|
|
1144
|
+
-------
|
|
1145
|
+
ndarray
|
|
1146
|
+
Indices of adjacent elements.
|
|
1147
|
+
|
|
1148
|
+
"""
|
|
1149
|
+
return self._impl.get_adjacent_elements(idx)
|
|
1150
|
+
|
|
1151
|
+
def get_vertex_neighbors(self, idx: int) -> NDArray[np.int32]:
|
|
1152
|
+
"""Get indices of vertices connected to a given vertex.
|
|
1153
|
+
|
|
1154
|
+
Parameters
|
|
1155
|
+
----------
|
|
1156
|
+
idx : int
|
|
1157
|
+
Vertex index (1-based for MMG).
|
|
1158
|
+
|
|
1159
|
+
Returns
|
|
1160
|
+
-------
|
|
1161
|
+
ndarray
|
|
1162
|
+
Indices of neighboring vertices.
|
|
1163
|
+
|
|
1164
|
+
"""
|
|
1165
|
+
return self._impl.get_vertex_neighbors(idx)
|
|
1166
|
+
|
|
1167
|
+
def get_element_quality(self, idx: int) -> float:
|
|
1168
|
+
"""Get quality metric for a single element.
|
|
1169
|
+
|
|
1170
|
+
Parameters
|
|
1171
|
+
----------
|
|
1172
|
+
idx : int
|
|
1173
|
+
Element index (1-based for MMG).
|
|
1174
|
+
|
|
1175
|
+
Returns
|
|
1176
|
+
-------
|
|
1177
|
+
float
|
|
1178
|
+
Quality metric (0-1, higher is better).
|
|
1179
|
+
|
|
1180
|
+
"""
|
|
1181
|
+
return self._impl.get_element_quality(idx)
|
|
1182
|
+
|
|
1183
|
+
def get_element_qualities(self) -> NDArray[np.float64]:
|
|
1184
|
+
"""Get quality metrics for all elements.
|
|
1185
|
+
|
|
1186
|
+
Returns
|
|
1187
|
+
-------
|
|
1188
|
+
ndarray
|
|
1189
|
+
Quality metrics for all elements.
|
|
1190
|
+
|
|
1191
|
+
"""
|
|
1192
|
+
return self._impl.get_element_qualities()
|
|
1193
|
+
|
|
1194
|
+
# =========================================================================
|
|
1195
|
+
# File I/O
|
|
1196
|
+
# =========================================================================
|
|
1197
|
+
|
|
1198
|
+
def save(self, filename: str | Path) -> None:
|
|
1199
|
+
"""Save mesh to file.
|
|
1200
|
+
|
|
1201
|
+
Parameters
|
|
1202
|
+
----------
|
|
1203
|
+
filename : str or Path
|
|
1204
|
+
Output file path. Format determined by extension.
|
|
1205
|
+
|
|
1206
|
+
"""
|
|
1207
|
+
self._impl.save(filename)
|
|
1208
|
+
|
|
1209
|
+
# =========================================================================
|
|
1210
|
+
# Remeshing operations
|
|
1211
|
+
# =========================================================================
|
|
1212
|
+
|
|
1213
|
+
def _prepare_field_transfer(
|
|
1214
|
+
self,
|
|
1215
|
+
transfer_fields: FieldTransferParam,
|
|
1216
|
+
) -> tuple[
|
|
1217
|
+
dict[str, NDArray[np.float64]],
|
|
1218
|
+
NDArray[np.float64] | None,
|
|
1219
|
+
NDArray[np.int32] | None,
|
|
1220
|
+
]:
|
|
1221
|
+
"""Prepare for field transfer before remeshing.
|
|
1222
|
+
|
|
1223
|
+
Returns fields to transfer, old vertices, and old elements.
|
|
1224
|
+
"""
|
|
1225
|
+
fields_to_transfer: dict[str, NDArray[np.float64]] = {}
|
|
1226
|
+
if not transfer_fields:
|
|
1227
|
+
return fields_to_transfer, None, None
|
|
1228
|
+
|
|
1229
|
+
if transfer_fields is True:
|
|
1230
|
+
fields_to_transfer = dict(self._user_fields)
|
|
1231
|
+
else:
|
|
1232
|
+
for name in transfer_fields:
|
|
1233
|
+
if name in self._user_fields:
|
|
1234
|
+
fields_to_transfer[name] = self._user_fields[name]
|
|
1235
|
+
else:
|
|
1236
|
+
msg = f"User field '{name}' not found for transfer"
|
|
1237
|
+
raise KeyError(msg)
|
|
1238
|
+
|
|
1239
|
+
if not fields_to_transfer:
|
|
1240
|
+
return fields_to_transfer, None, None
|
|
1241
|
+
|
|
1242
|
+
old_vertices = self._impl.get_vertices().copy()
|
|
1243
|
+
if self._kind == MeshKind.TETRAHEDRAL:
|
|
1244
|
+
impl_3d = cast("MmgMesh3D", self._impl)
|
|
1245
|
+
old_elements = impl_3d.get_tetrahedra().copy()
|
|
1246
|
+
else:
|
|
1247
|
+
old_elements = self._impl.get_triangles().copy()
|
|
1248
|
+
|
|
1249
|
+
return fields_to_transfer, old_vertices, old_elements
|
|
1250
|
+
|
|
1251
|
+
def _execute_field_transfer(
|
|
1252
|
+
self,
|
|
1253
|
+
fields_to_transfer: dict[str, NDArray[np.float64]],
|
|
1254
|
+
old_vertices: NDArray[np.float64],
|
|
1255
|
+
old_elements: NDArray[np.int32],
|
|
1256
|
+
interpolation: str,
|
|
1257
|
+
) -> None:
|
|
1258
|
+
"""Execute field transfer after remeshing."""
|
|
1259
|
+
from mmgpy._transfer import transfer_fields as _transfer # noqa: PLC0415
|
|
1260
|
+
|
|
1261
|
+
new_vertices = self._impl.get_vertices()
|
|
1262
|
+
self._user_fields = _transfer(
|
|
1263
|
+
source_vertices=old_vertices,
|
|
1264
|
+
source_elements=old_elements,
|
|
1265
|
+
target_points=new_vertices,
|
|
1266
|
+
fields=fields_to_transfer,
|
|
1267
|
+
method=interpolation,
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
def remesh( # noqa: C901, PLR0912
|
|
1271
|
+
self,
|
|
1272
|
+
options: Mmg3DOptions | Mmg2DOptions | MmgSOptions | None = None,
|
|
1273
|
+
*,
|
|
1274
|
+
progress: ProgressParam = True,
|
|
1275
|
+
transfer_fields: FieldTransferParam = False,
|
|
1276
|
+
interpolation: str = "linear",
|
|
1277
|
+
**kwargs: Any, # noqa: ANN401
|
|
1278
|
+
) -> RemeshResult:
|
|
1279
|
+
"""Remesh the mesh in-place.
|
|
1280
|
+
|
|
1281
|
+
Parameters
|
|
1282
|
+
----------
|
|
1283
|
+
options : Mmg3DOptions | Mmg2DOptions | MmgSOptions, optional
|
|
1284
|
+
Options object for remeshing parameters.
|
|
1285
|
+
progress : bool | Callable[[ProgressEvent], bool] | None, default=True
|
|
1286
|
+
Progress reporting option:
|
|
1287
|
+
- True: Show Rich progress bar (default)
|
|
1288
|
+
- False or None: No progress reporting
|
|
1289
|
+
- Callable: Custom callback that receives ProgressEvent and returns
|
|
1290
|
+
True to continue or False to cancel
|
|
1291
|
+
transfer_fields : bool | Sequence[str] | None, default=False
|
|
1292
|
+
Transfer user-defined fields to the new mesh via interpolation:
|
|
1293
|
+
- False or None: No field transfer (default), clears existing user fields
|
|
1294
|
+
- True: Transfer all user fields
|
|
1295
|
+
- List of field names: Transfer only specified fields
|
|
1296
|
+
interpolation : str, default="linear"
|
|
1297
|
+
Interpolation method for field transfer:
|
|
1298
|
+
- "linear": Barycentric interpolation (recommended)
|
|
1299
|
+
- "nearest": Nearest vertex value
|
|
1300
|
+
**kwargs : float
|
|
1301
|
+
Individual remeshing parameters (hmin, hmax, hsiz, hausd, etc.).
|
|
1302
|
+
|
|
1303
|
+
Notes
|
|
1304
|
+
-----
|
|
1305
|
+
Memory: When ``transfer_fields`` is enabled, the original mesh vertices
|
|
1306
|
+
and elements are copied before remeshing, temporarily doubling memory
|
|
1307
|
+
usage for large meshes.
|
|
1308
|
+
|
|
1309
|
+
Surface meshes (TRIANGULAR_SURFACE): Field transfer uses 3D Delaunay
|
|
1310
|
+
triangulation for point location, which may not work well for nearly
|
|
1311
|
+
planar surface meshes. Consider using volumetric meshes for field transfer.
|
|
1312
|
+
|
|
1313
|
+
Returns
|
|
1314
|
+
-------
|
|
1315
|
+
RemeshResult
|
|
1316
|
+
Statistics from the remeshing operation.
|
|
1317
|
+
|
|
1318
|
+
Raises
|
|
1319
|
+
------
|
|
1320
|
+
CancellationError
|
|
1321
|
+
If the progress callback returns False to cancel the operation.
|
|
1322
|
+
|
|
1323
|
+
Examples
|
|
1324
|
+
--------
|
|
1325
|
+
>>> mesh.remesh(hmax=0.1) # Shows progress bar by default
|
|
1326
|
+
|
|
1327
|
+
>>> mesh.remesh(hmax=0.1, progress=False) # No progress bar
|
|
1328
|
+
|
|
1329
|
+
>>> # Transfer fields during remeshing
|
|
1330
|
+
>>> mesh.set_user_field("temperature", temperature_array)
|
|
1331
|
+
>>> mesh.remesh(hmax=0.1, transfer_fields=True)
|
|
1332
|
+
>>> new_temp = mesh.get_user_field("temperature")
|
|
1333
|
+
|
|
1334
|
+
>>> # Transfer specific fields
|
|
1335
|
+
>>> mesh.remesh(hmax=0.1, transfer_fields=["temperature", "velocity"])
|
|
1336
|
+
|
|
1337
|
+
>>> def my_callback(event):
|
|
1338
|
+
... print(f"{event.phase}: {event.message}")
|
|
1339
|
+
... return True # Continue
|
|
1340
|
+
>>> mesh.remesh(hmax=0.1, progress=my_callback)
|
|
1341
|
+
|
|
1342
|
+
"""
|
|
1343
|
+
from mmgpy._options import ( # noqa: PLC0415
|
|
1344
|
+
Mmg2DOptions,
|
|
1345
|
+
Mmg3DOptions,
|
|
1346
|
+
MmgSOptions,
|
|
1347
|
+
)
|
|
1348
|
+
from mmgpy._progress import CancellationError, _emit_event # noqa: PLC0415
|
|
1349
|
+
|
|
1350
|
+
# Validate interpolation method
|
|
1351
|
+
valid_methods = ("linear", "nearest")
|
|
1352
|
+
if interpolation not in valid_methods:
|
|
1353
|
+
msg = (
|
|
1354
|
+
f"Invalid interpolation method: {interpolation!r}. "
|
|
1355
|
+
f"Must be one of {valid_methods}"
|
|
1356
|
+
)
|
|
1357
|
+
raise ValueError(msg)
|
|
1358
|
+
|
|
1359
|
+
# Validate and convert options object
|
|
1360
|
+
if options is not None:
|
|
1361
|
+
if kwargs:
|
|
1362
|
+
msg = (
|
|
1363
|
+
"Cannot pass both options object and keyword arguments. "
|
|
1364
|
+
"Use one or the other."
|
|
1365
|
+
)
|
|
1366
|
+
raise TypeError(msg)
|
|
1367
|
+
|
|
1368
|
+
# Validate options type matches mesh type
|
|
1369
|
+
options_type_map = {
|
|
1370
|
+
MeshKind.TETRAHEDRAL: Mmg3DOptions,
|
|
1371
|
+
MeshKind.TRIANGULAR_2D: Mmg2DOptions,
|
|
1372
|
+
MeshKind.TRIANGULAR_SURFACE: MmgSOptions,
|
|
1373
|
+
}
|
|
1374
|
+
expected_type = options_type_map[self._kind]
|
|
1375
|
+
if not isinstance(options, expected_type):
|
|
1376
|
+
msg = (
|
|
1377
|
+
f"Expected {expected_type.__name__} for {self._kind.value} mesh, "
|
|
1378
|
+
f"got {type(options).__name__}"
|
|
1379
|
+
)
|
|
1380
|
+
raise TypeError(msg)
|
|
1381
|
+
kwargs = options.to_dict()
|
|
1382
|
+
|
|
1383
|
+
# Apply sizing constraints before remeshing
|
|
1384
|
+
if self._sizing_constraints:
|
|
1385
|
+
self._apply_sizing_to_metric()
|
|
1386
|
+
|
|
1387
|
+
# Prepare field transfer
|
|
1388
|
+
fields_to_transfer, old_vertices, old_elements = self._prepare_field_transfer(
|
|
1389
|
+
transfer_fields,
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
# Resolve progress callback
|
|
1393
|
+
callback, reporter_ctx = _resolve_progress_callback(progress)
|
|
1394
|
+
if reporter_ctx is not None: # pragma: no cover
|
|
1395
|
+
reporter_ctx.__enter__()
|
|
1396
|
+
|
|
1397
|
+
try:
|
|
1398
|
+
# Emit progress events
|
|
1399
|
+
if not _emit_event(callback, "init", "start", "Initializing", progress=0.0):
|
|
1400
|
+
raise CancellationError.for_phase("init") # noqa: EM101
|
|
1401
|
+
|
|
1402
|
+
initial_vertices = len(self._impl.get_vertices())
|
|
1403
|
+
|
|
1404
|
+
if not _emit_event(callback, "options", "start", "Options", progress=0.0):
|
|
1405
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
1406
|
+
|
|
1407
|
+
_emit_event(callback, "options", "complete", "Options set", progress=1.0)
|
|
1408
|
+
|
|
1409
|
+
if not _emit_event(callback, "remesh", "start", "Remeshing", progress=0.0):
|
|
1410
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
1411
|
+
|
|
1412
|
+
# Call raw C++ method and convert result
|
|
1413
|
+
stats = self._impl.remesh(**kwargs) # type: ignore[arg-type]
|
|
1414
|
+
final_vertices = len(self._impl.get_vertices())
|
|
1415
|
+
|
|
1416
|
+
# Transfer fields to new mesh if captured, otherwise clear stale fields
|
|
1417
|
+
if (
|
|
1418
|
+
fields_to_transfer
|
|
1419
|
+
and old_vertices is not None
|
|
1420
|
+
and old_elements is not None
|
|
1421
|
+
):
|
|
1422
|
+
self._execute_field_transfer(
|
|
1423
|
+
fields_to_transfer,
|
|
1424
|
+
old_vertices,
|
|
1425
|
+
old_elements,
|
|
1426
|
+
interpolation,
|
|
1427
|
+
)
|
|
1428
|
+
else:
|
|
1429
|
+
# Clear user fields as they have incorrect vertex count after remeshing
|
|
1430
|
+
self._user_fields.clear()
|
|
1431
|
+
|
|
1432
|
+
_emit_event(
|
|
1433
|
+
callback,
|
|
1434
|
+
"remesh",
|
|
1435
|
+
"complete",
|
|
1436
|
+
"Remeshing complete",
|
|
1437
|
+
progress=1.0,
|
|
1438
|
+
details={
|
|
1439
|
+
"initial_vertices": initial_vertices,
|
|
1440
|
+
"final_vertices": final_vertices,
|
|
1441
|
+
"vertex_change": final_vertices - initial_vertices,
|
|
1442
|
+
},
|
|
1443
|
+
)
|
|
1444
|
+
return _dict_to_remesh_result(stats)
|
|
1445
|
+
finally:
|
|
1446
|
+
if reporter_ctx is not None: # pragma: no cover
|
|
1447
|
+
reporter_ctx.__exit__(None, None, None)
|
|
1448
|
+
|
|
1449
|
+
def remesh_lagrangian(
|
|
1450
|
+
self,
|
|
1451
|
+
displacement: NDArray[np.float64],
|
|
1452
|
+
*,
|
|
1453
|
+
progress: ProgressParam = True,
|
|
1454
|
+
**kwargs: Any, # noqa: ANN401
|
|
1455
|
+
) -> RemeshResult:
|
|
1456
|
+
"""Remesh with Lagrangian motion.
|
|
1457
|
+
|
|
1458
|
+
Only available for TETRAHEDRAL and TRIANGULAR_2D meshes.
|
|
1459
|
+
|
|
1460
|
+
Parameters
|
|
1461
|
+
----------
|
|
1462
|
+
displacement : ndarray
|
|
1463
|
+
Displacement field for each vertex.
|
|
1464
|
+
progress : bool | Callable[[ProgressEvent], bool] | None, default=True
|
|
1465
|
+
Progress reporting option:
|
|
1466
|
+
- True: Show Rich progress bar (default)
|
|
1467
|
+
- False or None: No progress reporting
|
|
1468
|
+
- Callable: Custom callback that receives ProgressEvent and returns
|
|
1469
|
+
True to continue or False to cancel
|
|
1470
|
+
**kwargs : float
|
|
1471
|
+
Additional remeshing parameters.
|
|
1472
|
+
|
|
1473
|
+
Returns
|
|
1474
|
+
-------
|
|
1475
|
+
RemeshResult
|
|
1476
|
+
Statistics from the remeshing operation.
|
|
1477
|
+
|
|
1478
|
+
Raises
|
|
1479
|
+
------
|
|
1480
|
+
TypeError
|
|
1481
|
+
If mesh is TRIANGULAR_SURFACE.
|
|
1482
|
+
CancellationError
|
|
1483
|
+
If the progress callback returns False to cancel the operation.
|
|
1484
|
+
|
|
1485
|
+
"""
|
|
1486
|
+
from mmgpy._progress import CancellationError, _emit_event # noqa: PLC0415
|
|
1487
|
+
|
|
1488
|
+
if self._kind == MeshKind.TRIANGULAR_SURFACE:
|
|
1489
|
+
msg = "remesh_lagrangian() is not available for TRIANGULAR_SURFACE meshes"
|
|
1490
|
+
raise TypeError(msg)
|
|
1491
|
+
|
|
1492
|
+
callback, reporter_ctx = _resolve_progress_callback(progress)
|
|
1493
|
+
if reporter_ctx is not None: # pragma: no cover
|
|
1494
|
+
reporter_ctx.__enter__()
|
|
1495
|
+
|
|
1496
|
+
try:
|
|
1497
|
+
if not _emit_event(callback, "init", "start", "Initializing", progress=0.0):
|
|
1498
|
+
raise CancellationError.for_phase("init") # noqa: EM101
|
|
1499
|
+
|
|
1500
|
+
initial_vertices = len(self._impl.get_vertices())
|
|
1501
|
+
|
|
1502
|
+
if not _emit_event(
|
|
1503
|
+
callback,
|
|
1504
|
+
"options",
|
|
1505
|
+
"start",
|
|
1506
|
+
"Displacement",
|
|
1507
|
+
progress=0.0,
|
|
1508
|
+
):
|
|
1509
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
1510
|
+
|
|
1511
|
+
_emit_event(
|
|
1512
|
+
callback,
|
|
1513
|
+
"options",
|
|
1514
|
+
"complete",
|
|
1515
|
+
"Displacement set",
|
|
1516
|
+
progress=1.0,
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
if not _emit_event(
|
|
1520
|
+
callback,
|
|
1521
|
+
"remesh",
|
|
1522
|
+
"start",
|
|
1523
|
+
"Lagrangian remeshing",
|
|
1524
|
+
progress=0.0,
|
|
1525
|
+
):
|
|
1526
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
1527
|
+
|
|
1528
|
+
impl = cast("MmgMesh3D | MmgMesh2D", self._impl)
|
|
1529
|
+
stats = impl.remesh_lagrangian(displacement, **kwargs) # type: ignore[arg-type]
|
|
1530
|
+
final_vertices = len(self._impl.get_vertices())
|
|
1531
|
+
|
|
1532
|
+
_emit_event(
|
|
1533
|
+
callback,
|
|
1534
|
+
"remesh",
|
|
1535
|
+
"complete",
|
|
1536
|
+
"Lagrangian complete",
|
|
1537
|
+
progress=1.0,
|
|
1538
|
+
details={
|
|
1539
|
+
"initial_vertices": initial_vertices,
|
|
1540
|
+
"final_vertices": final_vertices,
|
|
1541
|
+
"vertex_change": final_vertices - initial_vertices,
|
|
1542
|
+
},
|
|
1543
|
+
)
|
|
1544
|
+
return _dict_to_remesh_result(stats)
|
|
1545
|
+
finally:
|
|
1546
|
+
if reporter_ctx is not None: # pragma: no cover
|
|
1547
|
+
reporter_ctx.__exit__(None, None, None)
|
|
1548
|
+
|
|
1549
|
+
def remesh_levelset(
|
|
1550
|
+
self,
|
|
1551
|
+
levelset: NDArray[np.float64],
|
|
1552
|
+
*,
|
|
1553
|
+
progress: ProgressParam = True,
|
|
1554
|
+
**kwargs: Any, # noqa: ANN401
|
|
1555
|
+
) -> RemeshResult:
|
|
1556
|
+
"""Remesh with level-set discretization.
|
|
1557
|
+
|
|
1558
|
+
Parameters
|
|
1559
|
+
----------
|
|
1560
|
+
levelset : ndarray
|
|
1561
|
+
Level-set field for each vertex.
|
|
1562
|
+
progress : bool | Callable[[ProgressEvent], bool] | None, default=True
|
|
1563
|
+
Progress reporting option:
|
|
1564
|
+
- True: Show Rich progress bar (default)
|
|
1565
|
+
- False or None: No progress reporting
|
|
1566
|
+
- Callable: Custom callback that receives ProgressEvent and returns
|
|
1567
|
+
True to continue or False to cancel
|
|
1568
|
+
**kwargs : float
|
|
1569
|
+
Additional remeshing parameters.
|
|
1570
|
+
|
|
1571
|
+
Returns
|
|
1572
|
+
-------
|
|
1573
|
+
RemeshResult
|
|
1574
|
+
Statistics from the remeshing operation.
|
|
1575
|
+
|
|
1576
|
+
Raises
|
|
1577
|
+
------
|
|
1578
|
+
CancellationError
|
|
1579
|
+
If the progress callback returns False to cancel the operation.
|
|
1580
|
+
|
|
1581
|
+
"""
|
|
1582
|
+
from mmgpy._progress import CancellationError, _emit_event # noqa: PLC0415
|
|
1583
|
+
|
|
1584
|
+
callback, reporter_ctx = _resolve_progress_callback(progress)
|
|
1585
|
+
if reporter_ctx is not None: # pragma: no cover
|
|
1586
|
+
reporter_ctx.__enter__()
|
|
1587
|
+
|
|
1588
|
+
try:
|
|
1589
|
+
if not _emit_event(callback, "init", "start", "Initializing", progress=0.0):
|
|
1590
|
+
raise CancellationError.for_phase("init") # noqa: EM101
|
|
1591
|
+
|
|
1592
|
+
initial_vertices = len(self._impl.get_vertices())
|
|
1593
|
+
|
|
1594
|
+
if not _emit_event(callback, "options", "start", "Level-set", progress=0.0):
|
|
1595
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
1596
|
+
|
|
1597
|
+
_emit_event(callback, "options", "complete", "Level-set set", progress=1.0)
|
|
1598
|
+
|
|
1599
|
+
if not _emit_event(
|
|
1600
|
+
callback,
|
|
1601
|
+
"remesh",
|
|
1602
|
+
"start",
|
|
1603
|
+
"Level-set remeshing",
|
|
1604
|
+
progress=0.0,
|
|
1605
|
+
):
|
|
1606
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
1607
|
+
|
|
1608
|
+
stats = self._impl.remesh_levelset(levelset, **kwargs) # type: ignore[arg-type]
|
|
1609
|
+
final_vertices = len(self._impl.get_vertices())
|
|
1610
|
+
|
|
1611
|
+
_emit_event(
|
|
1612
|
+
callback,
|
|
1613
|
+
"remesh",
|
|
1614
|
+
"complete",
|
|
1615
|
+
"Level-set complete",
|
|
1616
|
+
progress=1.0,
|
|
1617
|
+
details={
|
|
1618
|
+
"initial_vertices": initial_vertices,
|
|
1619
|
+
"final_vertices": final_vertices,
|
|
1620
|
+
"vertex_change": final_vertices - initial_vertices,
|
|
1621
|
+
},
|
|
1622
|
+
)
|
|
1623
|
+
return _dict_to_remesh_result(stats)
|
|
1624
|
+
finally:
|
|
1625
|
+
if reporter_ctx is not None: # pragma: no cover
|
|
1626
|
+
reporter_ctx.__exit__(None, None, None)
|
|
1627
|
+
|
|
1628
|
+
def remesh_optimize(
|
|
1629
|
+
self,
|
|
1630
|
+
*,
|
|
1631
|
+
progress: ProgressParam = True,
|
|
1632
|
+
verbose: int | None = None,
|
|
1633
|
+
) -> RemeshResult:
|
|
1634
|
+
"""Optimize mesh quality without changing topology.
|
|
1635
|
+
|
|
1636
|
+
Only moves vertices to improve element quality.
|
|
1637
|
+
No points are inserted or removed.
|
|
1638
|
+
|
|
1639
|
+
Parameters
|
|
1640
|
+
----------
|
|
1641
|
+
progress : bool | Callable[[ProgressEvent], bool] | None, default=True
|
|
1642
|
+
Progress reporting option:
|
|
1643
|
+
- True: Show Rich progress bar (default)
|
|
1644
|
+
- False or None: No progress reporting
|
|
1645
|
+
- Callable: Custom callback
|
|
1646
|
+
verbose : int | None
|
|
1647
|
+
Verbosity level (-1=silent, 0=errors, 1=info).
|
|
1648
|
+
|
|
1649
|
+
Returns
|
|
1650
|
+
-------
|
|
1651
|
+
RemeshResult
|
|
1652
|
+
Statistics from the remeshing operation.
|
|
1653
|
+
|
|
1654
|
+
"""
|
|
1655
|
+
opts: dict[str, int | float] = {"optim": 1, "noinsert": 1}
|
|
1656
|
+
if verbose is not None:
|
|
1657
|
+
opts["verbose"] = verbose
|
|
1658
|
+
return self.remesh(progress=progress, **opts) # type: ignore[arg-type]
|
|
1659
|
+
|
|
1660
|
+
def remesh_uniform(
|
|
1661
|
+
self,
|
|
1662
|
+
size: float,
|
|
1663
|
+
*,
|
|
1664
|
+
progress: ProgressParam = True,
|
|
1665
|
+
verbose: int | None = None,
|
|
1666
|
+
) -> RemeshResult:
|
|
1667
|
+
"""Remesh with uniform element size.
|
|
1668
|
+
|
|
1669
|
+
Parameters
|
|
1670
|
+
----------
|
|
1671
|
+
size : float
|
|
1672
|
+
Target edge size for all elements.
|
|
1673
|
+
progress : bool | Callable[[ProgressEvent], bool] | None, default=True
|
|
1674
|
+
Progress reporting option:
|
|
1675
|
+
- True: Show Rich progress bar (default)
|
|
1676
|
+
- False or None: No progress reporting
|
|
1677
|
+
- Callable: Custom callback
|
|
1678
|
+
verbose : int | None
|
|
1679
|
+
Verbosity level (-1=silent, 0=errors, 1=info).
|
|
1680
|
+
|
|
1681
|
+
Returns
|
|
1682
|
+
-------
|
|
1683
|
+
RemeshResult
|
|
1684
|
+
Statistics from the remeshing operation.
|
|
1685
|
+
|
|
1686
|
+
"""
|
|
1687
|
+
opts: dict[str, int | float] = {"hsiz": size}
|
|
1688
|
+
if verbose is not None:
|
|
1689
|
+
opts["verbose"] = verbose
|
|
1690
|
+
return self.remesh(progress=progress, **opts) # type: ignore[arg-type]
|
|
1691
|
+
|
|
1692
|
+
# =========================================================================
|
|
1693
|
+
# Local sizing constraints
|
|
1694
|
+
# =========================================================================
|
|
1695
|
+
|
|
1696
|
+
def set_size_sphere(
|
|
1697
|
+
self,
|
|
1698
|
+
center: Sequence[float] | NDArray[np.float64],
|
|
1699
|
+
radius: float,
|
|
1700
|
+
size: float,
|
|
1701
|
+
) -> None:
|
|
1702
|
+
"""Set target edge size within a spherical region.
|
|
1703
|
+
|
|
1704
|
+
Parameters
|
|
1705
|
+
----------
|
|
1706
|
+
center : array-like
|
|
1707
|
+
Center point of the sphere.
|
|
1708
|
+
radius : float
|
|
1709
|
+
Radius of the sphere.
|
|
1710
|
+
size : float
|
|
1711
|
+
Target edge size within the sphere.
|
|
1712
|
+
|
|
1713
|
+
"""
|
|
1714
|
+
from mmgpy.sizing import SphereSize # noqa: PLC0415
|
|
1715
|
+
|
|
1716
|
+
center_arr = np.asarray(center, dtype=np.float64)
|
|
1717
|
+
self._sizing_constraints.append(SphereSize(center_arr, radius, size))
|
|
1718
|
+
|
|
1719
|
+
def set_size_box(
|
|
1720
|
+
self,
|
|
1721
|
+
bounds: Sequence[Sequence[float]] | NDArray[np.float64],
|
|
1722
|
+
size: float,
|
|
1723
|
+
) -> None:
|
|
1724
|
+
"""Set target edge size within a box region.
|
|
1725
|
+
|
|
1726
|
+
Parameters
|
|
1727
|
+
----------
|
|
1728
|
+
bounds : array-like
|
|
1729
|
+
Bounding box as [[xmin, ymin, zmin], [xmax, ymax, zmax]].
|
|
1730
|
+
size : float
|
|
1731
|
+
Target edge size within the box.
|
|
1732
|
+
|
|
1733
|
+
"""
|
|
1734
|
+
from mmgpy.sizing import BoxSize # noqa: PLC0415
|
|
1735
|
+
|
|
1736
|
+
bounds_arr = np.asarray(bounds, dtype=np.float64)
|
|
1737
|
+
self._sizing_constraints.append(BoxSize(bounds_arr, size))
|
|
1738
|
+
|
|
1739
|
+
def set_size_cylinder(
|
|
1740
|
+
self,
|
|
1741
|
+
point1: Sequence[float] | NDArray[np.float64],
|
|
1742
|
+
point2: Sequence[float] | NDArray[np.float64],
|
|
1743
|
+
radius: float,
|
|
1744
|
+
size: float,
|
|
1745
|
+
) -> None:
|
|
1746
|
+
"""Set target edge size within a cylindrical region.
|
|
1747
|
+
|
|
1748
|
+
Only available for TETRAHEDRAL and TRIANGULAR_SURFACE meshes.
|
|
1749
|
+
|
|
1750
|
+
Parameters
|
|
1751
|
+
----------
|
|
1752
|
+
point1 : array-like
|
|
1753
|
+
First endpoint of the cylinder axis.
|
|
1754
|
+
point2 : array-like
|
|
1755
|
+
Second endpoint of the cylinder axis.
|
|
1756
|
+
radius : float
|
|
1757
|
+
Radius of the cylinder.
|
|
1758
|
+
size : float
|
|
1759
|
+
Target edge size within the cylinder.
|
|
1760
|
+
|
|
1761
|
+
Raises
|
|
1762
|
+
------
|
|
1763
|
+
TypeError
|
|
1764
|
+
If mesh is TRIANGULAR_2D.
|
|
1765
|
+
|
|
1766
|
+
"""
|
|
1767
|
+
if self._kind == MeshKind.TRIANGULAR_2D:
|
|
1768
|
+
msg = "set_size_cylinder() is not available for TRIANGULAR_2D meshes"
|
|
1769
|
+
raise TypeError(msg)
|
|
1770
|
+
|
|
1771
|
+
from mmgpy.sizing import CylinderSize # noqa: PLC0415
|
|
1772
|
+
|
|
1773
|
+
point1_arr = np.asarray(point1, dtype=np.float64)
|
|
1774
|
+
point2_arr = np.asarray(point2, dtype=np.float64)
|
|
1775
|
+
self._sizing_constraints.append(
|
|
1776
|
+
CylinderSize(point1_arr, point2_arr, radius, size),
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
def set_size_from_point(
|
|
1780
|
+
self,
|
|
1781
|
+
point: Sequence[float] | NDArray[np.float64],
|
|
1782
|
+
near_size: float,
|
|
1783
|
+
far_size: float,
|
|
1784
|
+
influence_radius: float,
|
|
1785
|
+
) -> None:
|
|
1786
|
+
"""Set target edge size based on distance from a point.
|
|
1787
|
+
|
|
1788
|
+
Parameters
|
|
1789
|
+
----------
|
|
1790
|
+
point : array-like
|
|
1791
|
+
Reference point.
|
|
1792
|
+
near_size : float
|
|
1793
|
+
Target edge size at the reference point.
|
|
1794
|
+
far_size : float
|
|
1795
|
+
Target edge size at the influence radius.
|
|
1796
|
+
influence_radius : float
|
|
1797
|
+
Radius of influence.
|
|
1798
|
+
|
|
1799
|
+
"""
|
|
1800
|
+
from mmgpy.sizing import PointSize # noqa: PLC0415
|
|
1801
|
+
|
|
1802
|
+
point_arr = np.asarray(point, dtype=np.float64)
|
|
1803
|
+
self._sizing_constraints.append(
|
|
1804
|
+
PointSize(point_arr, near_size, far_size, influence_radius),
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
def clear_local_sizing(self) -> None:
|
|
1808
|
+
"""Clear all local sizing constraints."""
|
|
1809
|
+
self._sizing_constraints.clear()
|
|
1810
|
+
|
|
1811
|
+
def get_local_sizing_count(self) -> int:
|
|
1812
|
+
"""Get the number of local sizing constraints.
|
|
1813
|
+
|
|
1814
|
+
Returns
|
|
1815
|
+
-------
|
|
1816
|
+
int
|
|
1817
|
+
Number of sizing constraints.
|
|
1818
|
+
|
|
1819
|
+
"""
|
|
1820
|
+
return len(self._sizing_constraints)
|
|
1821
|
+
|
|
1822
|
+
def apply_local_sizing(self) -> None:
|
|
1823
|
+
"""Apply local sizing constraints to the metric field.
|
|
1824
|
+
|
|
1825
|
+
This is called automatically before remeshing if sizing
|
|
1826
|
+
constraints have been added.
|
|
1827
|
+
"""
|
|
1828
|
+
self._apply_sizing_to_metric()
|
|
1829
|
+
|
|
1830
|
+
def _apply_sizing_to_metric(self) -> None:
|
|
1831
|
+
"""Apply sizing constraints to the metric field."""
|
|
1832
|
+
if not self._sizing_constraints:
|
|
1833
|
+
return
|
|
1834
|
+
|
|
1835
|
+
from mmgpy.sizing import apply_sizing_constraints # noqa: PLC0415
|
|
1836
|
+
|
|
1837
|
+
# apply_sizing_constraints expects a mesh object and sets the field directly
|
|
1838
|
+
apply_sizing_constraints(self._impl, self._sizing_constraints)
|
|
1839
|
+
|
|
1840
|
+
# =========================================================================
|
|
1841
|
+
# PyVista conversion
|
|
1842
|
+
# =========================================================================
|
|
1843
|
+
|
|
1844
|
+
def to_pyvista(
|
|
1845
|
+
self,
|
|
1846
|
+
*,
|
|
1847
|
+
include_refs: bool = True,
|
|
1848
|
+
) -> pv.UnstructuredGrid | pv.PolyData:
|
|
1849
|
+
"""Convert to PyVista mesh.
|
|
1850
|
+
|
|
1851
|
+
Parameters
|
|
1852
|
+
----------
|
|
1853
|
+
include_refs : bool
|
|
1854
|
+
Include reference markers as cell data.
|
|
1855
|
+
|
|
1856
|
+
Returns
|
|
1857
|
+
-------
|
|
1858
|
+
pv.UnstructuredGrid | pv.PolyData
|
|
1859
|
+
PyVista mesh object.
|
|
1860
|
+
|
|
1861
|
+
"""
|
|
1862
|
+
from mmgpy._pyvista import to_pyvista as _to_pyvista # noqa: PLC0415
|
|
1863
|
+
|
|
1864
|
+
return _to_pyvista(self._impl, include_refs=include_refs)
|
|
1865
|
+
|
|
1866
|
+
@property
|
|
1867
|
+
def vtk(self) -> pv.UnstructuredGrid | pv.PolyData:
|
|
1868
|
+
"""Get the PyVista mesh representation.
|
|
1869
|
+
|
|
1870
|
+
This property provides direct access to the PyVista mesh for use with
|
|
1871
|
+
custom plotters or other PyVista operations.
|
|
1872
|
+
|
|
1873
|
+
Returns
|
|
1874
|
+
-------
|
|
1875
|
+
pv.UnstructuredGrid | pv.PolyData
|
|
1876
|
+
PyVista mesh object.
|
|
1877
|
+
|
|
1878
|
+
Examples
|
|
1879
|
+
--------
|
|
1880
|
+
>>> plotter = pv.Plotter()
|
|
1881
|
+
>>> plotter.add_mesh(mesh.vtk, show_edges=True)
|
|
1882
|
+
>>> plotter.show()
|
|
1883
|
+
|
|
1884
|
+
"""
|
|
1885
|
+
return self.to_pyvista()
|
|
1886
|
+
|
|
1887
|
+
def plot(
|
|
1888
|
+
self,
|
|
1889
|
+
*,
|
|
1890
|
+
show_edges: bool = True,
|
|
1891
|
+
**kwargs: Any, # noqa: ANN401
|
|
1892
|
+
) -> None:
|
|
1893
|
+
"""Plot the mesh using PyVista.
|
|
1894
|
+
|
|
1895
|
+
Parameters
|
|
1896
|
+
----------
|
|
1897
|
+
show_edges : bool
|
|
1898
|
+
Show mesh edges (default: True).
|
|
1899
|
+
**kwargs : Any
|
|
1900
|
+
Additional arguments passed to PyVista's plot() method.
|
|
1901
|
+
|
|
1902
|
+
Examples
|
|
1903
|
+
--------
|
|
1904
|
+
>>> mesh = Mesh(vertices, cells)
|
|
1905
|
+
>>> mesh.plot() # Simple plot with edges
|
|
1906
|
+
|
|
1907
|
+
>>> mesh.plot(color="blue", opacity=0.8) # Custom styling
|
|
1908
|
+
|
|
1909
|
+
"""
|
|
1910
|
+
self.to_pyvista().plot(show_edges=show_edges, **kwargs)
|
|
1911
|
+
|
|
1912
|
+
# =========================================================================
|
|
1913
|
+
# Validation
|
|
1914
|
+
# =========================================================================
|
|
1915
|
+
|
|
1916
|
+
def validate( # noqa: PLR0913
|
|
1917
|
+
self,
|
|
1918
|
+
*,
|
|
1919
|
+
detailed: bool = False,
|
|
1920
|
+
strict: bool = False,
|
|
1921
|
+
check_geometry: bool = True,
|
|
1922
|
+
check_topology: bool = True,
|
|
1923
|
+
check_quality: bool = True,
|
|
1924
|
+
min_quality: float = 0.1,
|
|
1925
|
+
) -> bool | ValidationReport:
|
|
1926
|
+
"""Validate the mesh and check for issues.
|
|
1927
|
+
|
|
1928
|
+
Parameters
|
|
1929
|
+
----------
|
|
1930
|
+
detailed : bool
|
|
1931
|
+
If True, return a ValidationReport with detailed information.
|
|
1932
|
+
If False, return a simple boolean.
|
|
1933
|
+
strict : bool
|
|
1934
|
+
If True, raise ValidationError on any issue (including warnings).
|
|
1935
|
+
check_geometry : bool
|
|
1936
|
+
Check for geometric issues (inverted/degenerate elements).
|
|
1937
|
+
check_topology : bool
|
|
1938
|
+
Check for topological issues (orphan vertices, non-manifold edges).
|
|
1939
|
+
check_quality : bool
|
|
1940
|
+
Check element quality against threshold.
|
|
1941
|
+
min_quality : float
|
|
1942
|
+
Minimum acceptable element quality (0-1).
|
|
1943
|
+
|
|
1944
|
+
Returns
|
|
1945
|
+
-------
|
|
1946
|
+
bool | ValidationReport
|
|
1947
|
+
If detailed=False, returns True if valid, False otherwise.
|
|
1948
|
+
If detailed=True, returns full ValidationReport.
|
|
1949
|
+
|
|
1950
|
+
Raises
|
|
1951
|
+
------
|
|
1952
|
+
ValidationError
|
|
1953
|
+
If strict=True and any issues are found.
|
|
1954
|
+
|
|
1955
|
+
Examples
|
|
1956
|
+
--------
|
|
1957
|
+
>>> mesh = Mesh(vertices, cells)
|
|
1958
|
+
>>> if mesh.validate():
|
|
1959
|
+
... print("Mesh is valid")
|
|
1960
|
+
|
|
1961
|
+
>>> report = mesh.validate(detailed=True)
|
|
1962
|
+
>>> print(f"Quality: {report.quality.mean:.3f}")
|
|
1963
|
+
|
|
1964
|
+
"""
|
|
1965
|
+
from mmgpy._validation import ( # noqa: PLC0415
|
|
1966
|
+
ValidationError,
|
|
1967
|
+
validate_mesh_2d,
|
|
1968
|
+
validate_mesh_3d,
|
|
1969
|
+
validate_mesh_surface,
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
# Dispatch to the correct validation function based on mesh kind
|
|
1973
|
+
if self._kind == MeshKind.TETRAHEDRAL:
|
|
1974
|
+
report = validate_mesh_3d(
|
|
1975
|
+
cast("MmgMesh3D", self._impl),
|
|
1976
|
+
check_geometry=check_geometry,
|
|
1977
|
+
check_topology=check_topology,
|
|
1978
|
+
check_quality=check_quality,
|
|
1979
|
+
min_quality=min_quality,
|
|
1980
|
+
)
|
|
1981
|
+
elif self._kind == MeshKind.TRIANGULAR_2D:
|
|
1982
|
+
report = validate_mesh_2d(
|
|
1983
|
+
cast("MmgMesh2D", self._impl),
|
|
1984
|
+
check_geometry=check_geometry,
|
|
1985
|
+
check_topology=check_topology,
|
|
1986
|
+
check_quality=check_quality,
|
|
1987
|
+
min_quality=min_quality,
|
|
1988
|
+
)
|
|
1989
|
+
else: # TRIANGULAR_SURFACE
|
|
1990
|
+
report = validate_mesh_surface(
|
|
1991
|
+
cast("MmgMeshS", self._impl),
|
|
1992
|
+
check_geometry=check_geometry,
|
|
1993
|
+
check_topology=check_topology,
|
|
1994
|
+
check_quality=check_quality,
|
|
1995
|
+
min_quality=min_quality,
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
# Handle strict mode - raise on any issues
|
|
1999
|
+
if strict and (report.errors or report.warnings):
|
|
2000
|
+
raise ValidationError(report)
|
|
2001
|
+
|
|
2002
|
+
# Return report or boolean based on detailed flag
|
|
2003
|
+
if detailed:
|
|
2004
|
+
return report
|
|
2005
|
+
return report.is_valid
|
|
2006
|
+
|
|
2007
|
+
# =========================================================================
|
|
2008
|
+
# Interactive editing
|
|
2009
|
+
# =========================================================================
|
|
2010
|
+
|
|
2011
|
+
def edit_sizing(
|
|
2012
|
+
self,
|
|
2013
|
+
*,
|
|
2014
|
+
mode: str = "sphere",
|
|
2015
|
+
default_size: float = 0.01,
|
|
2016
|
+
default_radius: float = 0.1,
|
|
2017
|
+
) -> None:
|
|
2018
|
+
"""Launch interactive sizing editor.
|
|
2019
|
+
|
|
2020
|
+
Opens a PyVista window for visually defining local sizing
|
|
2021
|
+
constraints by clicking on the mesh.
|
|
2022
|
+
|
|
2023
|
+
Parameters
|
|
2024
|
+
----------
|
|
2025
|
+
mode : str
|
|
2026
|
+
Initial interaction mode: "sphere", "box", "cylinder", or "point".
|
|
2027
|
+
default_size : float
|
|
2028
|
+
Default target edge size for constraints.
|
|
2029
|
+
default_radius : float
|
|
2030
|
+
Default radius for sphere and cylinder constraints.
|
|
2031
|
+
|
|
2032
|
+
Examples
|
|
2033
|
+
--------
|
|
2034
|
+
>>> mesh = Mesh(vertices, cells)
|
|
2035
|
+
>>> mesh.edit_sizing() # Opens interactive editor
|
|
2036
|
+
>>> mesh.remesh() # Uses interactively defined sizing
|
|
2037
|
+
|
|
2038
|
+
"""
|
|
2039
|
+
from mmgpy.interactive import SizingEditor # noqa: PLC0415
|
|
2040
|
+
|
|
2041
|
+
editor = SizingEditor(self._impl)
|
|
2042
|
+
editor._current_size = default_size # noqa: SLF001
|
|
2043
|
+
editor._current_radius = default_radius # noqa: SLF001
|
|
2044
|
+
|
|
2045
|
+
mode_map = {
|
|
2046
|
+
"sphere": editor.add_sphere_tool,
|
|
2047
|
+
"box": editor.add_box_tool,
|
|
2048
|
+
"cylinder": editor.add_cylinder_tool,
|
|
2049
|
+
"point": editor.add_point_tool,
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if mode in mode_map:
|
|
2053
|
+
mode_map[mode]()
|
|
2054
|
+
|
|
2055
|
+
editor.run()
|
|
2056
|
+
editor.apply_to_mesh()
|
|
2057
|
+
|
|
2058
|
+
# =========================================================================
|
|
2059
|
+
# Context manager support
|
|
2060
|
+
# =========================================================================
|
|
2061
|
+
|
|
2062
|
+
def __enter__(self) -> Mesh: # noqa: PYI034
|
|
2063
|
+
"""Enter the context manager.
|
|
2064
|
+
|
|
2065
|
+
Returns
|
|
2066
|
+
-------
|
|
2067
|
+
Mesh
|
|
2068
|
+
The mesh instance.
|
|
2069
|
+
|
|
2070
|
+
Examples
|
|
2071
|
+
--------
|
|
2072
|
+
>>> with Mesh(vertices, cells) as mesh:
|
|
2073
|
+
... mesh.remesh(hmax=0.1)
|
|
2074
|
+
... mesh.save("output.vtk")
|
|
2075
|
+
|
|
2076
|
+
"""
|
|
2077
|
+
return self
|
|
2078
|
+
|
|
2079
|
+
def __exit__(
|
|
2080
|
+
self,
|
|
2081
|
+
exc_type: type[BaseException] | None,
|
|
2082
|
+
exc_val: BaseException | None,
|
|
2083
|
+
exc_tb: TracebackType | None,
|
|
2084
|
+
) -> bool:
|
|
2085
|
+
"""Exit the context manager.
|
|
2086
|
+
|
|
2087
|
+
Currently performs no cleanup, but provides a consistent API
|
|
2088
|
+
for resource management patterns.
|
|
2089
|
+
|
|
2090
|
+
Returns
|
|
2091
|
+
-------
|
|
2092
|
+
bool
|
|
2093
|
+
False, to not suppress any exceptions.
|
|
2094
|
+
|
|
2095
|
+
"""
|
|
2096
|
+
return False
|
|
2097
|
+
|
|
2098
|
+
def checkpoint(self) -> MeshCheckpoint:
|
|
2099
|
+
"""Create a checkpoint for transactional modifications.
|
|
2100
|
+
|
|
2101
|
+
Returns a context manager that captures the current mesh state.
|
|
2102
|
+
On exit, if `commit()` was not called or an exception occurred,
|
|
2103
|
+
the mesh is automatically rolled back to its checkpoint state.
|
|
2104
|
+
|
|
2105
|
+
Returns
|
|
2106
|
+
-------
|
|
2107
|
+
MeshCheckpoint
|
|
2108
|
+
A context manager for transactional mesh modifications.
|
|
2109
|
+
|
|
2110
|
+
Notes
|
|
2111
|
+
-----
|
|
2112
|
+
The checkpoint stores a complete copy of the mesh data including
|
|
2113
|
+
vertices, elements, reference markers, and solution fields
|
|
2114
|
+
(metric, displacement, levelset). For large meshes, this may
|
|
2115
|
+
consume significant memory.
|
|
2116
|
+
|
|
2117
|
+
Note: The tensor field is not saved because it shares memory with
|
|
2118
|
+
metric in MMG's internal representation.
|
|
2119
|
+
|
|
2120
|
+
Examples
|
|
2121
|
+
--------
|
|
2122
|
+
>>> mesh = Mesh(vertices, cells)
|
|
2123
|
+
>>> with mesh.checkpoint() as snapshot:
|
|
2124
|
+
... mesh.remesh(hmax=0.01)
|
|
2125
|
+
... if mesh.validate():
|
|
2126
|
+
... snapshot.commit() # Keep changes
|
|
2127
|
+
... # If not committed, changes are rolled back
|
|
2128
|
+
|
|
2129
|
+
>>> # Automatic rollback on exception
|
|
2130
|
+
>>> with mesh.checkpoint():
|
|
2131
|
+
... mesh.remesh(hmax=0.01)
|
|
2132
|
+
... raise ValueError("Simulated failure")
|
|
2133
|
+
>>> # mesh is restored to original state
|
|
2134
|
+
|
|
2135
|
+
"""
|
|
2136
|
+
vertices, vertex_refs = self._impl.get_vertices_with_refs()
|
|
2137
|
+
triangles, triangle_refs = self._impl.get_triangles_with_refs()
|
|
2138
|
+
edges, edge_refs = self._impl.get_edges_with_refs()
|
|
2139
|
+
|
|
2140
|
+
tetrahedra = None
|
|
2141
|
+
tetrahedra_refs = None
|
|
2142
|
+
if self._kind == MeshKind.TETRAHEDRAL:
|
|
2143
|
+
impl_3d = cast("MmgMesh3D", self._impl)
|
|
2144
|
+
tetrahedra, tetrahedra_refs = impl_3d.get_tetrahedra_with_refs()
|
|
2145
|
+
|
|
2146
|
+
# Save solution fields (metric, displacement, levelset)
|
|
2147
|
+
# Note: tensor is not saved because it shares memory with metric in MMG
|
|
2148
|
+
fields: dict[str, NDArray[np.float64]] = {}
|
|
2149
|
+
for field_name in ("metric", "displacement", "levelset"):
|
|
2150
|
+
field_data = self._try_get_field(field_name)
|
|
2151
|
+
if field_data is not None:
|
|
2152
|
+
fields[field_name] = field_data.copy()
|
|
2153
|
+
|
|
2154
|
+
return MeshCheckpoint(
|
|
2155
|
+
_mesh=self,
|
|
2156
|
+
_vertices=vertices.copy(),
|
|
2157
|
+
_vertex_refs=vertex_refs.copy(),
|
|
2158
|
+
_triangles=triangles.copy(),
|
|
2159
|
+
_triangle_refs=triangle_refs.copy(),
|
|
2160
|
+
_edges=edges.copy(),
|
|
2161
|
+
_edge_refs=edge_refs.copy(),
|
|
2162
|
+
_tetrahedra=tetrahedra.copy() if tetrahedra is not None else None,
|
|
2163
|
+
_tetrahedra_refs=(
|
|
2164
|
+
tetrahedra_refs.copy() if tetrahedra_refs is not None else None
|
|
2165
|
+
),
|
|
2166
|
+
_fields=fields,
|
|
2167
|
+
)
|
|
2168
|
+
|
|
2169
|
+
@contextmanager
|
|
2170
|
+
def copy(self) -> Generator[Mesh, None, None]:
|
|
2171
|
+
"""Create a working copy that is discarded on exit.
|
|
2172
|
+
|
|
2173
|
+
Returns a context manager that yields a copy of the mesh.
|
|
2174
|
+
The copy can be freely modified without affecting the original.
|
|
2175
|
+
Use `update_from()` to apply changes from the copy to the original.
|
|
2176
|
+
|
|
2177
|
+
Yields
|
|
2178
|
+
------
|
|
2179
|
+
Mesh
|
|
2180
|
+
A copy of this mesh.
|
|
2181
|
+
|
|
2182
|
+
Examples
|
|
2183
|
+
--------
|
|
2184
|
+
>>> original = Mesh(vertices, cells)
|
|
2185
|
+
>>> with original.copy() as working:
|
|
2186
|
+
... working.remesh(hmax=0.1)
|
|
2187
|
+
... if len(working.get_vertices()) < len(original.get_vertices()) * 2:
|
|
2188
|
+
... original.update_from(working)
|
|
2189
|
+
>>> # working is discarded on exit
|
|
2190
|
+
|
|
2191
|
+
"""
|
|
2192
|
+
vertices = self._impl.get_vertices().copy()
|
|
2193
|
+
|
|
2194
|
+
if self._kind == MeshKind.TETRAHEDRAL:
|
|
2195
|
+
impl_3d = cast("MmgMesh3D", self._impl)
|
|
2196
|
+
cells = impl_3d.get_tetrahedra().copy()
|
|
2197
|
+
else:
|
|
2198
|
+
cells = self._impl.get_triangles().copy()
|
|
2199
|
+
|
|
2200
|
+
working = Mesh(vertices, cells)
|
|
2201
|
+
|
|
2202
|
+
try:
|
|
2203
|
+
yield working
|
|
2204
|
+
finally:
|
|
2205
|
+
# No cleanup needed - working mesh is garbage collected automatically
|
|
2206
|
+
# when it goes out of scope. The finally block is kept for future
|
|
2207
|
+
# extensibility (e.g., releasing resources or logging).
|
|
2208
|
+
pass
|
|
2209
|
+
|
|
2210
|
+
def update_from(self, other: Mesh) -> None:
|
|
2211
|
+
"""Update this mesh from another mesh's state.
|
|
2212
|
+
|
|
2213
|
+
Replaces the vertices and elements of this mesh with those from
|
|
2214
|
+
the other mesh. Both meshes must be of the same kind.
|
|
2215
|
+
|
|
2216
|
+
Parameters
|
|
2217
|
+
----------
|
|
2218
|
+
other : Mesh
|
|
2219
|
+
The mesh to copy state from.
|
|
2220
|
+
|
|
2221
|
+
Raises
|
|
2222
|
+
------
|
|
2223
|
+
TypeError
|
|
2224
|
+
If the meshes are of different kinds.
|
|
2225
|
+
|
|
2226
|
+
Examples
|
|
2227
|
+
--------
|
|
2228
|
+
>>> original = Mesh(vertices, cells)
|
|
2229
|
+
>>> with original.copy() as working:
|
|
2230
|
+
... working.remesh(hmax=0.1)
|
|
2231
|
+
... original.update_from(working)
|
|
2232
|
+
|
|
2233
|
+
"""
|
|
2234
|
+
if self._kind != other._kind:
|
|
2235
|
+
msg = f"Cannot update {self._kind.value} mesh from {other._kind.value} mesh"
|
|
2236
|
+
raise TypeError(msg)
|
|
2237
|
+
|
|
2238
|
+
vertices, vertex_refs = other._impl.get_vertices_with_refs()
|
|
2239
|
+
triangles, triangle_refs = other._impl.get_triangles_with_refs()
|
|
2240
|
+
edges, edge_refs = other._impl.get_edges_with_refs()
|
|
2241
|
+
|
|
2242
|
+
if self._kind == MeshKind.TETRAHEDRAL:
|
|
2243
|
+
other_impl = cast("MmgMesh3D", other._impl)
|
|
2244
|
+
self_impl = cast("MmgMesh3D", self._impl)
|
|
2245
|
+
tetrahedra, tetrahedra_refs = other_impl.get_tetrahedra_with_refs()
|
|
2246
|
+
self_impl.set_mesh_size(
|
|
2247
|
+
vertices=len(vertices),
|
|
2248
|
+
tetrahedra=len(tetrahedra),
|
|
2249
|
+
triangles=len(triangles),
|
|
2250
|
+
edges=len(edges),
|
|
2251
|
+
)
|
|
2252
|
+
self_impl.set_vertices(vertices, vertex_refs)
|
|
2253
|
+
self_impl.set_tetrahedra(tetrahedra, tetrahedra_refs)
|
|
2254
|
+
if len(triangles) > 0:
|
|
2255
|
+
self_impl.set_triangles(triangles, triangle_refs)
|
|
2256
|
+
if len(edges) > 0:
|
|
2257
|
+
self_impl.set_edges(edges, edge_refs)
|
|
2258
|
+
elif self._kind == MeshKind.TRIANGULAR_2D:
|
|
2259
|
+
impl_2d = cast("MmgMesh2D", self._impl)
|
|
2260
|
+
impl_2d.set_mesh_size(
|
|
2261
|
+
vertices=len(vertices),
|
|
2262
|
+
triangles=len(triangles),
|
|
2263
|
+
edges=len(edges),
|
|
2264
|
+
)
|
|
2265
|
+
impl_2d.set_vertices(vertices, vertex_refs)
|
|
2266
|
+
impl_2d.set_triangles(triangles, triangle_refs)
|
|
2267
|
+
if len(edges) > 0:
|
|
2268
|
+
impl_2d.set_edges(edges, edge_refs)
|
|
2269
|
+
else: # TRIANGULAR_SURFACE
|
|
2270
|
+
impl_s = cast("MmgMeshS", self._impl)
|
|
2271
|
+
impl_s.set_mesh_size(
|
|
2272
|
+
vertices=len(vertices),
|
|
2273
|
+
triangles=len(triangles),
|
|
2274
|
+
edges=len(edges),
|
|
2275
|
+
)
|
|
2276
|
+
impl_s.set_vertices(vertices, vertex_refs)
|
|
2277
|
+
impl_s.set_triangles(triangles, triangle_refs)
|
|
2278
|
+
if len(edges) > 0:
|
|
2279
|
+
impl_s.set_edges(edges, edge_refs)
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
__all__ = [
|
|
2283
|
+
"Mesh",
|
|
2284
|
+
"MeshCheckpoint",
|
|
2285
|
+
"MeshKind",
|
|
2286
|
+
]
|