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.
- bin/__init__.py +10 -0
- bin/concrt140.dll +0 -0
- bin/mmg.dll +0 -0
- bin/mmg2d.dll +0 -0
- bin/mmg2d_O3.exe +0 -0
- bin/mmg3d.dll +0 -0
- bin/mmg3d_O3.exe +0 -0
- bin/mmgs.dll +0 -0
- bin/mmgs_O3.exe +0 -0
- bin/msvcp140.dll +0 -0
- bin/msvcp140_1.dll +0 -0
- bin/msvcp140_2.dll +0 -0
- bin/msvcp140_atomic_wait.dll +0 -0
- bin/msvcp140_codecvt_ids.dll +0 -0
- bin/vcruntime140.dll +0 -0
- bin/vcruntime140_1.dll +0 -0
- include/__init__.py +10 -0
- include/mmg/common/libmmgtypes.h +687 -0
- include/mmg/common/libmmgtypesf.h +762 -0
- include/mmg/common/mmg_export.h +47 -0
- include/mmg/common/mmgcmakedefines.h +46 -0
- include/mmg/common/mmgcmakedefinesf.h +29 -0
- include/mmg/common/mmgversion.h +54 -0
- include/mmg/libmmg.h +67 -0
- include/mmg/libmmgf.h +42 -0
- include/mmg/mmg2d/libmmg2d.h +2761 -0
- include/mmg/mmg2d/libmmg2df.h +3263 -0
- include/mmg/mmg2d/mmg2d_export.h +34 -0
- include/mmg/mmg3d/libmmg3d.h +3444 -0
- include/mmg/mmg3d/libmmg3df.h +4041 -0
- include/mmg/mmg3d/mmg3d_export.h +34 -0
- include/mmg/mmgs/libmmgs.h +2560 -0
- include/mmg/mmgs/libmmgsf.h +3028 -0
- include/mmg/mmgs/mmgs_export.h +34 -0
- lib/__init__.py +10 -0
- lib/cmake/mmg/FindElas.cmake +57 -0
- lib/cmake/mmg/FindSCOTCH.cmake +373 -0
- lib/cmake/mmg/MmgTargets-release.cmake +53 -0
- lib/cmake/mmg/MmgTargets.cmake +127 -0
- lib/cmake/mmg/mmgConfig.cmake +43 -0
- lib/mmg.lib +0 -0
- lib/mmg2d.lib +0 -0
- lib/mmg3d.lib +0 -0
- lib/mmgs.lib +0 -0
- mmgpy/__init__.py +888 -0
- mmgpy/_logging.py +86 -0
- mmgpy/_mmgpy.cp310-win_amd64.pyd +0 -0
- mmgpy/_mmgpy.pyi +650 -0
- mmgpy/_options.py +304 -0
- mmgpy/_progress.py +539 -0
- mmgpy/_pyvista.py +423 -0
- mmgpy/_version.py +3 -0
- mmgpy/_version.py.in +3 -0
- mmgpy/lagrangian.py +394 -0
- mmgpy/metrics.py +595 -0
- mmgpy/mmg2d.dll +0 -0
- mmgpy/mmg2d.lib +0 -0
- mmgpy/mmg3d.dll +0 -0
- mmgpy/mmg3d.lib +0 -0
- mmgpy/mmgs.dll +0 -0
- mmgpy/mmgs.lib +0 -0
- mmgpy/progress.py +57 -0
- mmgpy/py.typed +0 -0
- mmgpy/sizing.py +370 -0
- mmgpy-0.2.0.dist-info/DELVEWHEEL +2 -0
- mmgpy-0.2.0.dist-info/METADATA +75 -0
- mmgpy-0.2.0.dist-info/RECORD +129 -0
- mmgpy-0.2.0.dist-info/WHEEL +5 -0
- mmgpy-0.2.0.dist-info/entry_points.txt +6 -0
- mmgpy-0.2.0.dist-info/licenses/LICENSE +38 -0
- mmgpy.libs/vtkCommonColor-9.4-799ae0f43eb3a04510b0ed500c05e895.dll +0 -0
- mmgpy.libs/vtkCommonComputationalGeometry-9.4-e1ee47e9ca84c220e3fda21c864610e9.dll +0 -0
- mmgpy.libs/vtkCommonCore-9.4.dll +0 -0
- mmgpy.libs/vtkCommonDataModel-9.4.dll +0 -0
- mmgpy.libs/vtkCommonExecutionModel-9.4-5fea279cddbd0dd8e39907cbfb423d2c.dll +0 -0
- mmgpy.libs/vtkCommonMath-9.4-70aba6f1b0ad06008b5990b6843ff4e9.dll +0 -0
- mmgpy.libs/vtkCommonMisc-9.4-96697cc413673520510a54c05fa0ef99.dll +0 -0
- mmgpy.libs/vtkCommonSystem-9.4-a748ca3c31712678e4e0f2ff3b09785b.dll +0 -0
- mmgpy.libs/vtkCommonTransforms-9.4-11e72e99aca2003a38a3ff9bcfb90d92.dll +0 -0
- mmgpy.libs/vtkDICOMParser-9.4-6f27d14c12768aac52622cbd28ca0b3a.dll +0 -0
- mmgpy.libs/vtkFiltersCellGrid-9.4-5675f4c4a8440bd5f5981149f99315e8.dll +0 -0
- mmgpy.libs/vtkFiltersCore-9.4-ef29a82b399f8ffeec5dfa8c7e010901.dll +0 -0
- mmgpy.libs/vtkFiltersExtraction-9.4-6a452db7095d68b3af501d1b39a05670.dll +0 -0
- mmgpy.libs/vtkFiltersGeneral-9.4-b9b071ef41cba7c6468dd7e0a9f5d4bf.dll +0 -0
- mmgpy.libs/vtkFiltersGeometry-9.4-757f3c8b9a1fd9774cce83aec0c94c55.dll +0 -0
- mmgpy.libs/vtkFiltersHybrid-9.4-6b343077b9da30bb95fc4492762b9bed.dll +0 -0
- mmgpy.libs/vtkFiltersHyperTree-9.4-831d7e88dd11a3faec876929089d21e8.dll +0 -0
- mmgpy.libs/vtkFiltersModeling-9.4-a3fc680a4836e8913376390217ceae1e.dll +0 -0
- mmgpy.libs/vtkFiltersParallel-9.4-5307d8b619459bb2aeb13c2872cebc2b.dll +0 -0
- mmgpy.libs/vtkFiltersReduction-9.4-f7abe86bfcde726b6e7daec4773a089f.dll +0 -0
- mmgpy.libs/vtkFiltersSources-9.4-d38546a5e2bfd83ea0a70f5be4fa7fdc.dll +0 -0
- mmgpy.libs/vtkFiltersStatistics-9.4-d2abec2372ab98574cdbd0461c73d459.dll +0 -0
- mmgpy.libs/vtkFiltersTexture-9.4-a374b3cfe75e10483976a6a6780ca1ca.dll +0 -0
- mmgpy.libs/vtkFiltersVerdict-9.4-a6a276b6e0d3ac6d042f192842c9382a.dll +0 -0
- mmgpy.libs/vtkIOCellGrid-9.4-0df3bee5d56b2062dad430b53f81f06a.dll +0 -0
- mmgpy.libs/vtkIOCore-9.4.dll +0 -0
- mmgpy.libs/vtkIOGeometry-9.4-13e2301d83eea31e8df955a8dd196eb1.dll +0 -0
- mmgpy.libs/vtkIOImage-9.4-666beab0c43da6c4b2c59fba464bb6b1.dll +0 -0
- mmgpy.libs/vtkIOLegacy-9.4.dll +0 -0
- mmgpy.libs/vtkIOParallel-9.4.dll +0 -0
- mmgpy.libs/vtkIOParallelXML-9.4.dll +0 -0
- mmgpy.libs/vtkIOXML-9.4.dll +0 -0
- mmgpy.libs/vtkIOXMLParser-9.4-f4144aed6a73ee50eefeae75a78fd71b.dll +0 -0
- mmgpy.libs/vtkImagingCore-9.4-ecc92d2d09d8ac4e8f3b199a917f96fd.dll +0 -0
- mmgpy.libs/vtkImagingSources-9.4-944fcaf71940d51ed5dc1b5c8eba2571.dll +0 -0
- mmgpy.libs/vtkParallelCore-9.4-114ce4e5a12302a9f3c3485ab88d5a39.dll +0 -0
- mmgpy.libs/vtkParallelDIY-9.4-7add130df7f5139505a17ee72afefa41.dll +0 -0
- mmgpy.libs/vtkRenderingCore-9.4-ec5612bc182f43047add4c6a55050be4.dll +0 -0
- mmgpy.libs/vtkdoubleconversion-9.4-676429e05704a1fd81323494aba16669.dll +0 -0
- mmgpy.libs/vtkexpat-9.4-58e3b6d3064cf0e02659e3a57f60f779.dll +0 -0
- mmgpy.libs/vtkfmt-9.4-fd2ae0aaa19f2f735b93d4feb613d558.dll +0 -0
- mmgpy.libs/vtkjpeg-9.4-2fd58669e746a0d571706ff3f4c1896f.dll +0 -0
- mmgpy.libs/vtkjsoncpp-9.4-c2076501decec6ce45be5280d480c02d.dll +0 -0
- mmgpy.libs/vtkkissfft-9.4-874ed77ec96808d2e395a6caeb69abbe.dll +0 -0
- mmgpy.libs/vtkloguru-9.4-1038f41dd2d3acc7d894c1d57b428cc6.dll +0 -0
- mmgpy.libs/vtklz4-9.4-9982d6f8f47ac19965668cd3c2926473.dll +0 -0
- mmgpy.libs/vtklzma-9.4-a473d065be12f766957c699a0aefac38.dll +0 -0
- mmgpy.libs/vtkmetaio-9.4-aa48920e94a28f23d08054f5e82867f4.dll +0 -0
- mmgpy.libs/vtkpng-9.4-cbefdfbef5c0d4dd7ae43b9d526e881a.dll +0 -0
- mmgpy.libs/vtkpugixml-9.4-9c7652d9a42fd99e62753e7d3fd87ee9.dll +0 -0
- mmgpy.libs/vtksys-9.4.dll +0 -0
- mmgpy.libs/vtktiff-9.4-9822dcf52447c045cf9a70960d45a9bd.dll +0 -0
- mmgpy.libs/vtktoken-9.4-471c14923651cbbd86067852f347807b.dll +0 -0
- mmgpy.libs/vtkverdict-9.4-c549882a3b8671666be1366008f27f7e.dll +0 -0
- mmgpy.libs/vtkzlib-9.4-3b9ffa51c80ccd91bb22a4c3ca11a8d7.dll +0 -0
- share/__init__.py +10 -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/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
|