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/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
|