mmgpy 0.2.0__cp310-cp310-win_amd64.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 (129) hide show
  1. bin/__init__.py +10 -0
  2. bin/concrt140.dll +0 -0
  3. bin/mmg.dll +0 -0
  4. bin/mmg2d.dll +0 -0
  5. bin/mmg2d_O3.exe +0 -0
  6. bin/mmg3d.dll +0 -0
  7. bin/mmg3d_O3.exe +0 -0
  8. bin/mmgs.dll +0 -0
  9. bin/mmgs_O3.exe +0 -0
  10. bin/msvcp140.dll +0 -0
  11. bin/msvcp140_1.dll +0 -0
  12. bin/msvcp140_2.dll +0 -0
  13. bin/msvcp140_atomic_wait.dll +0 -0
  14. bin/msvcp140_codecvt_ids.dll +0 -0
  15. bin/vcruntime140.dll +0 -0
  16. bin/vcruntime140_1.dll +0 -0
  17. include/__init__.py +10 -0
  18. include/mmg/common/libmmgtypes.h +687 -0
  19. include/mmg/common/libmmgtypesf.h +762 -0
  20. include/mmg/common/mmg_export.h +47 -0
  21. include/mmg/common/mmgcmakedefines.h +46 -0
  22. include/mmg/common/mmgcmakedefinesf.h +29 -0
  23. include/mmg/common/mmgversion.h +54 -0
  24. include/mmg/libmmg.h +67 -0
  25. include/mmg/libmmgf.h +42 -0
  26. include/mmg/mmg2d/libmmg2d.h +2761 -0
  27. include/mmg/mmg2d/libmmg2df.h +3263 -0
  28. include/mmg/mmg2d/mmg2d_export.h +34 -0
  29. include/mmg/mmg3d/libmmg3d.h +3444 -0
  30. include/mmg/mmg3d/libmmg3df.h +4041 -0
  31. include/mmg/mmg3d/mmg3d_export.h +34 -0
  32. include/mmg/mmgs/libmmgs.h +2560 -0
  33. include/mmg/mmgs/libmmgsf.h +3028 -0
  34. include/mmg/mmgs/mmgs_export.h +34 -0
  35. lib/__init__.py +10 -0
  36. lib/cmake/mmg/FindElas.cmake +57 -0
  37. lib/cmake/mmg/FindSCOTCH.cmake +373 -0
  38. lib/cmake/mmg/MmgTargets-release.cmake +53 -0
  39. lib/cmake/mmg/MmgTargets.cmake +127 -0
  40. lib/cmake/mmg/mmgConfig.cmake +43 -0
  41. lib/mmg.lib +0 -0
  42. lib/mmg2d.lib +0 -0
  43. lib/mmg3d.lib +0 -0
  44. lib/mmgs.lib +0 -0
  45. mmgpy/__init__.py +888 -0
  46. mmgpy/_logging.py +86 -0
  47. mmgpy/_mmgpy.cp310-win_amd64.pyd +0 -0
  48. mmgpy/_mmgpy.pyi +650 -0
  49. mmgpy/_options.py +304 -0
  50. mmgpy/_progress.py +539 -0
  51. mmgpy/_pyvista.py +423 -0
  52. mmgpy/_version.py +3 -0
  53. mmgpy/_version.py.in +3 -0
  54. mmgpy/lagrangian.py +394 -0
  55. mmgpy/metrics.py +595 -0
  56. mmgpy/mmg2d.dll +0 -0
  57. mmgpy/mmg2d.lib +0 -0
  58. mmgpy/mmg3d.dll +0 -0
  59. mmgpy/mmg3d.lib +0 -0
  60. mmgpy/mmgs.dll +0 -0
  61. mmgpy/mmgs.lib +0 -0
  62. mmgpy/progress.py +57 -0
  63. mmgpy/py.typed +0 -0
  64. mmgpy/sizing.py +370 -0
  65. mmgpy-0.2.0.dist-info/DELVEWHEEL +2 -0
  66. mmgpy-0.2.0.dist-info/METADATA +75 -0
  67. mmgpy-0.2.0.dist-info/RECORD +129 -0
  68. mmgpy-0.2.0.dist-info/WHEEL +5 -0
  69. mmgpy-0.2.0.dist-info/entry_points.txt +6 -0
  70. mmgpy-0.2.0.dist-info/licenses/LICENSE +38 -0
  71. mmgpy.libs/vtkCommonColor-9.4-799ae0f43eb3a04510b0ed500c05e895.dll +0 -0
  72. mmgpy.libs/vtkCommonComputationalGeometry-9.4-e1ee47e9ca84c220e3fda21c864610e9.dll +0 -0
  73. mmgpy.libs/vtkCommonCore-9.4.dll +0 -0
  74. mmgpy.libs/vtkCommonDataModel-9.4.dll +0 -0
  75. mmgpy.libs/vtkCommonExecutionModel-9.4-5fea279cddbd0dd8e39907cbfb423d2c.dll +0 -0
  76. mmgpy.libs/vtkCommonMath-9.4-70aba6f1b0ad06008b5990b6843ff4e9.dll +0 -0
  77. mmgpy.libs/vtkCommonMisc-9.4-96697cc413673520510a54c05fa0ef99.dll +0 -0
  78. mmgpy.libs/vtkCommonSystem-9.4-a748ca3c31712678e4e0f2ff3b09785b.dll +0 -0
  79. mmgpy.libs/vtkCommonTransforms-9.4-11e72e99aca2003a38a3ff9bcfb90d92.dll +0 -0
  80. mmgpy.libs/vtkDICOMParser-9.4-6f27d14c12768aac52622cbd28ca0b3a.dll +0 -0
  81. mmgpy.libs/vtkFiltersCellGrid-9.4-5675f4c4a8440bd5f5981149f99315e8.dll +0 -0
  82. mmgpy.libs/vtkFiltersCore-9.4-ef29a82b399f8ffeec5dfa8c7e010901.dll +0 -0
  83. mmgpy.libs/vtkFiltersExtraction-9.4-6a452db7095d68b3af501d1b39a05670.dll +0 -0
  84. mmgpy.libs/vtkFiltersGeneral-9.4-b9b071ef41cba7c6468dd7e0a9f5d4bf.dll +0 -0
  85. mmgpy.libs/vtkFiltersGeometry-9.4-757f3c8b9a1fd9774cce83aec0c94c55.dll +0 -0
  86. mmgpy.libs/vtkFiltersHybrid-9.4-6b343077b9da30bb95fc4492762b9bed.dll +0 -0
  87. mmgpy.libs/vtkFiltersHyperTree-9.4-831d7e88dd11a3faec876929089d21e8.dll +0 -0
  88. mmgpy.libs/vtkFiltersModeling-9.4-a3fc680a4836e8913376390217ceae1e.dll +0 -0
  89. mmgpy.libs/vtkFiltersParallel-9.4-5307d8b619459bb2aeb13c2872cebc2b.dll +0 -0
  90. mmgpy.libs/vtkFiltersReduction-9.4-f7abe86bfcde726b6e7daec4773a089f.dll +0 -0
  91. mmgpy.libs/vtkFiltersSources-9.4-d38546a5e2bfd83ea0a70f5be4fa7fdc.dll +0 -0
  92. mmgpy.libs/vtkFiltersStatistics-9.4-d2abec2372ab98574cdbd0461c73d459.dll +0 -0
  93. mmgpy.libs/vtkFiltersTexture-9.4-a374b3cfe75e10483976a6a6780ca1ca.dll +0 -0
  94. mmgpy.libs/vtkFiltersVerdict-9.4-a6a276b6e0d3ac6d042f192842c9382a.dll +0 -0
  95. mmgpy.libs/vtkIOCellGrid-9.4-0df3bee5d56b2062dad430b53f81f06a.dll +0 -0
  96. mmgpy.libs/vtkIOCore-9.4.dll +0 -0
  97. mmgpy.libs/vtkIOGeometry-9.4-13e2301d83eea31e8df955a8dd196eb1.dll +0 -0
  98. mmgpy.libs/vtkIOImage-9.4-666beab0c43da6c4b2c59fba464bb6b1.dll +0 -0
  99. mmgpy.libs/vtkIOLegacy-9.4.dll +0 -0
  100. mmgpy.libs/vtkIOParallel-9.4.dll +0 -0
  101. mmgpy.libs/vtkIOParallelXML-9.4.dll +0 -0
  102. mmgpy.libs/vtkIOXML-9.4.dll +0 -0
  103. mmgpy.libs/vtkIOXMLParser-9.4-f4144aed6a73ee50eefeae75a78fd71b.dll +0 -0
  104. mmgpy.libs/vtkImagingCore-9.4-ecc92d2d09d8ac4e8f3b199a917f96fd.dll +0 -0
  105. mmgpy.libs/vtkImagingSources-9.4-944fcaf71940d51ed5dc1b5c8eba2571.dll +0 -0
  106. mmgpy.libs/vtkParallelCore-9.4-114ce4e5a12302a9f3c3485ab88d5a39.dll +0 -0
  107. mmgpy.libs/vtkParallelDIY-9.4-7add130df7f5139505a17ee72afefa41.dll +0 -0
  108. mmgpy.libs/vtkRenderingCore-9.4-ec5612bc182f43047add4c6a55050be4.dll +0 -0
  109. mmgpy.libs/vtkdoubleconversion-9.4-676429e05704a1fd81323494aba16669.dll +0 -0
  110. mmgpy.libs/vtkexpat-9.4-58e3b6d3064cf0e02659e3a57f60f779.dll +0 -0
  111. mmgpy.libs/vtkfmt-9.4-fd2ae0aaa19f2f735b93d4feb613d558.dll +0 -0
  112. mmgpy.libs/vtkjpeg-9.4-2fd58669e746a0d571706ff3f4c1896f.dll +0 -0
  113. mmgpy.libs/vtkjsoncpp-9.4-c2076501decec6ce45be5280d480c02d.dll +0 -0
  114. mmgpy.libs/vtkkissfft-9.4-874ed77ec96808d2e395a6caeb69abbe.dll +0 -0
  115. mmgpy.libs/vtkloguru-9.4-1038f41dd2d3acc7d894c1d57b428cc6.dll +0 -0
  116. mmgpy.libs/vtklz4-9.4-9982d6f8f47ac19965668cd3c2926473.dll +0 -0
  117. mmgpy.libs/vtklzma-9.4-a473d065be12f766957c699a0aefac38.dll +0 -0
  118. mmgpy.libs/vtkmetaio-9.4-aa48920e94a28f23d08054f5e82867f4.dll +0 -0
  119. mmgpy.libs/vtkpng-9.4-cbefdfbef5c0d4dd7ae43b9d526e881a.dll +0 -0
  120. mmgpy.libs/vtkpugixml-9.4-9c7652d9a42fd99e62753e7d3fd87ee9.dll +0 -0
  121. mmgpy.libs/vtksys-9.4.dll +0 -0
  122. mmgpy.libs/vtktiff-9.4-9822dcf52447c045cf9a70960d45a9bd.dll +0 -0
  123. mmgpy.libs/vtktoken-9.4-471c14923651cbbd86067852f347807b.dll +0 -0
  124. mmgpy.libs/vtkverdict-9.4-c549882a3b8671666be1366008f27f7e.dll +0 -0
  125. mmgpy.libs/vtkzlib-9.4-3b9ffa51c80ccd91bb22a4c3ca11a8d7.dll +0 -0
  126. share/__init__.py +10 -0
  127. share/man/man1/mmg2d.1.gz +0 -0
  128. share/man/man1/mmg3d.1.gz +0 -0
  129. share/man/man1/mmgs.1.gz +0 -0
mmgpy/metrics.py ADDED
@@ -0,0 +1,595 @@
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 principal directions
9
+
10
+ Tensor storage format (row-major, upper triangular):
11
+ - **3D**: [m11, m12, m13, m22, m23, m33] (6 components)
12
+ - **2D**: [m11, m12, m22] (3 components)
13
+
14
+ The metric eigenvalues encode element sizes: size_i = 1 / sqrt(eigenvalue_i)
15
+
16
+ Examples
17
+ --------
18
+ Create an isotropic metric with uniform size h=0.1:
19
+
20
+ >>> import numpy as np
21
+ >>> from mmgpy.metrics import create_isotropic_metric
22
+ >>> n_vertices = 100
23
+ >>> h = 0.1
24
+ >>> metric = create_isotropic_metric(h, n_vertices, dim=3)
25
+ >>> metric.shape
26
+ (100, 6)
27
+
28
+ Create an anisotropic metric with different sizes in each direction:
29
+
30
+ >>> from mmgpy.metrics import create_anisotropic_metric
31
+ >>> sizes = np.array([0.1, 0.5, 0.1]) # Small in x,z, large in y
32
+ >>> directions = np.eye(3) # Aligned with coordinate axes
33
+ >>> metric = create_anisotropic_metric(sizes, directions)
34
+ >>> metric.shape
35
+ (6,)
36
+
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from typing import TYPE_CHECKING
42
+
43
+ import numpy as np
44
+
45
+ if TYPE_CHECKING:
46
+ from numpy.typing import NDArray
47
+
48
+
49
+ def create_isotropic_metric(
50
+ h: float | NDArray[np.float64],
51
+ n_vertices: int | None = None,
52
+ dim: int = 3,
53
+ ) -> NDArray[np.float64]:
54
+ """Create an isotropic metric field from scalar sizing values.
55
+
56
+ Parameters
57
+ ----------
58
+ h : float or array_like
59
+ Desired element size(s). If scalar, same size at all vertices.
60
+ If array, must have shape (n_vertices,) or (n_vertices, 1).
61
+ n_vertices : int, optional
62
+ Number of vertices. Required if h is a scalar.
63
+ dim : int, optional
64
+ Mesh dimension (2 or 3). Default is 3.
65
+
66
+ Returns
67
+ -------
68
+ NDArray[np.float64]
69
+ Metric tensor array with shape (n_vertices, n_components) where
70
+ n_components is 6 for 3D and 3 for 2D.
71
+
72
+ Examples
73
+ --------
74
+ >>> metric = create_isotropic_metric(0.1, n_vertices=100, dim=3)
75
+ >>> metric.shape
76
+ (100, 6)
77
+
78
+ >>> sizes = np.linspace(0.1, 0.5, 100)
79
+ >>> metric = create_isotropic_metric(sizes, dim=3)
80
+ >>> metric.shape
81
+ (100, 6)
82
+
83
+ """
84
+ if dim not in (2, 3):
85
+ msg = f"dim must be 2 or 3, got {dim}"
86
+ raise ValueError(msg)
87
+
88
+ h = np.asarray(h, dtype=np.float64)
89
+ if h.ndim == 0:
90
+ if n_vertices is None:
91
+ msg = "n_vertices required when h is a scalar"
92
+ raise ValueError(msg)
93
+ h = np.full(n_vertices, h.item(), dtype=np.float64)
94
+ elif h.ndim == 2 and h.shape[1] == 1:
95
+ h = h.ravel()
96
+
97
+ if h.ndim != 1:
98
+ msg = f"h must be scalar or 1D array, got shape {h.shape}"
99
+ raise ValueError(msg)
100
+
101
+ n_verts = len(h)
102
+
103
+ eigenvalue = 1.0 / (h * h)
104
+
105
+ if dim == 3:
106
+ metric = np.zeros((n_verts, 6), dtype=np.float64)
107
+ metric[:, 0] = eigenvalue # m11
108
+ metric[:, 3] = eigenvalue # m22
109
+ metric[:, 5] = eigenvalue # m33
110
+ else:
111
+ metric = np.zeros((n_verts, 3), dtype=np.float64)
112
+ metric[:, 0] = eigenvalue # m11
113
+ metric[:, 2] = eigenvalue # m22
114
+
115
+ return metric
116
+
117
+
118
+ def create_anisotropic_metric(
119
+ sizes: NDArray[np.float64],
120
+ directions: NDArray[np.float64] | None = None,
121
+ ) -> NDArray[np.float64]:
122
+ """Create an anisotropic metric tensor from principal sizes and directions.
123
+
124
+ The metric tensor M is constructed as: M = R @ D @ R.T
125
+ where D is diagonal with D[i,i] = 1/sizes[i]^2 and R contains the
126
+ principal direction vectors as columns.
127
+
128
+ Parameters
129
+ ----------
130
+ sizes : array_like
131
+ Principal element sizes. Shape (3,) for 3D, (2,) for 2D.
132
+ Can also be (n_vertices, 3) or (n_vertices, 2) for per-vertex sizes.
133
+ directions : array_like, optional
134
+ Principal direction vectors. Shape (3, 3) or (2, 2) for single metric,
135
+ or (n_vertices, 3, 3) or (n_vertices, 2, 2) for per-vertex directions.
136
+ Columns are eigenvectors. If None, uses identity (coordinate-aligned).
137
+
138
+ Returns
139
+ -------
140
+ NDArray[np.float64]
141
+ Metric tensor(s). Shape (6,) for single 3D metric, (3,) for single 2D,
142
+ or (n_vertices, 6) / (n_vertices, 3) for per-vertex metrics.
143
+
144
+ Examples
145
+ --------
146
+ Create a metric with 10x stretch in x-direction:
147
+
148
+ >>> sizes = np.array([0.1, 1.0, 1.0]) # Small in x, large in y,z
149
+ >>> metric = create_anisotropic_metric(sizes)
150
+ >>> metric
151
+ array([100., 0., 0., 1., 0., 1.])
152
+
153
+ Create a rotated metric:
154
+
155
+ >>> import numpy as np
156
+ >>> theta = np.pi / 4 # 45 degrees
157
+ >>> R = np.array([[np.cos(theta), -np.sin(theta), 0],
158
+ ... [np.sin(theta), np.cos(theta), 0],
159
+ ... [0, 0, 1]])
160
+ >>> sizes = np.array([0.1, 1.0, 1.0])
161
+ >>> metric = create_anisotropic_metric(sizes, R)
162
+
163
+ """
164
+ sizes = np.asarray(sizes, dtype=np.float64)
165
+
166
+ if sizes.ndim == 1:
167
+ dim = len(sizes)
168
+ single_metric = True
169
+ sizes = sizes.reshape(1, dim)
170
+ n_vertices = 1
171
+ elif sizes.ndim == 2:
172
+ n_vertices, dim = sizes.shape
173
+ single_metric = False
174
+ else:
175
+ msg = f"sizes must be 1D or 2D array, got shape {sizes.shape}"
176
+ raise ValueError(msg)
177
+
178
+ if dim not in (2, 3):
179
+ msg = f"sizes must have 2 or 3 components, got {dim}"
180
+ raise ValueError(msg)
181
+
182
+ if directions is None:
183
+ directions = np.eye(dim, dtype=np.float64)
184
+ if not single_metric:
185
+ directions = np.broadcast_to(directions, (n_vertices, dim, dim)).copy()
186
+
187
+ directions = np.asarray(directions, dtype=np.float64)
188
+
189
+ if single_metric and directions.ndim == 2:
190
+ directions = directions.reshape(1, dim, dim)
191
+
192
+ if directions.shape[-2:] != (dim, dim):
193
+ msg = f"directions must have shape (..., {dim}, {dim}), got {directions.shape}"
194
+ raise ValueError(msg)
195
+
196
+ eigenvalues = 1.0 / (sizes * sizes)
197
+ D = np.zeros((n_vertices, dim, dim), dtype=np.float64)
198
+ for i in range(dim):
199
+ D[:, i, i] = eigenvalues[:, i]
200
+
201
+ M = np.einsum("...ij,...jk,...lk->...il", directions, D, directions)
202
+
203
+ if dim == 3:
204
+ metric = np.zeros((n_vertices, 6), dtype=np.float64)
205
+ metric[:, 0] = M[:, 0, 0] # m11
206
+ metric[:, 1] = M[:, 0, 1] # m12
207
+ metric[:, 2] = M[:, 0, 2] # m13
208
+ metric[:, 3] = M[:, 1, 1] # m22
209
+ metric[:, 4] = M[:, 1, 2] # m23
210
+ metric[:, 5] = M[:, 2, 2] # m33
211
+ else:
212
+ metric = np.zeros((n_vertices, 3), dtype=np.float64)
213
+ metric[:, 0] = M[:, 0, 0] # m11
214
+ metric[:, 1] = M[:, 0, 1] # m12
215
+ metric[:, 2] = M[:, 1, 1] # m22
216
+
217
+ if single_metric:
218
+ return metric[0]
219
+ return metric
220
+
221
+
222
+ def tensor_to_matrix(
223
+ tensor: NDArray[np.float64],
224
+ dim: int | None = None,
225
+ ) -> NDArray[np.float64]:
226
+ """Convert tensor storage format to full symmetric matrix.
227
+
228
+ Parameters
229
+ ----------
230
+ tensor : array_like
231
+ Tensor in storage format. Shape (6,) or (n, 6) for 3D,
232
+ (3,) or (n, 3) for 2D.
233
+ dim : int, optional
234
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
235
+
236
+ Returns
237
+ -------
238
+ NDArray[np.float64]
239
+ Full symmetric matrix. Shape (3, 3) or (n, 3, 3) for 3D,
240
+ (2, 2) or (n, 2, 2) for 2D.
241
+
242
+ """
243
+ tensor = np.asarray(tensor, dtype=np.float64)
244
+ single = tensor.ndim == 1
245
+ if single:
246
+ tensor = tensor.reshape(1, -1)
247
+
248
+ n_components = tensor.shape[1]
249
+ if dim is None:
250
+ dim = 3 if n_components == 6 else 2
251
+
252
+ n = tensor.shape[0]
253
+ M = np.zeros((n, dim, dim), dtype=np.float64)
254
+
255
+ if dim == 3:
256
+ M[:, 0, 0] = tensor[:, 0] # m11
257
+ M[:, 0, 1] = tensor[:, 1] # m12
258
+ M[:, 0, 2] = tensor[:, 2] # m13
259
+ M[:, 1, 0] = tensor[:, 1] # m21 = m12
260
+ M[:, 1, 1] = tensor[:, 3] # m22
261
+ M[:, 1, 2] = tensor[:, 4] # m23
262
+ M[:, 2, 0] = tensor[:, 2] # m31 = m13
263
+ M[:, 2, 1] = tensor[:, 4] # m32 = m23
264
+ M[:, 2, 2] = tensor[:, 5] # m33
265
+ else:
266
+ M[:, 0, 0] = tensor[:, 0] # m11
267
+ M[:, 0, 1] = tensor[:, 1] # m12
268
+ M[:, 1, 0] = tensor[:, 1] # m21 = m12
269
+ M[:, 1, 1] = tensor[:, 2] # m22
270
+
271
+ if single:
272
+ return M[0]
273
+ return M
274
+
275
+
276
+ def matrix_to_tensor(
277
+ M: NDArray[np.float64],
278
+ ) -> NDArray[np.float64]:
279
+ """Convert full symmetric matrix to tensor storage format.
280
+
281
+ Parameters
282
+ ----------
283
+ M : array_like
284
+ Full symmetric matrix. Shape (3, 3) or (n, 3, 3) for 3D,
285
+ (2, 2) or (n, 2, 2) for 2D.
286
+
287
+ Returns
288
+ -------
289
+ NDArray[np.float64]
290
+ Tensor in storage format. Shape (6,) or (n, 6) for 3D,
291
+ (3,) or (n, 3) for 2D.
292
+
293
+ """
294
+ M = np.asarray(M, dtype=np.float64)
295
+ single = M.ndim == 2
296
+ if single:
297
+ M = M.reshape(1, *M.shape)
298
+
299
+ dim = M.shape[1]
300
+ n = M.shape[0]
301
+
302
+ if dim == 3:
303
+ tensor = np.zeros((n, 6), dtype=np.float64)
304
+ tensor[:, 0] = M[:, 0, 0] # m11
305
+ tensor[:, 1] = M[:, 0, 1] # m12
306
+ tensor[:, 2] = M[:, 0, 2] # m13
307
+ tensor[:, 3] = M[:, 1, 1] # m22
308
+ tensor[:, 4] = M[:, 1, 2] # m23
309
+ tensor[:, 5] = M[:, 2, 2] # m33
310
+ else:
311
+ tensor = np.zeros((n, 3), dtype=np.float64)
312
+ tensor[:, 0] = M[:, 0, 0] # m11
313
+ tensor[:, 1] = M[:, 0, 1] # m12
314
+ tensor[:, 2] = M[:, 1, 1] # m22
315
+
316
+ if single:
317
+ return tensor[0]
318
+ return tensor
319
+
320
+
321
+ def validate_metric_tensor(
322
+ tensor: NDArray[np.float64],
323
+ dim: int | None = None,
324
+ *,
325
+ raise_on_invalid: bool = True,
326
+ ) -> tuple[bool, str]:
327
+ """Validate that metric tensor(s) are positive-definite.
328
+
329
+ A valid metric tensor must be symmetric positive-definite, meaning
330
+ all eigenvalues must be strictly positive.
331
+
332
+ Parameters
333
+ ----------
334
+ tensor : array_like
335
+ Tensor(s) to validate. Shape (6,) or (n, 6) for 3D,
336
+ (3,) or (n, 3) for 2D.
337
+ dim : int, optional
338
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
339
+ raise_on_invalid : bool, optional
340
+ If True, raises ValueError on invalid tensors. Default is True.
341
+
342
+ Returns
343
+ -------
344
+ tuple[bool, str]
345
+ (is_valid, message) tuple.
346
+
347
+ Raises
348
+ ------
349
+ ValueError
350
+ If raise_on_invalid is True and tensor is not valid.
351
+
352
+ Examples
353
+ --------
354
+ >>> valid_tensor = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 1.0])
355
+ >>> validate_metric_tensor(valid_tensor)
356
+ (True, 'Valid positive-definite metric tensor')
357
+
358
+ >>> invalid_tensor = np.array([-1.0, 0.0, 0.0, 1.0, 0.0, 1.0])
359
+ >>> validate_metric_tensor(invalid_tensor, raise_on_invalid=False)
360
+ (False, 'Tensor has non-positive eigenvalues...')
361
+
362
+ """
363
+ tensor = np.asarray(tensor, dtype=np.float64)
364
+ M = tensor_to_matrix(tensor, dim)
365
+
366
+ single = M.ndim == 2
367
+ if single:
368
+ M = M.reshape(1, *M.shape)
369
+
370
+ all_valid = True
371
+ messages = []
372
+
373
+ for i, m in enumerate(M):
374
+ eigvals = np.linalg.eigvalsh(m)
375
+
376
+ if not np.all(eigvals > 0):
377
+ all_valid = False
378
+ prefix = f"Tensor {i}: " if not single else ""
379
+ messages.append(
380
+ f"{prefix}Has non-positive eigenvalues: {eigvals}. "
381
+ "Metric tensors must be positive-definite.",
382
+ )
383
+
384
+ if all_valid:
385
+ msg = "Valid positive-definite metric tensor"
386
+ if not single:
387
+ msg += "s"
388
+ return (True, msg)
389
+
390
+ error_msg = "; ".join(messages)
391
+ if raise_on_invalid:
392
+ raise ValueError(error_msg)
393
+ return (False, error_msg)
394
+
395
+
396
+ def compute_metric_eigenpairs(
397
+ tensor: NDArray[np.float64],
398
+ dim: int | None = None,
399
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
400
+ """Extract principal sizes and directions from metric tensor(s).
401
+
402
+ Parameters
403
+ ----------
404
+ tensor : array_like
405
+ Metric tensor(s). Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.
406
+ dim : int, optional
407
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
408
+
409
+ Returns
410
+ -------
411
+ tuple[NDArray, NDArray]
412
+ (sizes, directions) where:
413
+ - sizes: Principal element sizes, shape (3,) or (n, 3) for 3D
414
+ - directions: Eigenvector matrices, shape (3, 3) or (n, 3, 3) for 3D
415
+ Columns are eigenvectors corresponding to sizes.
416
+
417
+ Examples
418
+ --------
419
+ >>> tensor = np.array([100., 0., 0., 1., 0., 1.]) # 10x stretch in x
420
+ >>> sizes, directions = compute_metric_eigenpairs(tensor)
421
+ >>> sizes
422
+ array([0.1, 1. , 1. ])
423
+
424
+ """
425
+ M = tensor_to_matrix(tensor, dim)
426
+
427
+ single = M.ndim == 2
428
+ if single:
429
+ M = M.reshape(1, *M.shape)
430
+
431
+ n = M.shape[0]
432
+ actual_dim = M.shape[1]
433
+
434
+ sizes = np.zeros((n, actual_dim), dtype=np.float64)
435
+ directions = np.zeros((n, actual_dim, actual_dim), dtype=np.float64)
436
+
437
+ for i, m in enumerate(M):
438
+ eigvals, eigvecs = np.linalg.eigh(m)
439
+ sizes[i] = 1.0 / np.sqrt(eigvals)
440
+ directions[i] = eigvecs
441
+
442
+ if single:
443
+ return sizes[0], directions[0]
444
+ return sizes, directions
445
+
446
+
447
+ def intersect_metrics(
448
+ m1: NDArray[np.float64],
449
+ m2: NDArray[np.float64],
450
+ dim: int | None = None,
451
+ ) -> NDArray[np.float64]:
452
+ """Compute the intersection of two metric tensors.
453
+
454
+ The intersection produces a metric that is at least as refined as both
455
+ input metrics in all directions. This is useful for combining metrics
456
+ from different sources (e.g., boundary layer + feature-based).
457
+
458
+ The intersection is computed via simultaneous diagonalization:
459
+ M_intersect = M1^(1/2) @ N @ M1^(1/2)
460
+ where N is diagonal with max eigenvalues of M1^(-1/2) @ M2 @ M1^(-1/2).
461
+
462
+ Parameters
463
+ ----------
464
+ m1, m2 : array_like
465
+ Metric tensors to intersect. Must have same shape.
466
+ Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.
467
+ dim : int, optional
468
+ Dimension (2 or 3). Inferred from tensor shape if not provided.
469
+
470
+ Returns
471
+ -------
472
+ NDArray[np.float64]
473
+ Intersected metric tensor(s), same shape as inputs.
474
+
475
+ """
476
+ m1 = np.asarray(m1, dtype=np.float64)
477
+ m2 = np.asarray(m2, dtype=np.float64)
478
+
479
+ if m1.shape != m2.shape:
480
+ msg = f"Metrics must have same shape: {m1.shape} vs {m2.shape}"
481
+ raise ValueError(msg)
482
+
483
+ M1 = tensor_to_matrix(m1, dim)
484
+ M2 = tensor_to_matrix(m2, dim)
485
+
486
+ single = M1.ndim == 2
487
+ if single:
488
+ M1 = M1.reshape(1, *M1.shape)
489
+ M2 = M2.reshape(1, *M2.shape)
490
+
491
+ n = M1.shape[0]
492
+ M_intersect = np.zeros_like(M1)
493
+
494
+ for i in range(n):
495
+ eigvals1, eigvecs1 = np.linalg.eigh(M1[i])
496
+
497
+ # Guard against near-singular metrics (eigenvalues close to zero)
498
+ # Use machine epsilon scaled by max eigenvalue for numerical stability
499
+ eps = np.finfo(np.float64).eps * np.max(np.abs(eigvals1)) * 100
500
+ eigvals1 = np.maximum(eigvals1, eps)
501
+
502
+ sqrt_eigvals1 = np.sqrt(eigvals1)
503
+ inv_sqrt_eigvals1 = 1.0 / sqrt_eigvals1
504
+
505
+ M1_sqrt = eigvecs1 @ np.diag(sqrt_eigvals1) @ eigvecs1.T
506
+ M1_inv_sqrt = eigvecs1 @ np.diag(inv_sqrt_eigvals1) @ eigvecs1.T
507
+
508
+ P = M1_inv_sqrt @ M2[i] @ M1_inv_sqrt
509
+
510
+ eigvals_P, eigvecs_P = np.linalg.eigh(P)
511
+
512
+ max_eigvals = np.maximum(eigvals_P, 1.0)
513
+
514
+ M_intersect[i] = (
515
+ M1_sqrt @ eigvecs_P @ np.diag(max_eigvals) @ eigvecs_P.T @ M1_sqrt
516
+ )
517
+
518
+ return matrix_to_tensor(M_intersect if not single else M_intersect[0])
519
+
520
+
521
+ def create_metric_from_hessian(
522
+ hessian: NDArray[np.float64],
523
+ target_error: float = 1e-3,
524
+ hmin: float | None = None,
525
+ hmax: float | None = None,
526
+ ) -> NDArray[np.float64]:
527
+ """Create metric tensor from Hessian matrix for interpolation error control.
528
+
529
+ Given a Hessian H of a solution field, constructs a metric M such that
530
+ the interpolation error is bounded by target_error. This is used for
531
+ solution-adaptive mesh refinement.
532
+
533
+ The metric eigenvalues are: lambda_i = c * |hessian_eigenvalue_i| / target_error
534
+ where c is a constant depending on the interpolation order.
535
+
536
+ Parameters
537
+ ----------
538
+ hessian : array_like
539
+ Hessian tensor(s). Shape (6,) or (n, 6) for 3D, (3,) or (n, 3) for 2D.
540
+ Components: [H11, H12, H13, H22, H23, H33] for 3D.
541
+ target_error : float, optional
542
+ Target interpolation error. Default is 1e-3.
543
+ hmin : float, optional
544
+ Minimum element size. Limits maximum metric eigenvalues.
545
+ hmax : float, optional
546
+ Maximum element size. Limits minimum metric eigenvalues.
547
+
548
+ Returns
549
+ -------
550
+ NDArray[np.float64]
551
+ Metric tensor(s) for adaptive remeshing.
552
+
553
+ Notes
554
+ -----
555
+ For P1 interpolation, the interpolation error is bounded by:
556
+ e <= (1/8) * h^2 * |d²u/ds²|_max
557
+
558
+ This function computes the metric that achieves a specified error bound.
559
+
560
+ """
561
+ hessian = np.asarray(hessian, dtype=np.float64)
562
+ H = tensor_to_matrix(hessian)
563
+
564
+ single = H.ndim == 2
565
+ if single:
566
+ H = H.reshape(1, *H.shape)
567
+
568
+ n = H.shape[0]
569
+ M = np.zeros_like(H)
570
+
571
+ c = 1.0 / 8.0
572
+
573
+ for i in range(n):
574
+ eigvals, eigvecs = np.linalg.eigh(H[i])
575
+ abs_eigvals = np.abs(eigvals)
576
+
577
+ metric_eigvals = c * abs_eigvals / target_error
578
+
579
+ # Floor eigenvalues to avoid singular metrics when Hessian is near-zero.
580
+ # 1e-12 corresponds to element sizes up to ~1e6, sufficient for most meshes.
581
+ # This is applied before hmin/hmax bounds which may further constrain values.
582
+ eps = 1e-12
583
+ metric_eigvals = np.maximum(metric_eigvals, eps)
584
+
585
+ if hmin is not None:
586
+ max_eigval = 1.0 / (hmin * hmin)
587
+ metric_eigvals = np.minimum(metric_eigvals, max_eigval)
588
+
589
+ if hmax is not None:
590
+ min_eigval = 1.0 / (hmax * hmax)
591
+ metric_eigvals = np.maximum(metric_eigvals, min_eigval)
592
+
593
+ M[i] = eigvecs @ np.diag(metric_eigvals) @ eigvecs.T
594
+
595
+ return matrix_to_tensor(M if not single else M[0])
mmgpy/mmg2d.dll ADDED
Binary file
mmgpy/mmg2d.lib ADDED
Binary file
mmgpy/mmg3d.dll ADDED
Binary file
mmgpy/mmg3d.lib ADDED
Binary file
mmgpy/mmgs.dll ADDED
Binary file
mmgpy/mmgs.lib ADDED
Binary file
mmgpy/progress.py ADDED
@@ -0,0 +1,57 @@
1
+ """Progress tracking utilities for mmgpy.
2
+
3
+ This module provides progress callbacks and Rich integration for monitoring
4
+ mesh operations like remeshing.
5
+
6
+ Examples
7
+ --------
8
+ Basic usage with logging:
9
+
10
+ >>> from mmgpy import MmgMesh3D
11
+ >>> from mmgpy.progress import LoggingProgressReporter
12
+ >>> mesh = MmgMesh3D(vertices, elements)
13
+ >>> reporter = LoggingProgressReporter()
14
+ >>> # Progress events are logged during remeshing
15
+
16
+ Using Rich progress display:
17
+
18
+ >>> from mmgpy import MmgMesh3D
19
+ >>> from mmgpy.progress import rich_progress
20
+ >>> mesh = MmgMesh3D(vertices, elements)
21
+ >>> with rich_progress() as callback:
22
+ ... # A nice progress bar is shown during operations
23
+ ... pass
24
+
25
+ Creating a custom callback:
26
+
27
+ >>> from mmgpy.progress import ProgressEvent
28
+ >>> def my_callback(event: ProgressEvent) -> None:
29
+ ... print(f"{event.phase}: {event.message}")
30
+
31
+ """
32
+
33
+ from ._progress import (
34
+ LoggingProgressReporter,
35
+ ProgressEvent,
36
+ ProgressReporter,
37
+ RichProgressReporter,
38
+ remesh_2d,
39
+ remesh_3d,
40
+ remesh_mesh,
41
+ remesh_mesh_lagrangian,
42
+ remesh_surface,
43
+ rich_progress,
44
+ )
45
+
46
+ __all__ = [
47
+ "LoggingProgressReporter",
48
+ "ProgressEvent",
49
+ "ProgressReporter",
50
+ "RichProgressReporter",
51
+ "remesh_2d",
52
+ "remesh_3d",
53
+ "remesh_mesh",
54
+ "remesh_mesh_lagrangian",
55
+ "remesh_surface",
56
+ "rich_progress",
57
+ ]
mmgpy/py.typed ADDED
File without changes