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/ui/remeshing.py ADDED
@@ -0,0 +1,448 @@
1
+ """Remeshing mixin for mmgpy UI - handles remeshing operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+
10
+ from mmgpy.ui.parsers import evaluate_levelset_formula
11
+ from mmgpy.ui.utils import (
12
+ compute_preset_values,
13
+ get_mesh_diagonal,
14
+ reset_solution_state,
15
+ to_float,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from mmgpy import Mesh
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Random number generator for reproducible displacement fields
24
+ # Using fixed seed for deterministic Lagrangian motion demo
25
+ _rng = np.random.default_rng(42)
26
+
27
+
28
+ class RemeshingMixin:
29
+ """Mixin class providing remeshing functionality.
30
+
31
+ This mixin provides methods for:
32
+ - Executing remeshing operations (standard, levelset, lagrangian, optimize)
33
+ - Building remesh options from UI state
34
+ - Transferring solution fields between meshes
35
+ - Managing sizing constraints
36
+ - Applying presets
37
+ """
38
+
39
+ # These attributes are expected to be defined by the main class
40
+ _mesh: Mesh | None
41
+ _original_mesh: Mesh | None
42
+ _solution_metric: np.ndarray | None
43
+ _solution_fields: dict[str, dict]
44
+ _original_solution_metric: np.ndarray | None
45
+ _original_solution_fields: dict[str, dict]
46
+ _applying_preset: bool
47
+ state: object
48
+
49
+ # These methods are expected to be defined by other mixins or the main class
50
+ def _update_mesh_info(self) -> None: ...
51
+ def _update_viewer(self, *, reset_camera: bool = True) -> None: ...
52
+ def _update_scalar_field_options(self) -> None: ...
53
+
54
+ def _apply_adaptive_defaults(self) -> None:
55
+ """Set default remeshing parameters based on mesh scale.
56
+
57
+ Uses the 'medium' preset values to initialize parameters.
58
+ """
59
+ diagonal = get_mesh_diagonal(self._mesh)
60
+ values = compute_preset_values("medium", diagonal)
61
+
62
+ self._applying_preset = True
63
+ try:
64
+ self.state.hmax = values.get("hmax")
65
+ self.state.hausd = values.get("hausd")
66
+ self.state.hgrad = values.get("hgrad", 1.3)
67
+ self.state.hmin = None
68
+ self.state.use_preset = "medium"
69
+ finally:
70
+ self._applying_preset = False
71
+ self.state.flush()
72
+
73
+ def _apply_preset_trigger(self, preset: str) -> None:
74
+ """Trigger handler for preset buttons."""
75
+ self.state.use_preset = preset
76
+ self._apply_preset(preset)
77
+
78
+ def _apply_preset(self, preset: str) -> None:
79
+ """Apply a remeshing preset scaled to mesh size."""
80
+ if preset == "custom":
81
+ return
82
+
83
+ diagonal = get_mesh_diagonal(self._mesh)
84
+ values = compute_preset_values(preset, diagonal)
85
+
86
+ if values:
87
+ self._applying_preset = True
88
+ try:
89
+ for key, value in values.items():
90
+ setattr(self.state, key, value)
91
+ finally:
92
+ self._applying_preset = False
93
+ self.state.flush()
94
+
95
+ def _run_remesh(self) -> None:
96
+ """Execute remeshing operation."""
97
+ from mmgpy import Mesh
98
+
99
+ if self._mesh is None:
100
+ return
101
+
102
+ self.state.is_remeshing = True
103
+
104
+ try:
105
+ # Choose source mesh and solution based on option
106
+ use_original = (
107
+ self.state.remesh_source == "original"
108
+ and self._original_mesh is not None
109
+ )
110
+ if use_original:
111
+ source_mesh = self._original_mesh
112
+ source_solution_fields = self._original_solution_fields
113
+ source_solution_metric = self._original_solution_metric
114
+ else:
115
+ source_mesh = self._mesh
116
+ source_solution_fields = self._solution_fields
117
+ source_solution_metric = self._solution_metric
118
+
119
+ # Store old mesh info for field transfer
120
+ old_vertices = source_mesh.get_vertices()
121
+ kind = source_mesh.kind.value
122
+ if kind == "tetrahedral":
123
+ old_elements = source_mesh.get_tetrahedra()
124
+ else:
125
+ old_elements = source_mesh.get_triangles()
126
+
127
+ # For tetrahedral meshes, preserve boundary triangle refs
128
+ # (they're stored separately in MMG and lost during PyVista round-trip)
129
+ boundary_triangles = None
130
+ boundary_refs = None
131
+ if kind == "tetrahedral":
132
+ try:
133
+ boundary_triangles, boundary_refs = (
134
+ source_mesh.get_triangles_with_refs()
135
+ )
136
+ except Exception:
137
+ pass
138
+
139
+ # Create a fresh Mesh object
140
+ pv_mesh = source_mesh.to_pyvista()
141
+ self._mesh = Mesh(pv_mesh)
142
+
143
+ # Restore boundary triangle refs for tetrahedral meshes
144
+ # Need to resize mesh to include triangles before setting them
145
+ if boundary_triangles is not None and boundary_refs is not None:
146
+ try:
147
+ # Access internal implementation to resize mesh
148
+ impl = self._mesh._impl # noqa: SLF001
149
+ vertices = self._mesh.get_vertices()
150
+ tetrahedra = self._mesh.get_tetrahedra()
151
+ impl.set_mesh_size(
152
+ vertices=len(vertices),
153
+ tetrahedra=len(tetrahedra),
154
+ triangles=len(boundary_triangles),
155
+ )
156
+ # Re-set vertices and tetrahedra after resize
157
+ _, vert_refs = impl.get_vertices_with_refs()
158
+ _, tet_refs = impl.get_tetrahedra_with_refs()
159
+ impl.set_vertices(vertices, vert_refs)
160
+ impl.set_tetrahedra(tetrahedra, tet_refs)
161
+ impl.set_triangles(boundary_triangles, boundary_refs)
162
+ except Exception:
163
+ logger.debug("Could not restore boundary triangle refs")
164
+
165
+ # Apply solution as metric if enabled
166
+ if self.state.use_solution_as_metric and source_solution_metric is not None:
167
+ n_vertices = len(self._mesh.get_vertices())
168
+ if len(source_solution_metric) == n_vertices:
169
+ metric = source_solution_metric
170
+ if metric.ndim == 1:
171
+ metric = metric.reshape(-1, 1)
172
+ self._mesh.set_field("metric", metric.astype(np.float64))
173
+
174
+ options = self._build_remesh_options()
175
+ result = self._execute_remesh(source_solution_metric, options)
176
+
177
+ self.state.remesh_result = {
178
+ "vertices_before": result.vertices_before,
179
+ "vertices_after": result.vertices_after,
180
+ "elements_before": result.elements_before,
181
+ "elements_after": result.elements_after,
182
+ "quality_before": f"{result.quality_mean_before:.3f}",
183
+ "quality_after": f"{result.quality_mean_after:.3f}",
184
+ "duration": f"{result.duration_seconds:.2f}s",
185
+ "warnings": list(result.warnings),
186
+ }
187
+
188
+ # Transfer solution fields
189
+ self._transfer_solution_fields(
190
+ source_solution_fields,
191
+ old_vertices,
192
+ old_elements,
193
+ )
194
+
195
+ self._update_mesh_info()
196
+ self._update_viewer(reset_camera=False)
197
+
198
+ except Exception as e:
199
+ logger.exception("Remeshing failed")
200
+ self.state.remesh_result = {"error": str(e)}
201
+ finally:
202
+ self.state.is_remeshing = False
203
+ self.state.flush()
204
+
205
+ def _build_remesh_options(self) -> dict:
206
+ """Build options dictionary for remeshing."""
207
+ options = {}
208
+
209
+ hmin = to_float(self.state.hmin)
210
+ hmax = to_float(self.state.hmax)
211
+ hsiz = to_float(self.state.hsiz)
212
+ hausd = to_float(self.state.hausd)
213
+ hgrad = to_float(self.state.hgrad)
214
+ ar = to_float(self.state.ar)
215
+
216
+ # Validate parameters
217
+ if hmin is not None and hmin <= 0:
218
+ msg = "hmin must be > 0"
219
+ raise ValueError(msg)
220
+ if hmax is not None and hmax <= 0:
221
+ msg = "hmax must be > 0"
222
+ raise ValueError(msg)
223
+ if hsiz is not None and hsiz <= 0:
224
+ msg = "hsiz must be > 0"
225
+ raise ValueError(msg)
226
+ if hausd is not None and hausd <= 0:
227
+ msg = "hausd must be > 0"
228
+ raise ValueError(msg)
229
+ if hgrad is not None and hgrad <= 1.0:
230
+ msg = "hgrad must be > 1.0"
231
+ raise ValueError(msg)
232
+ if hmin is not None and hmax is not None and hmin > hmax:
233
+ msg = "hmin must be <= hmax"
234
+ raise ValueError(msg)
235
+
236
+ if hmin is not None:
237
+ options["hmin"] = hmin
238
+ if hmax is not None:
239
+ options["hmax"] = hmax
240
+ if hsiz is not None:
241
+ options["hsiz"] = hsiz
242
+ if hausd is not None:
243
+ options["hausd"] = hausd
244
+ if hgrad is not None:
245
+ options["hgrad"] = hgrad
246
+ if ar is not None:
247
+ options["ar"] = ar
248
+
249
+ options["verbose"] = int(self.state.verbose or 1)
250
+
251
+ # Memory limit
252
+ mem = to_float(self.state.mem)
253
+ if mem is not None:
254
+ if mem <= 0:
255
+ msg = "mem must be > 0"
256
+ raise ValueError(msg)
257
+ options["mem"] = int(mem)
258
+
259
+ # Get selected options from multi-select button group
260
+ selected = self.state.selected_options or []
261
+ if "optim" in selected:
262
+ options["optim"] = 1
263
+ if "noinsert" in selected:
264
+ options["noinsert"] = 1
265
+ if "noswap" in selected:
266
+ options["noswap"] = 1
267
+ if "nomove" in selected:
268
+ options["nomove"] = 1
269
+ if "nosurf" in selected and self.state.mesh_kind == "tetrahedral":
270
+ options["nosurf"] = 1
271
+ if "nreg" in selected:
272
+ options["nreg"] = 1
273
+ if "opnbdy" in selected and self.state.mesh_kind == "tetrahedral":
274
+ options["opnbdy"] = 1
275
+
276
+ return options
277
+
278
+ def _execute_remesh(self, source_solution_metric, options: dict):
279
+ """Execute the appropriate remesh operation."""
280
+ mode = self.state.remesh_mode
281
+
282
+ if mode == "standard":
283
+ return self._mesh.remesh(progress=False, **options)
284
+
285
+ if mode == "levelset":
286
+ if (
287
+ self.state.use_solution_as_levelset
288
+ and source_solution_metric is not None
289
+ ):
290
+ levelset = source_solution_metric
291
+ if levelset.ndim == 1:
292
+ levelset = levelset.reshape(-1, 1)
293
+ else:
294
+ levelset = self._compute_levelset()
295
+ # Add levelset isovalue (ls) parameter
296
+ ls_value = to_float(self.state.levelset_isovalue)
297
+ if ls_value is not None:
298
+ options["ls"] = ls_value
299
+ return self._mesh.remesh_levelset(levelset, progress=False, **options)
300
+
301
+ if mode == "lagrangian":
302
+ displacement = self._compute_displacement()
303
+ return self._mesh.remesh_lagrangian(
304
+ displacement,
305
+ progress=False,
306
+ **options,
307
+ )
308
+
309
+ # Fallback to standard remesh
310
+ return self._mesh.remesh(progress=False, **options)
311
+
312
+ def _transfer_solution_fields(
313
+ self,
314
+ source_solution_fields: dict,
315
+ old_vertices: np.ndarray,
316
+ old_elements: np.ndarray,
317
+ ) -> None:
318
+ """Transfer solution fields to new mesh."""
319
+ if not source_solution_fields:
320
+ return
321
+
322
+ from mmgpy._transfer import transfer_fields
323
+
324
+ new_vertices = self._mesh.get_vertices()
325
+ vertex_fields = {
326
+ name: info["data"]
327
+ for name, info in source_solution_fields.items()
328
+ if info["location"] == "vertices"
329
+ }
330
+
331
+ if not vertex_fields:
332
+ return
333
+
334
+ try:
335
+ transferred = transfer_fields(
336
+ source_vertices=old_vertices,
337
+ source_elements=old_elements,
338
+ target_points=new_vertices,
339
+ fields=vertex_fields,
340
+ )
341
+ for name, new_data in transferred.items():
342
+ if name in self._solution_fields:
343
+ self._solution_fields[name]["data"] = new_data
344
+ else:
345
+ loc = source_solution_fields[name]["location"]
346
+ self._solution_fields[name] = {
347
+ "data": new_data,
348
+ "location": loc,
349
+ }
350
+ first_field = next(iter(vertex_fields.keys()))
351
+ self._solution_metric = transferred[first_field].copy()
352
+ self._update_scalar_field_options()
353
+ except Exception:
354
+ logger.warning(
355
+ "Failed to transfer solution fields, clearing solution state",
356
+ )
357
+ for key, value in reset_solution_state().items():
358
+ setattr(self.state, key, value)
359
+ self._solution_fields = {}
360
+ self._solution_metric = None
361
+ if self.state.show_scalar.startswith("user_"):
362
+ self.state.show_scalar = "quality"
363
+ self._update_scalar_field_options()
364
+
365
+ def _compute_levelset(self) -> np.ndarray:
366
+ """Compute levelset field from formula using safe evaluation."""
367
+ vertices = self._mesh.get_vertices()
368
+ x, y, z = vertices[:, 0], vertices[:, 1], vertices[:, 2]
369
+
370
+ formula = self.state.levelset_formula
371
+ return evaluate_levelset_formula(formula, x, y, z)
372
+
373
+ def _compute_displacement(self) -> np.ndarray:
374
+ """Compute displacement field."""
375
+ vertices = self._mesh.get_vertices()
376
+ n_verts = len(vertices)
377
+ dim = vertices.shape[1]
378
+
379
+ scale = float(self.state.displacement_scale)
380
+ displacement = _rng.standard_normal((n_verts, dim)) * scale
381
+
382
+ return displacement.astype(np.float64)
383
+
384
+ def _run_validation(self) -> None:
385
+ """Run mesh validation."""
386
+ if self._mesh is None:
387
+ return
388
+
389
+ report = self._mesh.validate(detailed=True)
390
+
391
+ quality_data = None
392
+ if report.quality:
393
+ quality_data = {
394
+ "min": f"{report.quality.min:.3f}",
395
+ "max": f"{report.quality.max:.3f}",
396
+ "mean": f"{report.quality.mean:.3f}",
397
+ "std": f"{report.quality.std:.3f}",
398
+ "histogram": list(report.quality.histogram),
399
+ }
400
+
401
+ self.state.validation_report = {
402
+ "is_valid": report.is_valid,
403
+ "mesh_type": report.mesh_type,
404
+ "errors": [
405
+ {"check": i.check_name, "message": i.message} for i in report.errors
406
+ ],
407
+ "warnings": [
408
+ {"check": i.check_name, "message": i.message} for i in report.warnings
409
+ ],
410
+ "quality": quality_data,
411
+ }
412
+
413
+ def _add_sizing_constraint(self, constraint_type: str, params: dict) -> None:
414
+ """Add a sizing constraint."""
415
+ if self._mesh is None:
416
+ return
417
+
418
+ if constraint_type == "sphere":
419
+ self._mesh.set_size_sphere(
420
+ center=params["center"],
421
+ radius=params["radius"],
422
+ size=params["size"],
423
+ )
424
+ elif constraint_type == "box":
425
+ self._mesh.set_size_box(
426
+ bounds=params["bounds"],
427
+ size=params["size"],
428
+ )
429
+ elif constraint_type == "point":
430
+ self._mesh.set_size_from_point(
431
+ point=params["point"],
432
+ near_size=params["near_size"],
433
+ far_size=params["far_size"],
434
+ influence_radius=params["influence_radius"],
435
+ )
436
+
437
+ constraints = list(self.state.sizing_constraints)
438
+ constraints.append({"type": constraint_type, "params": params})
439
+ self.state.sizing_constraints = constraints
440
+
441
+ self._update_viewer()
442
+
443
+ def _clear_sizing_constraints(self) -> None:
444
+ """Clear all sizing constraints."""
445
+ if self._mesh is not None:
446
+ self._mesh.clear_local_sizing()
447
+ self.state.sizing_constraints = []
448
+ self._update_viewer()
mmgpy/ui/samples.py ADDED
@@ -0,0 +1,249 @@
1
+ """Sample mesh generators for the mmgpy UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pyvista as pv
7
+
8
+ # =============================================================================
9
+ # Sample mesh generation constants
10
+ # =============================================================================
11
+
12
+ # Tetrahedral cube: points per axis for structured grid
13
+ TETRA_CUBE_POINTS_PER_AXIS = 5
14
+
15
+ # Tetrahedral sphere: concentric shells for point distribution
16
+ TETRA_SPHERE_NUM_SHELLS = 3
17
+ TETRA_SPHERE_BASE_PHI_POINTS = 4 # Base number of latitude points
18
+ TETRA_SPHERE_PHI_INCREMENT = 2 # Additional phi points per shell
19
+ TETRA_SPHERE_BASE_THETA_POINTS = 8 # Base number of longitude points
20
+ TETRA_SPHERE_THETA_INCREMENT = 4 # Additional theta points per shell
21
+
22
+ # 2D disc: ring and sector configuration
23
+ DISC_2D_NUM_RINGS = 5
24
+ DISC_2D_NUM_SECTORS = 16
25
+ DISC_2D_INNER_RADIUS = 0.1 # Avoid degenerate triangles at center
26
+
27
+ # 2D rectangle: grid resolution
28
+ RECT_2D_RESOLUTION = 10
29
+
30
+ # Surface mesh resolutions (for pyvista primitives)
31
+ SURFACE_SPHERE_RESOLUTION = 20
32
+ SURFACE_CYLINDER_RESOLUTION = 20
33
+ SURFACE_CONE_RESOLUTION = 20
34
+ SURFACE_TORUS_U_RESOLUTION = 30
35
+ SURFACE_TORUS_V_RESOLUTION = 30
36
+
37
+
38
+ # =============================================================================
39
+ # Sample mesh generator functions
40
+ # =============================================================================
41
+
42
+
43
+ def create_tetra_cube() -> pv.UnstructuredGrid:
44
+ """Create a tetrahedral cube mesh from interior points.
45
+
46
+ Uses a structured grid of points inside a unit cube, then applies
47
+ Delaunay tetrahedralization.
48
+
49
+ Returns
50
+ -------
51
+ pv.UnstructuredGrid
52
+ A tetrahedral mesh of a unit cube centered at origin.
53
+
54
+ """
55
+ n = TETRA_CUBE_POINTS_PER_AXIS
56
+ x = np.linspace(-0.5, 0.5, n)
57
+ y = np.linspace(-0.5, 0.5, n)
58
+ z = np.linspace(-0.5, 0.5, n)
59
+ xx, yy, zz = np.meshgrid(x, y, z)
60
+ points = np.column_stack([xx.ravel(), yy.ravel(), zz.ravel()])
61
+ cloud = pv.PolyData(points)
62
+ return cloud.delaunay_3d()
63
+
64
+
65
+ def create_tetra_sphere() -> pv.UnstructuredGrid:
66
+ """Create a tetrahedral sphere mesh from structured interior points.
67
+
68
+ Uses spherical shells to create a structured point distribution
69
+ inside the sphere, then applies Delaunay tetrahedralization.
70
+ Point density increases with shell radius for better quality.
71
+
72
+ Returns
73
+ -------
74
+ pv.UnstructuredGrid
75
+ A tetrahedral mesh of a unit sphere centered at origin.
76
+
77
+ """
78
+ points = [[0, 0, 0]] # Center point
79
+ for shell in range(1, TETRA_SPHERE_NUM_SHELLS + 1):
80
+ r = shell / TETRA_SPHERE_NUM_SHELLS
81
+ # Increase point density with shell radius
82
+ n_phi = TETRA_SPHERE_BASE_PHI_POINTS + shell * TETRA_SPHERE_PHI_INCREMENT
83
+ n_theta = TETRA_SPHERE_BASE_THETA_POINTS + shell * TETRA_SPHERE_THETA_INCREMENT
84
+ for i in range(n_phi):
85
+ phi = np.pi * (i + 0.5) / n_phi # Offset to avoid poles
86
+ for j in range(n_theta):
87
+ theta = 2 * np.pi * j / n_theta
88
+ x = r * np.sin(phi) * np.cos(theta)
89
+ y = r * np.sin(phi) * np.sin(theta)
90
+ z = r * np.cos(phi)
91
+ points.append([x, y, z])
92
+ points = np.array(points)
93
+ cloud = pv.PolyData(points)
94
+ return cloud.delaunay_3d()
95
+
96
+
97
+ def create_2d_disc() -> pv.PolyData:
98
+ """Create a 2D triangular disc mesh with good quality.
99
+
100
+ Uses concentric rings with a small inner radius to avoid
101
+ degenerate center triangles, then applies 2D Delaunay triangulation.
102
+
103
+ Returns
104
+ -------
105
+ pv.PolyData
106
+ A 2D triangular mesh of a unit disc in the XY plane.
107
+
108
+ """
109
+ points = []
110
+ for i in range(DISC_2D_NUM_RINGS + 1):
111
+ # Start from inner radius to avoid center issues
112
+ r = DISC_2D_INNER_RADIUS + (1 - DISC_2D_INNER_RADIUS) * i / DISC_2D_NUM_RINGS
113
+ if i == 0:
114
+ points.append([0, 0, 0]) # Center point
115
+ else:
116
+ for j in range(DISC_2D_NUM_SECTORS):
117
+ theta = 2 * np.pi * j / DISC_2D_NUM_SECTORS
118
+ points.append([r * np.cos(theta), r * np.sin(theta), 0])
119
+ points = np.array(points)
120
+ cloud = pv.PolyData(points)
121
+ return cloud.delaunay_2d()
122
+
123
+
124
+ def create_2d_rectangle() -> pv.PolyData:
125
+ """Create a 2D triangular rectangle mesh.
126
+
127
+ Returns
128
+ -------
129
+ pv.PolyData
130
+ A 2D triangular mesh of a unit rectangle in the XY plane.
131
+
132
+ """
133
+ plane = pv.Plane(i_resolution=RECT_2D_RESOLUTION, j_resolution=RECT_2D_RESOLUTION)
134
+ return plane.triangulate()
135
+
136
+
137
+ # Sample mesh registry
138
+ SAMPLE_MESHES: dict[str, dict] = {
139
+ # Surface meshes (mmgs)
140
+ "sphere": {
141
+ "create": lambda: pv.Sphere(
142
+ theta_resolution=SURFACE_SPHERE_RESOLUTION,
143
+ phi_resolution=SURFACE_SPHERE_RESOLUTION,
144
+ ),
145
+ "category": "surface",
146
+ "icon": "mdi-sphere",
147
+ },
148
+ "cube": {
149
+ "create": lambda: pv.Cube().triangulate(),
150
+ "category": "surface",
151
+ "icon": "mdi-cube-outline",
152
+ },
153
+ "cylinder": {
154
+ "create": lambda: pv.Cylinder(
155
+ resolution=SURFACE_CYLINDER_RESOLUTION,
156
+ ).triangulate(),
157
+ "category": "surface",
158
+ "icon": "mdi-cylinder",
159
+ },
160
+ "cone": {
161
+ "create": lambda: pv.Cone(resolution=SURFACE_CONE_RESOLUTION).triangulate(),
162
+ "category": "surface",
163
+ "icon": "mdi-cone",
164
+ },
165
+ "torus": {
166
+ "create": lambda: pv.ParametricTorus(
167
+ u_res=SURFACE_TORUS_U_RESOLUTION,
168
+ v_res=SURFACE_TORUS_V_RESOLUTION,
169
+ ),
170
+ "category": "surface",
171
+ "icon": "mdi-circle-double",
172
+ },
173
+ "bunny": {
174
+ "create": lambda: pv.examples.download_bunny(),
175
+ "category": "surface",
176
+ "icon": "mdi-rabbit",
177
+ },
178
+ # Tetrahedral meshes (mmg3d)
179
+ "tetra_cube": {
180
+ "create": create_tetra_cube,
181
+ "category": "tetrahedral",
182
+ "icon": "mdi-cube",
183
+ },
184
+ "tetra_sphere": {
185
+ "create": create_tetra_sphere,
186
+ "category": "tetrahedral",
187
+ "icon": "mdi-sphere",
188
+ },
189
+ # 2D meshes (mmg2d)
190
+ "disc_2d": {
191
+ "create": create_2d_disc,
192
+ "category": "2d",
193
+ "icon": "mdi-circle",
194
+ },
195
+ "rect_2d": {
196
+ "create": create_2d_rectangle,
197
+ "category": "2d",
198
+ "icon": "mdi-rectangle",
199
+ },
200
+ }
201
+
202
+
203
+ def get_sample_mesh(name: str) -> pv.DataSet | None:
204
+ """Get a sample mesh by name.
205
+
206
+ Parameters
207
+ ----------
208
+ name : str
209
+ Name of the sample mesh.
210
+
211
+ Returns
212
+ -------
213
+ pv.DataSet | None
214
+ The sample mesh, or None if not found.
215
+
216
+ """
217
+ if name not in SAMPLE_MESHES:
218
+ return None
219
+
220
+ pv_mesh = SAMPLE_MESHES[name]["create"]()
221
+
222
+ # Triangulate if needed (but not for tetrahedral meshes)
223
+ if hasattr(pv_mesh, "triangulate") and pv_mesh.n_cells > 0:
224
+ # Only triangulate if not already tetrahedral
225
+ if not (hasattr(pv_mesh, "celltypes") and 10 in pv_mesh.celltypes):
226
+ pv_mesh = pv_mesh.triangulate()
227
+
228
+ return pv_mesh
229
+
230
+
231
+ def list_samples_by_category() -> dict[str, list[str]]:
232
+ """List available sample meshes grouped by category.
233
+
234
+ Returns
235
+ -------
236
+ dict[str, list[str]]
237
+ Sample names grouped by category (surface, tetrahedral, 2d).
238
+
239
+ """
240
+ categories: dict[str, list[str]] = {
241
+ "surface": [],
242
+ "tetrahedral": [],
243
+ "2d": [],
244
+ }
245
+ for name, info in SAMPLE_MESHES.items():
246
+ category = info["category"]
247
+ if category in categories:
248
+ categories[category].append(name)
249
+ return categories