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/_progress.py
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
"""Progress callback utilities for mmgpy with Rich integration.
|
|
2
|
+
|
|
3
|
+
This module provides progress callbacks for remeshing operations with support
|
|
4
|
+
for cancellation and Rich progress bar integration.
|
|
5
|
+
|
|
6
|
+
Example usage with a simple callback:
|
|
7
|
+
|
|
8
|
+
>>> def my_progress(event: ProgressEvent) -> bool:
|
|
9
|
+
... print(f"{event.phase}: {event.progress_percent:.0f}% - {event.message}")
|
|
10
|
+
... return True # Return False to cancel
|
|
11
|
+
>>> mesh.remesh(hmax=0.1, progress=my_progress)
|
|
12
|
+
|
|
13
|
+
Example with Rich progress:
|
|
14
|
+
|
|
15
|
+
>>> with rich_progress() as callback:
|
|
16
|
+
... mesh.remesh(hmax=0.1, progress=callback)
|
|
17
|
+
|
|
18
|
+
Example with cancellation:
|
|
19
|
+
|
|
20
|
+
>>> import threading
|
|
21
|
+
>>> cancel_flag = threading.Event()
|
|
22
|
+
>>> def check_cancel(event: ProgressEvent) -> bool:
|
|
23
|
+
... return not cancel_flag.is_set() # Return False to cancel
|
|
24
|
+
>>> # In another thread: cancel_flag.set()
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import sys
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import TYPE_CHECKING, Protocol
|
|
34
|
+
|
|
35
|
+
if sys.version_info >= (3, 11):
|
|
36
|
+
from typing import Self
|
|
37
|
+
else:
|
|
38
|
+
from typing_extensions import Self
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import Callable, Generator
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
import numpy as np
|
|
46
|
+
from numpy.typing import NDArray
|
|
47
|
+
|
|
48
|
+
from ._mmgpy import MmgMesh2D, MmgMesh3D, MmgMeshS
|
|
49
|
+
|
|
50
|
+
MeshType = MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
51
|
+
ProgressCallback = Callable[["ProgressEvent"], bool]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CancellationError(Exception):
|
|
55
|
+
"""Exception raised when a remeshing operation is cancelled via callback.
|
|
56
|
+
|
|
57
|
+
This exception is raised when a progress callback returns False, indicating
|
|
58
|
+
the user wants to cancel the operation.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
phase : str, optional
|
|
63
|
+
The phase during which cancellation occurred.
|
|
64
|
+
message : str, optional
|
|
65
|
+
Custom message describing the cancellation.
|
|
66
|
+
|
|
67
|
+
Attributes
|
|
68
|
+
----------
|
|
69
|
+
phase : str | None
|
|
70
|
+
The phase during which cancellation occurred.
|
|
71
|
+
|
|
72
|
+
Examples
|
|
73
|
+
--------
|
|
74
|
+
>>> def cancel_callback(event: ProgressEvent) -> bool:
|
|
75
|
+
... return False # Cancel immediately
|
|
76
|
+
>>> try:
|
|
77
|
+
... remesh_mesh(mesh, progress=cancel_callback, hmax=0.1)
|
|
78
|
+
... except CancellationError as e:
|
|
79
|
+
... print(f"Operation was cancelled at phase: {e.phase}")
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, phase: str | None = None, message: str | None = None) -> None:
|
|
84
|
+
"""Initialize the CancellationError.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
phase : str | None
|
|
89
|
+
The phase during which cancellation occurred.
|
|
90
|
+
message : str | None
|
|
91
|
+
Custom message. If not provided, a default message based on phase is used.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
self.phase = phase
|
|
95
|
+
if message is None and phase is not None:
|
|
96
|
+
phase_messages = {
|
|
97
|
+
"init": "Operation cancelled during init phase",
|
|
98
|
+
"load": "Operation cancelled during load phase",
|
|
99
|
+
"options": "Operation cancelled during options phase",
|
|
100
|
+
"remesh": "Operation cancelled before remeshing",
|
|
101
|
+
"save": "Operation cancelled during save phase",
|
|
102
|
+
}
|
|
103
|
+
message = phase_messages.get(phase, f"Operation cancelled at {phase}")
|
|
104
|
+
elif message is None:
|
|
105
|
+
message = "Operation cancelled"
|
|
106
|
+
super().__init__(message)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def for_phase(cls, phase: str) -> Self:
|
|
110
|
+
"""Create a CancellationError for a specific phase.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
phase : str
|
|
115
|
+
The phase during which cancellation occurred.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
CancellationError
|
|
120
|
+
A new CancellationError with appropriate message for the phase.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
return cls(phase=phase)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ProgressEvent:
|
|
128
|
+
"""Event emitted during mesh operations.
|
|
129
|
+
|
|
130
|
+
Attributes
|
|
131
|
+
----------
|
|
132
|
+
phase : str
|
|
133
|
+
The current phase of the operation. One of:
|
|
134
|
+
- "init": Initializing mesh structures
|
|
135
|
+
- "load": Loading mesh from file
|
|
136
|
+
- "options": Setting remeshing options
|
|
137
|
+
- "remesh": Performing remeshing (status="start" or "complete")
|
|
138
|
+
- "save": Saving mesh to file
|
|
139
|
+
status : str
|
|
140
|
+
Status within the phase ("start", "complete", or "progress").
|
|
141
|
+
message : str
|
|
142
|
+
Human-readable description of what's happening.
|
|
143
|
+
progress : float | None
|
|
144
|
+
Progress within the current phase as a value from 0.0 to 1.0.
|
|
145
|
+
None indicates indeterminate progress.
|
|
146
|
+
details : dict[str, Any] | None
|
|
147
|
+
Optional additional details (e.g., vertex/element counts after remesh).
|
|
148
|
+
|
|
149
|
+
Notes
|
|
150
|
+
-----
|
|
151
|
+
Due to limitations in the underlying MMG library, fine-grained progress
|
|
152
|
+
during the actual remeshing phase is not available. Progress events are
|
|
153
|
+
emitted at phase boundaries (start/complete) with progress values of
|
|
154
|
+
0.0 at start and 1.0 at complete.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
phase: str
|
|
159
|
+
status: str
|
|
160
|
+
message: str
|
|
161
|
+
progress: float | None = None
|
|
162
|
+
details: dict[str, Any] | None = None
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def progress_percent(self) -> float:
|
|
166
|
+
"""Return progress as a percentage (0-100).
|
|
167
|
+
|
|
168
|
+
Returns 0 if progress is None (indeterminate).
|
|
169
|
+
"""
|
|
170
|
+
return (self.progress or 0.0) * 100
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProgressReporter(Protocol):
|
|
174
|
+
"""Protocol for progress reporters.
|
|
175
|
+
|
|
176
|
+
A progress reporter is a callable that receives ProgressEvent instances
|
|
177
|
+
and returns a boolean indicating whether to continue the operation.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
bool
|
|
182
|
+
True to continue the operation, False to cancel.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __call__(self, event: ProgressEvent) -> bool: # pragma: no cover
|
|
187
|
+
"""Report a progress event and return whether to continue."""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _emit_event( # noqa: PLR0913
|
|
192
|
+
callback: ProgressCallback | None,
|
|
193
|
+
phase: str,
|
|
194
|
+
status: str,
|
|
195
|
+
message: str,
|
|
196
|
+
progress: float | None = None,
|
|
197
|
+
details: dict[str, Any] | None = None,
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""Emit a progress event if callback is provided.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
callback : ProgressCallback | None
|
|
204
|
+
The progress callback to invoke.
|
|
205
|
+
phase : str
|
|
206
|
+
The current phase of the operation.
|
|
207
|
+
status : str
|
|
208
|
+
Status within the phase.
|
|
209
|
+
message : str
|
|
210
|
+
Human-readable description.
|
|
211
|
+
progress : float | None
|
|
212
|
+
Progress value from 0.0 to 1.0.
|
|
213
|
+
details : dict[str, Any] | None
|
|
214
|
+
Optional additional details.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
bool
|
|
219
|
+
True if operation should continue, False if cancelled.
|
|
220
|
+
Always returns True if callback is None.
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
if callback is None:
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
event = ProgressEvent(
|
|
227
|
+
phase=phase,
|
|
228
|
+
status=status,
|
|
229
|
+
message=message,
|
|
230
|
+
progress=progress,
|
|
231
|
+
details=details,
|
|
232
|
+
)
|
|
233
|
+
result = callback(event)
|
|
234
|
+
# Handle callbacks that return None (treat as continue)
|
|
235
|
+
return result if result is not None else True
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class LoggingProgressReporter:
|
|
239
|
+
"""Progress reporter that logs events using mmgpy's logger.
|
|
240
|
+
|
|
241
|
+
This reporter logs all progress events using mmgpy's configured logger.
|
|
242
|
+
It always returns True, so it cannot be used for cancellation.
|
|
243
|
+
|
|
244
|
+
Examples
|
|
245
|
+
--------
|
|
246
|
+
>>> from mmgpy.progress import LoggingProgressReporter, remesh_mesh
|
|
247
|
+
>>> reporter = LoggingProgressReporter()
|
|
248
|
+
>>> remesh_mesh(mesh, progress=reporter, hmax=0.1)
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self) -> None:
|
|
253
|
+
"""Initialize the logging progress reporter."""
|
|
254
|
+
from ._logging import get_logger
|
|
255
|
+
|
|
256
|
+
self._logger = get_logger()
|
|
257
|
+
|
|
258
|
+
def __call__(self, event: ProgressEvent) -> bool:
|
|
259
|
+
"""Log the progress event.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
event : ProgressEvent
|
|
264
|
+
The progress event to log.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
bool
|
|
269
|
+
Always returns True (never cancels).
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
msg = f"[{event.phase}] {event.message}"
|
|
273
|
+
if event.progress is not None:
|
|
274
|
+
msg = f"{msg} ({event.progress_percent:.0f}%)"
|
|
275
|
+
if event.details:
|
|
276
|
+
details_str = ", ".join(f"{k}={v}" for k, v in event.details.items())
|
|
277
|
+
msg = f"{msg} ({details_str})"
|
|
278
|
+
self._logger.info(msg)
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class RichProgressReporter:
|
|
283
|
+
"""Progress reporter using Rich's progress display.
|
|
284
|
+
|
|
285
|
+
This reporter creates a Rich Progress display with multiple tasks
|
|
286
|
+
corresponding to the phases of remeshing operations. It always returns
|
|
287
|
+
True, so it cannot be used for cancellation.
|
|
288
|
+
|
|
289
|
+
Examples
|
|
290
|
+
--------
|
|
291
|
+
>>> from mmgpy import MmgMesh3D
|
|
292
|
+
>>> from mmgpy.progress import RichProgressReporter
|
|
293
|
+
>>> mesh = MmgMesh3D(vertices, elements)
|
|
294
|
+
>>> with RichProgressReporter() as reporter:
|
|
295
|
+
... mesh.remesh(hmax=0.1, progress=reporter)
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, *, transient: bool = True) -> None: # pragma: no cover
|
|
300
|
+
"""Initialize the Rich progress reporter.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
transient : bool, default=True
|
|
305
|
+
If True, the progress display is removed after completion.
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
self._transient = transient
|
|
309
|
+
self._progress = None
|
|
310
|
+
self._tasks: dict[str, Any] = {}
|
|
311
|
+
self._phase_names = {
|
|
312
|
+
"init": "Initializing",
|
|
313
|
+
"load": "Loading mesh",
|
|
314
|
+
"options": "Setting options",
|
|
315
|
+
"remesh": "Remeshing",
|
|
316
|
+
"save": "Saving mesh",
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
def __enter__(self) -> Self: # pragma: no cover
|
|
320
|
+
"""Start the progress display."""
|
|
321
|
+
from rich.progress import (
|
|
322
|
+
BarColumn,
|
|
323
|
+
Progress,
|
|
324
|
+
SpinnerColumn,
|
|
325
|
+
TextColumn,
|
|
326
|
+
TimeElapsedColumn,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
self._progress = Progress(
|
|
330
|
+
SpinnerColumn(),
|
|
331
|
+
TextColumn("[progress.description]{task.description}"),
|
|
332
|
+
BarColumn(),
|
|
333
|
+
TimeElapsedColumn(),
|
|
334
|
+
transient=self._transient,
|
|
335
|
+
)
|
|
336
|
+
self._progress.start()
|
|
337
|
+
return self
|
|
338
|
+
|
|
339
|
+
def __exit__(self, *args: object) -> None: # pragma: no cover
|
|
340
|
+
"""Stop the progress display."""
|
|
341
|
+
if self._progress is not None:
|
|
342
|
+
self._progress.stop()
|
|
343
|
+
|
|
344
|
+
def __call__(self, event: ProgressEvent) -> bool: # pragma: no cover
|
|
345
|
+
"""Update the progress display with the event.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
event : ProgressEvent
|
|
350
|
+
The progress event to display.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
bool
|
|
355
|
+
Always returns True (never cancels).
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
if self._progress is None:
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
phase_desc = self._phase_names.get(event.phase, event.phase.capitalize())
|
|
362
|
+
|
|
363
|
+
if event.phase not in self._tasks:
|
|
364
|
+
task_id = self._progress.add_task(
|
|
365
|
+
description=phase_desc,
|
|
366
|
+
total=1.0,
|
|
367
|
+
)
|
|
368
|
+
self._tasks[event.phase] = task_id
|
|
369
|
+
|
|
370
|
+
task_id = self._tasks[event.phase]
|
|
371
|
+
|
|
372
|
+
if event.status == "complete":
|
|
373
|
+
self._progress.update(task_id, completed=1.0, description=f"{phase_desc}")
|
|
374
|
+
elif event.status == "start":
|
|
375
|
+
self._progress.update(
|
|
376
|
+
task_id,
|
|
377
|
+
completed=event.progress or 0.0,
|
|
378
|
+
description=f"{phase_desc}...",
|
|
379
|
+
)
|
|
380
|
+
elif event.status == "progress" and event.progress is not None:
|
|
381
|
+
self._progress.update(task_id, completed=event.progress)
|
|
382
|
+
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@contextmanager
|
|
387
|
+
def rich_progress(
|
|
388
|
+
*,
|
|
389
|
+
transient: bool = True,
|
|
390
|
+
) -> Generator[ProgressCallback, None, None]:
|
|
391
|
+
"""Context manager for Rich progress display.
|
|
392
|
+
|
|
393
|
+
This is a convenience function for using Rich progress with remeshing.
|
|
394
|
+
The yielded callback always returns True, so it cannot be used for
|
|
395
|
+
cancellation on its own.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
transient : bool, default=True
|
|
400
|
+
If True, the progress display is removed after completion.
|
|
401
|
+
|
|
402
|
+
Yields
|
|
403
|
+
------
|
|
404
|
+
ProgressCallback
|
|
405
|
+
A progress callback function that always returns True.
|
|
406
|
+
|
|
407
|
+
Examples
|
|
408
|
+
--------
|
|
409
|
+
>>> from mmgpy import MmgMesh3D
|
|
410
|
+
>>> from mmgpy.progress import rich_progress
|
|
411
|
+
>>> mesh = MmgMesh3D(vertices, elements)
|
|
412
|
+
>>> with rich_progress() as callback:
|
|
413
|
+
... mesh.remesh(hmax=0.1, progress=callback)
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
from rich.progress import (
|
|
417
|
+
BarColumn,
|
|
418
|
+
Progress,
|
|
419
|
+
SpinnerColumn,
|
|
420
|
+
TextColumn,
|
|
421
|
+
TimeElapsedColumn,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
phase_names = {
|
|
425
|
+
"init": "Initializing",
|
|
426
|
+
"load": "Loading mesh",
|
|
427
|
+
"options": "Setting options",
|
|
428
|
+
"remesh": "Remeshing",
|
|
429
|
+
"save": "Saving mesh",
|
|
430
|
+
}
|
|
431
|
+
tasks: dict[str, Any] = {}
|
|
432
|
+
|
|
433
|
+
with Progress(
|
|
434
|
+
SpinnerColumn(),
|
|
435
|
+
TextColumn("[progress.description]{task.description}"),
|
|
436
|
+
BarColumn(),
|
|
437
|
+
TimeElapsedColumn(),
|
|
438
|
+
transient=transient,
|
|
439
|
+
) as progress:
|
|
440
|
+
|
|
441
|
+
def callback(event: ProgressEvent) -> bool:
|
|
442
|
+
phase_desc = phase_names.get(event.phase, event.phase.capitalize())
|
|
443
|
+
|
|
444
|
+
if event.phase not in tasks:
|
|
445
|
+
task_id = progress.add_task(
|
|
446
|
+
description=phase_desc,
|
|
447
|
+
total=1.0,
|
|
448
|
+
)
|
|
449
|
+
tasks[event.phase] = task_id
|
|
450
|
+
|
|
451
|
+
task_id = tasks[event.phase]
|
|
452
|
+
|
|
453
|
+
if event.status == "complete":
|
|
454
|
+
progress.update(task_id, completed=1.0, description=f"{phase_desc}")
|
|
455
|
+
elif event.status == "start":
|
|
456
|
+
progress.update(
|
|
457
|
+
task_id,
|
|
458
|
+
completed=event.progress or 0.0,
|
|
459
|
+
description=f"{phase_desc}...",
|
|
460
|
+
)
|
|
461
|
+
elif event.status == "progress" and event.progress is not None:
|
|
462
|
+
progress.update(task_id, completed=event.progress)
|
|
463
|
+
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
yield callback
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def remesh_3d( # pragma: no cover
|
|
470
|
+
input_mesh: str | Path,
|
|
471
|
+
output_mesh: str | Path,
|
|
472
|
+
*,
|
|
473
|
+
input_sol: str | Path | None = None,
|
|
474
|
+
output_sol: str | Path | None = None,
|
|
475
|
+
progress: ProgressCallback | None = None,
|
|
476
|
+
**options: float,
|
|
477
|
+
) -> bool:
|
|
478
|
+
"""Remesh a 3D mesh with optional progress callback.
|
|
479
|
+
|
|
480
|
+
This is a wrapper around mmg3d.remesh that adds progress callback support.
|
|
481
|
+
The callback can return False to request cancellation before the remeshing
|
|
482
|
+
operation starts.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
input_mesh : str | Path
|
|
487
|
+
Path to the input mesh file.
|
|
488
|
+
output_mesh : str | Path
|
|
489
|
+
Path to the output mesh file.
|
|
490
|
+
input_sol : str | Path | None, optional
|
|
491
|
+
Path to the input solution file.
|
|
492
|
+
output_sol : str | Path | None, optional
|
|
493
|
+
Path to the output solution file.
|
|
494
|
+
progress : ProgressCallback | None, optional
|
|
495
|
+
Callback function to receive progress events. Return False to cancel.
|
|
496
|
+
**options
|
|
497
|
+
Additional options passed to mmg3d.remesh (hmin, hmax, hausd, etc.).
|
|
498
|
+
|
|
499
|
+
Returns
|
|
500
|
+
-------
|
|
501
|
+
bool
|
|
502
|
+
True if remeshing succeeded, False otherwise.
|
|
503
|
+
|
|
504
|
+
Raises
|
|
505
|
+
------
|
|
506
|
+
CancellationError
|
|
507
|
+
If the callback returns False to cancel the operation.
|
|
508
|
+
|
|
509
|
+
Examples
|
|
510
|
+
--------
|
|
511
|
+
>>> from mmgpy.progress import remesh_3d, rich_progress
|
|
512
|
+
>>> with rich_progress() as callback:
|
|
513
|
+
... remesh_3d("input.mesh", "output.mesh", hmax=0.1, progress=callback)
|
|
514
|
+
|
|
515
|
+
"""
|
|
516
|
+
from ._mmgpy import mmg3d
|
|
517
|
+
|
|
518
|
+
if not _emit_event(progress, "load", "start", "Loading input mesh", progress=0.0):
|
|
519
|
+
raise CancellationError.for_phase("load") # noqa: EM101
|
|
520
|
+
|
|
521
|
+
if not _emit_event(
|
|
522
|
+
progress,
|
|
523
|
+
"options",
|
|
524
|
+
"start",
|
|
525
|
+
"Setting remesh options",
|
|
526
|
+
progress=0.0,
|
|
527
|
+
):
|
|
528
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
529
|
+
|
|
530
|
+
if not _emit_event(progress, "remesh", "start", "Starting remeshing", progress=0.0):
|
|
531
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
532
|
+
|
|
533
|
+
result = mmg3d.remesh(
|
|
534
|
+
input_mesh=input_mesh,
|
|
535
|
+
input_sol=input_sol,
|
|
536
|
+
output_mesh=output_mesh,
|
|
537
|
+
output_sol=output_sol,
|
|
538
|
+
options=options,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
_emit_event(
|
|
542
|
+
progress,
|
|
543
|
+
"remesh",
|
|
544
|
+
"complete",
|
|
545
|
+
"Remeshing complete",
|
|
546
|
+
progress=1.0,
|
|
547
|
+
details={"success": result},
|
|
548
|
+
)
|
|
549
|
+
_emit_event(progress, "save", "complete", "Mesh saved", progress=1.0)
|
|
550
|
+
|
|
551
|
+
return result
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def remesh_2d( # pragma: no cover
|
|
555
|
+
input_mesh: str | Path,
|
|
556
|
+
output_mesh: str | Path,
|
|
557
|
+
*,
|
|
558
|
+
input_sol: str | Path | None = None,
|
|
559
|
+
output_sol: str | Path | None = None,
|
|
560
|
+
progress: ProgressCallback | None = None,
|
|
561
|
+
**options: float,
|
|
562
|
+
) -> bool:
|
|
563
|
+
"""Remesh a 2D mesh with optional progress callback.
|
|
564
|
+
|
|
565
|
+
This is a wrapper around mmg2d.remesh that adds progress callback support.
|
|
566
|
+
The callback can return False to request cancellation before the remeshing
|
|
567
|
+
operation starts.
|
|
568
|
+
|
|
569
|
+
Parameters
|
|
570
|
+
----------
|
|
571
|
+
input_mesh : str | Path
|
|
572
|
+
Path to the input mesh file.
|
|
573
|
+
output_mesh : str | Path
|
|
574
|
+
Path to the output mesh file.
|
|
575
|
+
input_sol : str | Path | None, optional
|
|
576
|
+
Path to the input solution file.
|
|
577
|
+
output_sol : str | Path | None, optional
|
|
578
|
+
Path to the output solution file.
|
|
579
|
+
progress : ProgressCallback | None, optional
|
|
580
|
+
Callback function to receive progress events. Return False to cancel.
|
|
581
|
+
**options
|
|
582
|
+
Additional options passed to mmg2d.remesh (hmin, hmax, hausd, etc.).
|
|
583
|
+
|
|
584
|
+
Returns
|
|
585
|
+
-------
|
|
586
|
+
bool
|
|
587
|
+
True if remeshing succeeded, False otherwise.
|
|
588
|
+
|
|
589
|
+
Raises
|
|
590
|
+
------
|
|
591
|
+
CancellationError
|
|
592
|
+
If the callback returns False to cancel the operation.
|
|
593
|
+
|
|
594
|
+
"""
|
|
595
|
+
from ._mmgpy import mmg2d
|
|
596
|
+
|
|
597
|
+
if not _emit_event(progress, "load", "start", "Loading input mesh", progress=0.0):
|
|
598
|
+
raise CancellationError.for_phase("load") # noqa: EM101
|
|
599
|
+
|
|
600
|
+
if not _emit_event(
|
|
601
|
+
progress,
|
|
602
|
+
"options",
|
|
603
|
+
"start",
|
|
604
|
+
"Setting remesh options",
|
|
605
|
+
progress=0.0,
|
|
606
|
+
):
|
|
607
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
608
|
+
|
|
609
|
+
if not _emit_event(progress, "remesh", "start", "Starting remeshing", progress=0.0):
|
|
610
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
611
|
+
|
|
612
|
+
result = mmg2d.remesh(
|
|
613
|
+
input_mesh=input_mesh,
|
|
614
|
+
input_sol=input_sol,
|
|
615
|
+
output_mesh=output_mesh,
|
|
616
|
+
output_sol=output_sol,
|
|
617
|
+
options=options,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
_emit_event(
|
|
621
|
+
progress,
|
|
622
|
+
"remesh",
|
|
623
|
+
"complete",
|
|
624
|
+
"Remeshing complete",
|
|
625
|
+
progress=1.0,
|
|
626
|
+
details={"success": result},
|
|
627
|
+
)
|
|
628
|
+
_emit_event(progress, "save", "complete", "Mesh saved", progress=1.0)
|
|
629
|
+
|
|
630
|
+
return result
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def remesh_surface( # pragma: no cover
|
|
634
|
+
input_mesh: str | Path,
|
|
635
|
+
output_mesh: str | Path,
|
|
636
|
+
*,
|
|
637
|
+
input_sol: str | Path | None = None,
|
|
638
|
+
output_sol: str | Path | None = None,
|
|
639
|
+
progress: ProgressCallback | None = None,
|
|
640
|
+
**options: float,
|
|
641
|
+
) -> bool:
|
|
642
|
+
"""Remesh a surface mesh with optional progress callback.
|
|
643
|
+
|
|
644
|
+
This is a wrapper around mmgs.remesh that adds progress callback support.
|
|
645
|
+
The callback can return False to request cancellation before the remeshing
|
|
646
|
+
operation starts.
|
|
647
|
+
|
|
648
|
+
Parameters
|
|
649
|
+
----------
|
|
650
|
+
input_mesh : str | Path
|
|
651
|
+
Path to the input mesh file.
|
|
652
|
+
output_mesh : str | Path
|
|
653
|
+
Path to the output mesh file.
|
|
654
|
+
input_sol : str | Path | None, optional
|
|
655
|
+
Path to the input solution file.
|
|
656
|
+
output_sol : str | Path | None, optional
|
|
657
|
+
Path to the output solution file.
|
|
658
|
+
progress : ProgressCallback | None, optional
|
|
659
|
+
Callback function to receive progress events. Return False to cancel.
|
|
660
|
+
**options
|
|
661
|
+
Additional options passed to mmgs.remesh (hmin, hmax, hausd, etc.).
|
|
662
|
+
|
|
663
|
+
Returns
|
|
664
|
+
-------
|
|
665
|
+
bool
|
|
666
|
+
True if remeshing succeeded, False otherwise.
|
|
667
|
+
|
|
668
|
+
Raises
|
|
669
|
+
------
|
|
670
|
+
CancellationError
|
|
671
|
+
If the callback returns False to cancel the operation.
|
|
672
|
+
|
|
673
|
+
"""
|
|
674
|
+
from ._mmgpy import mmgs
|
|
675
|
+
|
|
676
|
+
if not _emit_event(progress, "load", "start", "Loading input mesh", progress=0.0):
|
|
677
|
+
raise CancellationError.for_phase("load") # noqa: EM101
|
|
678
|
+
|
|
679
|
+
if not _emit_event(
|
|
680
|
+
progress,
|
|
681
|
+
"options",
|
|
682
|
+
"start",
|
|
683
|
+
"Setting remesh options",
|
|
684
|
+
progress=0.0,
|
|
685
|
+
):
|
|
686
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
687
|
+
|
|
688
|
+
if not _emit_event(progress, "remesh", "start", "Starting remeshing", progress=0.0):
|
|
689
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
690
|
+
|
|
691
|
+
result = mmgs.remesh(
|
|
692
|
+
input_mesh=input_mesh,
|
|
693
|
+
input_sol=input_sol,
|
|
694
|
+
output_mesh=output_mesh,
|
|
695
|
+
output_sol=output_sol,
|
|
696
|
+
options=options,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
_emit_event(
|
|
700
|
+
progress,
|
|
701
|
+
"remesh",
|
|
702
|
+
"complete",
|
|
703
|
+
"Remeshing complete",
|
|
704
|
+
progress=1.0,
|
|
705
|
+
details={"success": result},
|
|
706
|
+
)
|
|
707
|
+
_emit_event(progress, "save", "complete", "Mesh saved", progress=1.0)
|
|
708
|
+
|
|
709
|
+
return result
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def remesh_mesh(
|
|
713
|
+
mesh: MeshType,
|
|
714
|
+
*,
|
|
715
|
+
progress: ProgressCallback | None = None,
|
|
716
|
+
**options: float | bool | None,
|
|
717
|
+
) -> None:
|
|
718
|
+
"""Remesh an in-memory mesh with optional progress callback.
|
|
719
|
+
|
|
720
|
+
This is a wrapper around MmgMesh.remesh that adds progress callback support.
|
|
721
|
+
The callback can return False to request cancellation before the remeshing
|
|
722
|
+
operation starts.
|
|
723
|
+
|
|
724
|
+
Parameters
|
|
725
|
+
----------
|
|
726
|
+
mesh : MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
727
|
+
The mesh object to remesh.
|
|
728
|
+
progress : ProgressCallback | None, optional
|
|
729
|
+
Callback function to receive progress events. Return False to cancel.
|
|
730
|
+
**options
|
|
731
|
+
Additional options passed to mesh.remesh (hmin, hmax, hausd, etc.).
|
|
732
|
+
|
|
733
|
+
Raises
|
|
734
|
+
------
|
|
735
|
+
CancellationError
|
|
736
|
+
If the callback returns False to cancel the operation.
|
|
737
|
+
|
|
738
|
+
Examples
|
|
739
|
+
--------
|
|
740
|
+
>>> from mmgpy import MmgMesh3D
|
|
741
|
+
>>> from mmgpy.progress import remesh_mesh, rich_progress
|
|
742
|
+
>>> mesh = MmgMesh3D(vertices, elements)
|
|
743
|
+
>>> with rich_progress() as callback:
|
|
744
|
+
... remesh_mesh(mesh, hmax=0.1, progress=callback)
|
|
745
|
+
|
|
746
|
+
"""
|
|
747
|
+
if not _emit_event(progress, "init", "start", "Initializing mesh", progress=0.0):
|
|
748
|
+
raise CancellationError.for_phase("init") # noqa: EM101
|
|
749
|
+
|
|
750
|
+
initial_vertices = len(mesh.get_vertices())
|
|
751
|
+
|
|
752
|
+
if not _emit_event(
|
|
753
|
+
progress,
|
|
754
|
+
"options",
|
|
755
|
+
"start",
|
|
756
|
+
"Setting remesh options",
|
|
757
|
+
progress=0.0,
|
|
758
|
+
):
|
|
759
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
760
|
+
|
|
761
|
+
if not _emit_event(progress, "remesh", "start", "Starting remeshing", progress=0.0):
|
|
762
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
763
|
+
|
|
764
|
+
mesh.remesh(**options)
|
|
765
|
+
|
|
766
|
+
final_vertices = len(mesh.get_vertices())
|
|
767
|
+
|
|
768
|
+
_emit_event(
|
|
769
|
+
progress,
|
|
770
|
+
"remesh",
|
|
771
|
+
"complete",
|
|
772
|
+
"Remeshing complete",
|
|
773
|
+
progress=1.0,
|
|
774
|
+
details={
|
|
775
|
+
"initial_vertices": initial_vertices,
|
|
776
|
+
"final_vertices": final_vertices,
|
|
777
|
+
"vertex_change": final_vertices - initial_vertices,
|
|
778
|
+
},
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def remesh_mesh_lagrangian( # pragma: no cover
|
|
783
|
+
mesh: MeshType,
|
|
784
|
+
displacement: NDArray[np.float64],
|
|
785
|
+
*,
|
|
786
|
+
progress: ProgressCallback | None = None,
|
|
787
|
+
**options: float | bool | None,
|
|
788
|
+
) -> None:
|
|
789
|
+
"""Remesh an in-memory mesh with Lagrangian motion and progress callback.
|
|
790
|
+
|
|
791
|
+
This is a wrapper around MmgMesh.remesh_lagrangian that adds progress
|
|
792
|
+
callback support. The callback can return False to request cancellation
|
|
793
|
+
before the remeshing operation starts.
|
|
794
|
+
|
|
795
|
+
Parameters
|
|
796
|
+
----------
|
|
797
|
+
mesh : MmgMesh3D | MmgMesh2D | MmgMeshS
|
|
798
|
+
The mesh object to remesh.
|
|
799
|
+
displacement : NDArray[np.float64]
|
|
800
|
+
Displacement field for Lagrangian motion.
|
|
801
|
+
progress : ProgressCallback | None, optional
|
|
802
|
+
Callback function to receive progress events. Return False to cancel.
|
|
803
|
+
**options
|
|
804
|
+
Additional options passed to mesh.remesh_lagrangian.
|
|
805
|
+
|
|
806
|
+
Raises
|
|
807
|
+
------
|
|
808
|
+
CancellationError
|
|
809
|
+
If the callback returns False to cancel the operation.
|
|
810
|
+
|
|
811
|
+
"""
|
|
812
|
+
if not _emit_event(progress, "init", "start", "Initializing mesh", progress=0.0):
|
|
813
|
+
raise CancellationError.for_phase("init") # noqa: EM101
|
|
814
|
+
|
|
815
|
+
initial_vertices = len(mesh.get_vertices())
|
|
816
|
+
|
|
817
|
+
if not _emit_event(
|
|
818
|
+
progress,
|
|
819
|
+
"options",
|
|
820
|
+
"start",
|
|
821
|
+
"Setting displacement field",
|
|
822
|
+
progress=0.0,
|
|
823
|
+
):
|
|
824
|
+
raise CancellationError.for_phase("options") # noqa: EM101
|
|
825
|
+
|
|
826
|
+
if not _emit_event(
|
|
827
|
+
progress,
|
|
828
|
+
"remesh",
|
|
829
|
+
"start",
|
|
830
|
+
"Starting Lagrangian remeshing",
|
|
831
|
+
progress=0.0,
|
|
832
|
+
):
|
|
833
|
+
raise CancellationError.for_phase("remesh") # noqa: EM101
|
|
834
|
+
|
|
835
|
+
mesh.remesh_lagrangian(displacement, **options)
|
|
836
|
+
|
|
837
|
+
final_vertices = len(mesh.get_vertices())
|
|
838
|
+
|
|
839
|
+
_emit_event(
|
|
840
|
+
progress,
|
|
841
|
+
"remesh",
|
|
842
|
+
"complete",
|
|
843
|
+
"Lagrangian remeshing complete",
|
|
844
|
+
progress=1.0,
|
|
845
|
+
details={
|
|
846
|
+
"initial_vertices": initial_vertices,
|
|
847
|
+
"final_vertices": final_vertices,
|
|
848
|
+
"vertex_change": final_vertices - initial_vertices,
|
|
849
|
+
},
|
|
850
|
+
)
|