mmgpy 0.5.0__cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. mmgpy/__init__.py +296 -0
  2. mmgpy/__main__.py +13 -0
  3. mmgpy/_io.py +535 -0
  4. mmgpy/_logging.py +290 -0
  5. mmgpy/_mesh.py +2286 -0
  6. mmgpy/_mmgpy.cpython-311-x86_64-linux-gnu.so +0 -0
  7. mmgpy/_mmgpy.pyi +2140 -0
  8. mmgpy/_options.py +304 -0
  9. mmgpy/_progress.py +850 -0
  10. mmgpy/_pyvista.py +410 -0
  11. mmgpy/_result.py +143 -0
  12. mmgpy/_transfer.py +273 -0
  13. mmgpy/_validation.py +669 -0
  14. mmgpy/_version.py +3 -0
  15. mmgpy/_version.py.in +3 -0
  16. mmgpy/bin/mmg2d_O3 +0 -0
  17. mmgpy/bin/mmg3d_O3 +0 -0
  18. mmgpy/bin/mmgs_O3 +0 -0
  19. mmgpy/interactive/__init__.py +24 -0
  20. mmgpy/interactive/sizing_editor.py +790 -0
  21. mmgpy/lagrangian.py +394 -0
  22. mmgpy/lib/libmmg2d.so +0 -0
  23. mmgpy/lib/libmmg2d.so.5 +0 -0
  24. mmgpy/lib/libmmg2d.so.5.8.0 +0 -0
  25. mmgpy/lib/libmmg3d.so +0 -0
  26. mmgpy/lib/libmmg3d.so.5 +0 -0
  27. mmgpy/lib/libmmg3d.so.5.8.0 +0 -0
  28. mmgpy/lib/libmmgs.so +0 -0
  29. mmgpy/lib/libmmgs.so.5 +0 -0
  30. mmgpy/lib/libmmgs.so.5.8.0 +0 -0
  31. mmgpy/lib/libvtkCommonColor-9.5.so.1 +0 -0
  32. mmgpy/lib/libvtkCommonComputationalGeometry-9.5.so.1 +0 -0
  33. mmgpy/lib/libvtkCommonCore-9.5.so.1 +0 -0
  34. mmgpy/lib/libvtkCommonDataModel-9.5.so.1 +0 -0
  35. mmgpy/lib/libvtkCommonExecutionModel-9.5.so.1 +0 -0
  36. mmgpy/lib/libvtkCommonMath-9.5.so.1 +0 -0
  37. mmgpy/lib/libvtkCommonMisc-9.5.so.1 +0 -0
  38. mmgpy/lib/libvtkCommonSystem-9.5.so.1 +0 -0
  39. mmgpy/lib/libvtkCommonTransforms-9.5.so.1 +0 -0
  40. mmgpy/lib/libvtkDICOMParser-9.5.so.1 +0 -0
  41. mmgpy/lib/libvtkFiltersCellGrid-9.5.so.1 +0 -0
  42. mmgpy/lib/libvtkFiltersCore-9.5.so.1 +0 -0
  43. mmgpy/lib/libvtkFiltersExtraction-9.5.so.1 +0 -0
  44. mmgpy/lib/libvtkFiltersGeneral-9.5.so.1 +0 -0
  45. mmgpy/lib/libvtkFiltersGeometry-9.5.so.1 +0 -0
  46. mmgpy/lib/libvtkFiltersHybrid-9.5.so.1 +0 -0
  47. mmgpy/lib/libvtkFiltersHyperTree-9.5.so.1 +0 -0
  48. mmgpy/lib/libvtkFiltersModeling-9.5.so.1 +0 -0
  49. mmgpy/lib/libvtkFiltersParallel-9.5.so.1 +0 -0
  50. mmgpy/lib/libvtkFiltersReduction-9.5.so.1 +0 -0
  51. mmgpy/lib/libvtkFiltersSources-9.5.so.1 +0 -0
  52. mmgpy/lib/libvtkFiltersStatistics-9.5.so.1 +0 -0
  53. mmgpy/lib/libvtkFiltersTexture-9.5.so.1 +0 -0
  54. mmgpy/lib/libvtkFiltersVerdict-9.5.so.1 +0 -0
  55. mmgpy/lib/libvtkIOCellGrid-9.5.so.1 +0 -0
  56. mmgpy/lib/libvtkIOCore-9.5.so.1 +0 -0
  57. mmgpy/lib/libvtkIOGeometry-9.5.so.1 +0 -0
  58. mmgpy/lib/libvtkIOImage-9.5.so.1 +0 -0
  59. mmgpy/lib/libvtkIOLegacy-9.5.so.1 +0 -0
  60. mmgpy/lib/libvtkIOParallel-9.5.so.1 +0 -0
  61. mmgpy/lib/libvtkIOParallelXML-9.5.so.1 +0 -0
  62. mmgpy/lib/libvtkIOXML-9.5.so.1 +0 -0
  63. mmgpy/lib/libvtkIOXMLParser-9.5.so.1 +0 -0
  64. mmgpy/lib/libvtkImagingCore-9.5.so.1 +0 -0
  65. mmgpy/lib/libvtkImagingSources-9.5.so.1 +0 -0
  66. mmgpy/lib/libvtkParallelCore-9.5.so.1 +0 -0
  67. mmgpy/lib/libvtkParallelDIY-9.5.so.1 +0 -0
  68. mmgpy/lib/libvtkRenderingCore-9.5.so.1 +0 -0
  69. mmgpy/lib/libvtkdoubleconversion-9.5.so.1 +0 -0
  70. mmgpy/lib/libvtkexpat-9.5.so.1 +0 -0
  71. mmgpy/lib/libvtkfmt-9.5.so.1 +0 -0
  72. mmgpy/lib/libvtkjpeg-9.5.so.1 +0 -0
  73. mmgpy/lib/libvtkjsoncpp-9.5.so.1 +0 -0
  74. mmgpy/lib/libvtkkissfft-9.5.so.1 +0 -0
  75. mmgpy/lib/libvtkloguru-9.5.so.1 +0 -0
  76. mmgpy/lib/libvtklz4-9.5.so.1 +0 -0
  77. mmgpy/lib/libvtklzma-9.5.so.1 +0 -0
  78. mmgpy/lib/libvtkmetaio-9.5.so.1 +0 -0
  79. mmgpy/lib/libvtkpng-9.5.so.1 +0 -0
  80. mmgpy/lib/libvtkpugixml-9.5.so.1 +0 -0
  81. mmgpy/lib/libvtksys-9.5.so.1 +0 -0
  82. mmgpy/lib/libvtktiff-9.5.so.1 +0 -0
  83. mmgpy/lib/libvtktoken-9.5.so.1 +0 -0
  84. mmgpy/lib/libvtkverdict-9.5.so.1 +0 -0
  85. mmgpy/lib/libvtkzlib-9.5.so.1 +0 -0
  86. mmgpy/metrics.py +596 -0
  87. mmgpy/progress.py +69 -0
  88. mmgpy/py.typed +0 -0
  89. mmgpy/repair/__init__.py +37 -0
  90. mmgpy/repair/_core.py +226 -0
  91. mmgpy/repair/_elements.py +241 -0
  92. mmgpy/repair/_vertices.py +219 -0
  93. mmgpy/sizing.py +370 -0
  94. mmgpy/ui/__init__.py +97 -0
  95. mmgpy/ui/__main__.py +87 -0
  96. mmgpy/ui/app.py +1837 -0
  97. mmgpy/ui/parsers.py +501 -0
  98. mmgpy/ui/remeshing.py +448 -0
  99. mmgpy/ui/samples.py +249 -0
  100. mmgpy/ui/utils.py +280 -0
  101. mmgpy/ui/viewer.py +587 -0
  102. mmgpy-0.5.0.dist-info/METADATA +186 -0
  103. mmgpy-0.5.0.dist-info/RECORD +109 -0
  104. mmgpy-0.5.0.dist-info/WHEEL +6 -0
  105. mmgpy-0.5.0.dist-info/entry_points.txt +13 -0
  106. mmgpy-0.5.0.dist-info/licenses/LICENSE +38 -0
  107. share/man/man1/mmg2d.1.gz +0 -0
  108. share/man/man1/mmg3d.1.gz +0 -0
  109. share/man/man1/mmgs.1.gz +0 -0
mmgpy/_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
+ )