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/metrics.py ADDED
@@ -0,0 +1,596 @@
1
+ """Metric tensor utilities for anisotropic mesh adaptation.
2
+
3
+ This module provides helper functions for creating, validating, and manipulating
4
+ metric tensors used in anisotropic mesh adaptation with MMG.
5
+
6
+ Metric tensors define the desired element size and shape at each vertex:
7
+ - **Isotropic metric**: Single scalar h → equilateral elements of size h
8
+ - **Anisotropic metric**: Symmetric tensor → stretched elements aligned with
9
+ principal directions
10
+
11
+ Tensor storage format (row-major, upper triangular):
12
+ - **3D**: [m11, m12, m13, m22, m23, m33] (6 components)
13
+ - **2D**: [m11, m12, m22] (3 components)
14
+
15
+ The metric eigenvalues encode element sizes: size_i = 1 / sqrt(eigenvalue_i)
16
+
17
+ Examples
18
+ --------
19
+ Create an isotropic metric with uniform size h=0.1:
20
+
21
+ >>> import numpy as np
22
+ >>> from mmgpy.metrics import create_isotropic_metric
23
+ >>> n_vertices = 100
24
+ >>> h = 0.1
25
+ >>> metric = create_isotropic_metric(h, n_vertices, dim=3)
26
+ >>> metric.shape
27
+ (100, 6)
28
+
29
+ Create an anisotropic metric with different sizes in each direction:
30
+
31
+ >>> from mmgpy.metrics import create_anisotropic_metric
32
+ >>> sizes = np.array([0.1, 0.5, 0.1]) # Small in x,z, large in y
33
+ >>> directions = np.eye(3) # Aligned with coordinate axes
34
+ >>> metric = create_anisotropic_metric(sizes, directions)
35
+ >>> metric.shape
36
+ (6,)
37
+
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ from typing import TYPE_CHECKING
43
+
44
+ import numpy as np
45
+
46
+ if TYPE_CHECKING:
47
+ from numpy.typing import NDArray
48
+
49
+
50
+ def create_isotropic_metric(
51
+ h: float | NDArray[np.float64],
52
+ n_vertices: int | None = None,
53
+ dim: int = 3,
54
+ ) -> NDArray[np.float64]:
55
+ """Create an isotropic metric field from scalar sizing values.
56
+
57
+ Parameters
58
+ ----------
59
+ h : float or array_like
60
+ Desired element size(s). If scalar, same size at all vertices.
61
+ If array, must have shape (n_vertices,) or (n_vertices, 1).
62
+ n_vertices : int, optional
63
+ Number of vertices. Required if h is a scalar.
64
+ dim : int, optional
65
+ Mesh dimension (2 or 3). Default is 3.
66
+
67
+ Returns
68
+ -------
69
+ NDArray[np.float64]
70
+ Metric tensor array with shape (n_vertices, n_components) where
71
+ n_components is 6 for 3D and 3 for 2D.
72
+
73
+ Examples
74
+ --------
75
+ >>> metric = create_isotropic_metric(0.1, n_vertices=100, dim=3)
76
+ >>> metric.shape
77
+ (100, 6)
78
+
79
+ >>> sizes = np.linspace(0.1, 0.5, 100)
80
+ >>> metric = create_isotropic_metric(sizes, dim=3)
81
+ >>> metric.shape
82
+ (100, 6)
83
+
84
+ """
85
+ if dim not in (2, 3):
86
+ msg = f"dim must be 2 or 3, got {dim}"
87
+ raise ValueError(msg)
88
+
89
+ h = np.asarray(h, dtype=np.float64)
90
+ if h.ndim == 0:
91
+ if n_vertices is None:
92
+ msg = "n_vertices required when h is a scalar"
93
+ raise ValueError(msg)
94
+ h = np.full(n_vertices, h.item(), dtype=np.float64)
95
+ elif h.ndim == 2 and h.shape[1] == 1:
96
+ h = h.ravel()
97
+
98
+ if h.ndim != 1:
99
+ msg = f"h must be scalar or 1D array, got shape {h.shape}"
100
+ raise ValueError(msg)
101
+
102
+ n_verts = len(h)
103
+
104
+ eigenvalue = 1.0 / (h * h)
105
+
106
+ if dim == 3:
107
+ metric = np.zeros((n_verts, 6), dtype=np.float64)
108
+ metric[:, 0] = eigenvalue # m11
109
+ metric[:, 3] = eigenvalue # m22
110
+ metric[:, 5] = eigenvalue # m33
111
+ else:
112
+ metric = np.zeros((n_verts, 3), dtype=np.float64)
113
+ metric[:, 0] = eigenvalue # m11
114
+ metric[:, 2] = eigenvalue # m22
115
+
116
+ return metric
117
+
118
+
119
+ def create_anisotropic_metric(
120
+ sizes: NDArray[np.float64],
121
+ directions: NDArray[np.float64] | None = None,
122
+ ) -> NDArray[np.float64]:
123
+ """Create an anisotropic metric tensor from principal sizes and directions.
124
+
125
+ The metric tensor M is constructed as: M = R @ D @ R.T
126
+ where D is diagonal with D[i,i] = 1/sizes[i]^2 and R contains the
127
+ principal direction vectors as columns.
128
+
129
+ Parameters
130
+ ----------
131
+ sizes : array_like
132
+ Principal element sizes. Shape (3,) for 3D, (2,) for 2D.
133
+ Can also be (n_vertices, 3) or (n_vertices, 2) for per-vertex sizes.
134
+ directions : array_like, optional
135
+ Principal direction vectors. Shape (3, 3) or (2, 2) for single metric,
136
+ or (n_vertices, 3, 3) or (n_vertices, 2, 2) for per-vertex directions.
137
+ Columns are eigenvectors. If None, uses identity (coordinate-aligned).
138
+
139
+ Returns
140
+ -------
141
+ NDArray[np.float64]
142
+ Metric tensor(s). Shape (6,) for single 3D metric, (3,) for single 2D,
143
+ or (n_vertices, 6) / (n_vertices, 3) for per-vertex metrics.
144
+
145
+ Examples
146
+ --------
147
+ Create a metric with 10x stretch in x-direction:
148
+
149
+ >>> sizes = np.array([0.1, 1.0, 1.0]) # Small in x, large in y,z
150
+ >>> metric = create_anisotropic_metric(sizes)
151
+ >>> metric
152
+ array([100., 0., 0., 1., 0., 1.])
153
+
154
+ Create a rotated metric:
155
+
156
+ >>> import numpy as np
157
+ >>> theta = np.pi / 4 # 45 degrees
158
+ >>> R = np.array([[np.cos(theta), -np.sin(theta), 0],
159
+ ... [np.sin(theta), np.cos(theta), 0],
160
+ ... [0, 0, 1]])
161
+ >>> sizes = np.array([0.1, 1.0, 1.0])
162
+ >>> metric = create_anisotropic_metric(sizes, R)
163
+
164
+ """
165
+ sizes = np.asarray(sizes, dtype=np.float64)
166
+
167
+ if sizes.ndim == 1:
168
+ dim = len(sizes)
169
+ single_metric = True
170
+ sizes = sizes.reshape(1, dim)
171
+ n_vertices = 1
172
+ elif sizes.ndim == 2:
173
+ n_vertices, dim = sizes.shape
174
+ single_metric = False
175
+ else:
176
+ msg = f"sizes must be 1D or 2D array, got shape {sizes.shape}"
177
+ raise ValueError(msg)
178
+
179
+ if dim not in (2, 3):
180
+ msg = f"sizes must have 2 or 3 components, got {dim}"
181
+ raise ValueError(msg)
182
+
183
+ if directions is None:
184
+ directions = np.eye(dim, dtype=np.float64)
185
+ if not single_metric:
186
+ directions = np.broadcast_to(directions, (n_vertices, dim, dim)).copy()
187
+
188
+ directions = np.asarray(directions, dtype=np.float64)
189
+
190
+ if single_metric and directions.ndim == 2:
191
+ directions = directions.reshape(1, dim, dim)
192
+
193
+ if directions.shape[-2:] != (dim, dim):
194
+ msg = f"directions must have shape (..., {dim}, {dim}), got {directions.shape}"
195
+ raise ValueError(msg)
196
+
197
+ eigenvalues = 1.0 / (sizes * sizes)
198
+ D = np.zeros((n_vertices, dim, dim), dtype=np.float64)
199
+ for i in range(dim):
200
+ D[:, i, i] = eigenvalues[:, i]
201
+
202
+ M = np.einsum("...ij,...jk,...lk->...il", directions, D, directions)
203
+
204
+ if dim == 3:
205
+ metric = np.zeros((n_vertices, 6), dtype=np.float64)
206
+ metric[:, 0] = M[:, 0, 0] # m11
207
+ metric[:, 1] = M[:, 0, 1] # m12
208
+ metric[:, 2] = M[:, 0, 2] # m13
209
+ metric[:, 3] = M[:, 1, 1] # m22
210
+ metric[:, 4] = M[:, 1, 2] # m23
211
+ metric[:, 5] = M[:, 2, 2] # m33
212
+ else:
213
+ metric = np.zeros((n_vertices, 3), dtype=np.float64)
214
+ metric[:, 0] = M[:, 0, 0] # m11
215
+ metric[:, 1] = M[:, 0, 1] # m12
216
+ metric[:, 2] = M[:, 1, 1] # m22
217
+
218
+ if single_metric:
219
+ return metric[0]
220
+ return metric
221
+
222
+
223
+ def tensor_to_matrix(
224
+ tensor: NDArray[np.float64],
225
+ dim: int | None = None,
226
+ ) -> NDArray[np.float64]:
227
+ """Convert tensor storage format to full symmetric matrix.
228
+
229
+ Parameters
230
+ ----------
231
+ tensor : array_like
232
+ Tensor in storage format. Shape (6,) or (n, 6) for 3D,
233
+ (3,) or (n, 3) for 2D.
234
+ dim : int, optional
235
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
236
+
237
+ Returns
238
+ -------
239
+ NDArray[np.float64]
240
+ Full symmetric matrix. Shape (3, 3) or (n, 3, 3) for 3D,
241
+ (2, 2) or (n, 2, 2) for 2D.
242
+
243
+ """
244
+ tensor = np.asarray(tensor, dtype=np.float64)
245
+ single = tensor.ndim == 1
246
+ if single:
247
+ tensor = tensor.reshape(1, -1)
248
+
249
+ n_components = tensor.shape[1]
250
+ if dim is None:
251
+ dim = 3 if n_components == 6 else 2
252
+
253
+ n = tensor.shape[0]
254
+ M = np.zeros((n, dim, dim), dtype=np.float64)
255
+
256
+ if dim == 3:
257
+ M[:, 0, 0] = tensor[:, 0] # m11
258
+ M[:, 0, 1] = tensor[:, 1] # m12
259
+ M[:, 0, 2] = tensor[:, 2] # m13
260
+ M[:, 1, 0] = tensor[:, 1] # m21 = m12
261
+ M[:, 1, 1] = tensor[:, 3] # m22
262
+ M[:, 1, 2] = tensor[:, 4] # m23
263
+ M[:, 2, 0] = tensor[:, 2] # m31 = m13
264
+ M[:, 2, 1] = tensor[:, 4] # m32 = m23
265
+ M[:, 2, 2] = tensor[:, 5] # m33
266
+ else:
267
+ M[:, 0, 0] = tensor[:, 0] # m11
268
+ M[:, 0, 1] = tensor[:, 1] # m12
269
+ M[:, 1, 0] = tensor[:, 1] # m21 = m12
270
+ M[:, 1, 1] = tensor[:, 2] # m22
271
+
272
+ if single:
273
+ return M[0]
274
+ return M
275
+
276
+
277
+ def matrix_to_tensor(
278
+ M: NDArray[np.float64],
279
+ ) -> NDArray[np.float64]:
280
+ """Convert full symmetric matrix to tensor storage format.
281
+
282
+ Parameters
283
+ ----------
284
+ M : array_like
285
+ Full symmetric matrix. Shape (3, 3) or (n, 3, 3) for 3D,
286
+ (2, 2) or (n, 2, 2) for 2D.
287
+
288
+ Returns
289
+ -------
290
+ NDArray[np.float64]
291
+ Tensor in storage format. Shape (6,) or (n, 6) for 3D,
292
+ (3,) or (n, 3) for 2D.
293
+
294
+ """
295
+ M = np.asarray(M, dtype=np.float64)
296
+ single = M.ndim == 2
297
+ if single:
298
+ M = M.reshape(1, *M.shape)
299
+
300
+ dim = M.shape[1]
301
+ n = M.shape[0]
302
+
303
+ if dim == 3:
304
+ tensor = np.zeros((n, 6), dtype=np.float64)
305
+ tensor[:, 0] = M[:, 0, 0] # m11
306
+ tensor[:, 1] = M[:, 0, 1] # m12
307
+ tensor[:, 2] = M[:, 0, 2] # m13
308
+ tensor[:, 3] = M[:, 1, 1] # m22
309
+ tensor[:, 4] = M[:, 1, 2] # m23
310
+ tensor[:, 5] = M[:, 2, 2] # m33
311
+ else:
312
+ tensor = np.zeros((n, 3), dtype=np.float64)
313
+ tensor[:, 0] = M[:, 0, 0] # m11
314
+ tensor[:, 1] = M[:, 0, 1] # m12
315
+ tensor[:, 2] = M[:, 1, 1] # m22
316
+
317
+ if single:
318
+ return tensor[0]
319
+ return tensor
320
+
321
+
322
+ def validate_metric_tensor(
323
+ tensor: NDArray[np.float64],
324
+ dim: int | None = None,
325
+ *,
326
+ raise_on_invalid: bool = True,
327
+ ) -> tuple[bool, str]:
328
+ """Validate that metric tensor(s) are positive-definite.
329
+
330
+ A valid metric tensor must be symmetric positive-definite, meaning
331
+ all eigenvalues must be strictly positive.
332
+
333
+ Parameters
334
+ ----------
335
+ tensor : array_like
336
+ Tensor(s) to validate. Shape (6,) or (n, 6) for 3D,
337
+ (3,) or (n, 3) for 2D.
338
+ dim : int, optional
339
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
340
+ raise_on_invalid : bool, optional
341
+ If True, raises ValueError on invalid tensors. Default is True.
342
+
343
+ Returns
344
+ -------
345
+ tuple[bool, str]
346
+ (is_valid, message) tuple.
347
+
348
+ Raises
349
+ ------
350
+ ValueError
351
+ If raise_on_invalid is True and tensor is not valid.
352
+
353
+ Examples
354
+ --------
355
+ >>> valid_tensor = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 1.0])
356
+ >>> validate_metric_tensor(valid_tensor)
357
+ (True, 'Valid positive-definite metric tensor')
358
+
359
+ >>> invalid_tensor = np.array([-1.0, 0.0, 0.0, 1.0, 0.0, 1.0])
360
+ >>> validate_metric_tensor(invalid_tensor, raise_on_invalid=False)
361
+ (False, 'Tensor has non-positive eigenvalues...')
362
+
363
+ """
364
+ tensor = np.asarray(tensor, dtype=np.float64)
365
+ M = tensor_to_matrix(tensor, dim)
366
+
367
+ single = M.ndim == 2
368
+ if single:
369
+ M = M.reshape(1, *M.shape)
370
+
371
+ all_valid = True
372
+ messages = []
373
+
374
+ for i, m in enumerate(M):
375
+ eigvals = np.linalg.eigvalsh(m)
376
+
377
+ if not np.all(eigvals > 0):
378
+ all_valid = False
379
+ prefix = f"Tensor {i}: " if not single else ""
380
+ messages.append(
381
+ f"{prefix}Has non-positive eigenvalues: {eigvals}. "
382
+ "Metric tensors must be positive-definite.",
383
+ )
384
+
385
+ if all_valid:
386
+ msg = "Valid positive-definite metric tensor"
387
+ if not single:
388
+ msg += "s"
389
+ return (True, msg)
390
+
391
+ error_msg = "; ".join(messages)
392
+ if raise_on_invalid:
393
+ raise ValueError(error_msg)
394
+ return (False, error_msg)
395
+
396
+
397
+ def compute_metric_eigenpairs(
398
+ tensor: NDArray[np.float64],
399
+ dim: int | None = None,
400
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
401
+ """Extract principal sizes and directions from metric tensor(s).
402
+
403
+ Parameters
404
+ ----------
405
+ tensor : array_like
406
+ Metric tensor(s). Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.
407
+ dim : int, optional
408
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
409
+
410
+ Returns
411
+ -------
412
+ tuple[NDArray, NDArray]
413
+ (sizes, directions) where:
414
+ - sizes: Principal element sizes, shape (3,) or (n, 3) for 3D
415
+ - directions: Eigenvector matrices, shape (3, 3) or (n, 3, 3) for 3D
416
+ Columns are eigenvectors corresponding to sizes.
417
+
418
+ Examples
419
+ --------
420
+ >>> tensor = np.array([100., 0., 0., 1., 0., 1.]) # 10x stretch in x
421
+ >>> sizes, directions = compute_metric_eigenpairs(tensor)
422
+ >>> sizes
423
+ array([0.1, 1. , 1. ])
424
+
425
+ """
426
+ M = tensor_to_matrix(tensor, dim)
427
+
428
+ single = M.ndim == 2
429
+ if single:
430
+ M = M.reshape(1, *M.shape)
431
+
432
+ n = M.shape[0]
433
+ actual_dim = M.shape[1]
434
+
435
+ sizes = np.zeros((n, actual_dim), dtype=np.float64)
436
+ directions = np.zeros((n, actual_dim, actual_dim), dtype=np.float64)
437
+
438
+ for i, m in enumerate(M):
439
+ eigvals, eigvecs = np.linalg.eigh(m)
440
+ sizes[i] = 1.0 / np.sqrt(eigvals)
441
+ directions[i] = eigvecs
442
+
443
+ if single:
444
+ return sizes[0], directions[0]
445
+ return sizes, directions
446
+
447
+
448
+ def intersect_metrics(
449
+ m1: NDArray[np.float64],
450
+ m2: NDArray[np.float64],
451
+ dim: int | None = None,
452
+ ) -> NDArray[np.float64]:
453
+ """Compute the intersection of two metric tensors.
454
+
455
+ The intersection produces a metric that is at least as refined as both
456
+ input metrics in all directions. This is useful for combining metrics
457
+ from different sources (e.g., boundary layer + feature-based).
458
+
459
+ The intersection is computed via simultaneous diagonalization:
460
+ M_intersect = M1^(1/2) @ N @ M1^(1/2)
461
+ where N is diagonal with max eigenvalues of M1^(-1/2) @ M2 @ M1^(-1/2).
462
+
463
+ Parameters
464
+ ----------
465
+ m1, m2 : array_like
466
+ Metric tensors to intersect. Must have same shape.
467
+ Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.
468
+ dim : int, optional
469
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
470
+
471
+ Returns
472
+ -------
473
+ NDArray[np.float64]
474
+ Intersected metric tensor(s), same shape as inputs.
475
+
476
+ """
477
+ m1 = np.asarray(m1, dtype=np.float64)
478
+ m2 = np.asarray(m2, dtype=np.float64)
479
+
480
+ if m1.shape != m2.shape:
481
+ msg = f"Metrics must have same shape: {m1.shape} vs {m2.shape}"
482
+ raise ValueError(msg)
483
+
484
+ M1 = tensor_to_matrix(m1, dim)
485
+ M2 = tensor_to_matrix(m2, dim)
486
+
487
+ single = M1.ndim == 2
488
+ if single:
489
+ M1 = M1.reshape(1, *M1.shape)
490
+ M2 = M2.reshape(1, *M2.shape)
491
+
492
+ n = M1.shape[0]
493
+ M_intersect = np.zeros_like(M1)
494
+
495
+ for i in range(n):
496
+ eigvals1, eigvecs1 = np.linalg.eigh(M1[i])
497
+
498
+ # Guard against near-singular metrics (eigenvalues close to zero)
499
+ # Use machine epsilon scaled by max eigenvalue for numerical stability
500
+ eps = np.finfo(np.float64).eps * np.max(np.abs(eigvals1)) * 100
501
+ eigvals1 = np.maximum(eigvals1, eps)
502
+
503
+ sqrt_eigvals1 = np.sqrt(eigvals1)
504
+ inv_sqrt_eigvals1 = 1.0 / sqrt_eigvals1
505
+
506
+ M1_sqrt = eigvecs1 @ np.diag(sqrt_eigvals1) @ eigvecs1.T
507
+ M1_inv_sqrt = eigvecs1 @ np.diag(inv_sqrt_eigvals1) @ eigvecs1.T
508
+
509
+ P = M1_inv_sqrt @ M2[i] @ M1_inv_sqrt
510
+
511
+ eigvals_P, eigvecs_P = np.linalg.eigh(P)
512
+
513
+ max_eigvals = np.maximum(eigvals_P, 1.0)
514
+
515
+ M_intersect[i] = (
516
+ M1_sqrt @ eigvecs_P @ np.diag(max_eigvals) @ eigvecs_P.T @ M1_sqrt
517
+ )
518
+
519
+ return matrix_to_tensor(M_intersect if not single else M_intersect[0])
520
+
521
+
522
+ def create_metric_from_hessian(
523
+ hessian: NDArray[np.float64],
524
+ target_error: float = 1e-3,
525
+ hmin: float | None = None,
526
+ hmax: float | None = None,
527
+ ) -> NDArray[np.float64]:
528
+ """Create metric tensor from Hessian matrix for interpolation error control.
529
+
530
+ Given a Hessian H of a solution field, constructs a metric M such that
531
+ the interpolation error is bounded by target_error. This is used for
532
+ solution-adaptive mesh refinement.
533
+
534
+ The metric eigenvalues are: lambda_i = c * |hessian_eigenvalue_i| / target_error
535
+ where c is a constant depending on the interpolation order.
536
+
537
+ Parameters
538
+ ----------
539
+ hessian : array_like
540
+ Hessian tensor(s). Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.
541
+ Components: [H11, H12, H13, H22, H23, H33] for 3D.
542
+ target_error : float, optional
543
+ Target interpolation error. Default is 1e-3.
544
+ hmin : float, optional
545
+ Minimum element size. Limits maximum metric eigenvalues.
546
+ hmax : float, optional
547
+ Maximum element size. Limits minimum metric eigenvalues.
548
+
549
+ Returns
550
+ -------
551
+ NDArray[np.float64]
552
+ Metric tensor(s) for adaptive remeshing.
553
+
554
+ Notes
555
+ -----
556
+ For P1 interpolation, the interpolation error is bounded by:
557
+ e <= (1/8) * h^2 * |d²u/ds²|_max
558
+
559
+ This function computes the metric that achieves a specified error bound.
560
+
561
+ """
562
+ hessian = np.asarray(hessian, dtype=np.float64)
563
+ H = tensor_to_matrix(hessian)
564
+
565
+ single = H.ndim == 2
566
+ if single:
567
+ H = H.reshape(1, *H.shape)
568
+
569
+ n = H.shape[0]
570
+ M = np.zeros_like(H)
571
+
572
+ c = 1.0 / 8.0
573
+
574
+ for i in range(n):
575
+ eigvals, eigvecs = np.linalg.eigh(H[i])
576
+ abs_eigvals = np.abs(eigvals)
577
+
578
+ metric_eigvals = c * abs_eigvals / target_error
579
+
580
+ # Floor eigenvalues to avoid singular metrics when Hessian is near-zero.
581
+ # 1e-12 corresponds to element sizes up to ~1e6, sufficient for most meshes.
582
+ # This is applied before hmin/hmax bounds which may further constrain values.
583
+ eps = 1e-12
584
+ metric_eigvals = np.maximum(metric_eigvals, eps)
585
+
586
+ if hmin is not None:
587
+ max_eigval = 1.0 / (hmin * hmin)
588
+ metric_eigvals = np.minimum(metric_eigvals, max_eigval)
589
+
590
+ if hmax is not None:
591
+ min_eigval = 1.0 / (hmax * hmax)
592
+ metric_eigvals = np.maximum(metric_eigvals, min_eigval)
593
+
594
+ M[i] = eigvecs @ np.diag(metric_eigvals) @ eigvecs.T
595
+
596
+ return matrix_to_tensor(M if not single else M[0])
mmgpy/progress.py ADDED
@@ -0,0 +1,69 @@
1
+ """Progress tracking utilities for mmgpy.
2
+
3
+ This module provides progress callbacks and Rich integration for monitoring
4
+ mesh operations like remeshing, with support for cancellation.
5
+
6
+ Examples
7
+ --------
8
+ Basic usage with logging:
9
+
10
+ >>> from mmgpy import MmgMesh3D
11
+ >>> from mmgpy.progress import LoggingProgressReporter, remesh_mesh
12
+ >>> mesh = MmgMesh3D(vertices, elements)
13
+ >>> reporter = LoggingProgressReporter()
14
+ >>> remesh_mesh(mesh, progress=reporter, hmax=0.1)
15
+
16
+ Using Rich progress display:
17
+
18
+ >>> from mmgpy import MmgMesh3D
19
+ >>> from mmgpy.progress import rich_progress, remesh_mesh
20
+ >>> mesh = MmgMesh3D(vertices, elements)
21
+ >>> with rich_progress() as callback:
22
+ ... remesh_mesh(mesh, progress=callback, hmax=0.1)
23
+
24
+ Creating a custom callback with cancellation support:
25
+
26
+ >>> from mmgpy.progress import ProgressEvent, CancellationError
27
+ >>> def my_callback(event: ProgressEvent) -> bool:
28
+ ... print(f"{event.phase}: {event.progress_percent:.0f}% - {event.message}")
29
+ ... return True # Return False to cancel
30
+ >>> # If callback returns False, CancellationError is raised
31
+
32
+ Cancellation example:
33
+
34
+ >>> import threading
35
+ >>> from mmgpy.progress import CancellationError, remesh_mesh
36
+ >>> cancel_flag = threading.Event()
37
+ >>> def check_cancel(event: ProgressEvent) -> bool:
38
+ ... return not cancel_flag.is_set() # Return False when cancelled
39
+ >>> # In another thread: cancel_flag.set()
40
+
41
+ """
42
+
43
+ from ._progress import (
44
+ CancellationError,
45
+ LoggingProgressReporter,
46
+ ProgressEvent,
47
+ ProgressReporter,
48
+ RichProgressReporter,
49
+ remesh_2d,
50
+ remesh_3d,
51
+ remesh_mesh,
52
+ remesh_mesh_lagrangian,
53
+ remesh_surface,
54
+ rich_progress,
55
+ )
56
+
57
+ __all__ = [
58
+ "CancellationError",
59
+ "LoggingProgressReporter",
60
+ "ProgressEvent",
61
+ "ProgressReporter",
62
+ "RichProgressReporter",
63
+ "remesh_2d",
64
+ "remesh_3d",
65
+ "remesh_mesh",
66
+ "remesh_mesh_lagrangian",
67
+ "remesh_surface",
68
+ "rich_progress",
69
+ ]
mmgpy/py.typed ADDED
File without changes
@@ -0,0 +1,37 @@
1
+ """Mesh repair utilities module for mmgpy.
2
+
3
+ This module provides utilities for repairing common mesh issues including
4
+ duplicate vertices, orphan vertices, degenerate elements, and inverted elements.
5
+
6
+ Example:
7
+ >>> from mmgpy import Mesh
8
+ >>> from mmgpy.repair import auto_repair, remove_duplicate_vertices
9
+ >>>
10
+ >>> mesh = Mesh(vertices, cells)
11
+ >>> mesh, report = auto_repair(mesh)
12
+ >>> print(report)
13
+
14
+ """
15
+
16
+ from mmgpy.repair._core import RepairReport, auto_repair
17
+ from mmgpy.repair._elements import (
18
+ fix_inverted_elements,
19
+ remove_degenerate_elements,
20
+ remove_duplicate_elements,
21
+ )
22
+ from mmgpy.repair._vertices import (
23
+ merge_close_vertices,
24
+ remove_duplicate_vertices,
25
+ remove_orphan_vertices,
26
+ )
27
+
28
+ __all__ = [
29
+ "RepairReport",
30
+ "auto_repair",
31
+ "fix_inverted_elements",
32
+ "merge_close_vertices",
33
+ "remove_degenerate_elements",
34
+ "remove_duplicate_elements",
35
+ "remove_duplicate_vertices",
36
+ "remove_orphan_vertices",
37
+ ]