adtlert 0.1.0__py3-none-any.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.
- adtlert/__init__.py +43 -0
- adtlert/fem/__init__.py +72 -0
- adtlert/fem/assembly.py +220 -0
- adtlert/fem/boundary.py +176 -0
- adtlert/fem/p1.py +134 -0
- adtlert/fem/p2.py +120 -0
- adtlert/fem/tet_p1.py +141 -0
- adtlert/forward/__init__.py +22 -0
- adtlert/forward/autograd.py +149 -0
- adtlert/forward/ert2p5d.py +4694 -0
- adtlert/forward/ert3d.py +603 -0
- adtlert/forward/integration.py +56 -0
- adtlert/forward/modeling.py +469 -0
- adtlert/inversion/__init__.py +101 -0
- adtlert/inversion/core.py +3142 -0
- adtlert/inversion/misfit.py +290 -0
- adtlert/inversion/optimizers.py +106 -0
- adtlert/inversion/petrophysics.py +484 -0
- adtlert/inversion/regularization.py +715 -0
- adtlert/mesh/__init__.py +27 -0
- adtlert/mesh/core.py +1000 -0
- adtlert/mesh/core3d.py +264 -0
- adtlert/survey/__init__.py +5 -0
- adtlert/survey/core.py +87 -0
- adtlert/utils/__init__.py +8 -0
- adtlert/utils/dtypes.py +22 -0
- adtlert/utils/progress.py +227 -0
- adtlert/utils/torch_runtime.py +337 -0
- adtlert/workflows/__init__.py +47 -0
- adtlert/workflows/terrain.py +1173 -0
- adtlert-0.1.0.dist-info/METADATA +262 -0
- adtlert-0.1.0.dist-info/RECORD +34 -0
- adtlert-0.1.0.dist-info/WHEEL +4 -0
- adtlert-0.1.0.dist-info/licenses/LICENSE +21 -0
adtlert/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Top-level package for differentiable time-lapse ERT on Torch."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from adtlert.fem import P1ElementData, build_p1_element_data, p1_shape_functions
|
|
6
|
+
from adtlert.forward import (
|
|
7
|
+
ERT2p5DApparentResistivityFunction,
|
|
8
|
+
ERTForward2p5D,
|
|
9
|
+
ERTForwardModeling,
|
|
10
|
+
ForwardResponse,
|
|
11
|
+
apparent_resistivity_autograd,
|
|
12
|
+
)
|
|
13
|
+
from adtlert.inversion import (
|
|
14
|
+
ERTInversion,
|
|
15
|
+
InversionConfig,
|
|
16
|
+
TimeLapseERTInversion,
|
|
17
|
+
WindowedTimeLapseERTInversion,
|
|
18
|
+
)
|
|
19
|
+
from adtlert.mesh import Mesh
|
|
20
|
+
from adtlert.survey import Survey
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
__version__ = version("adtlert")
|
|
24
|
+
except PackageNotFoundError: # pragma: no cover - source tree without installation
|
|
25
|
+
__version__ = "0+unknown"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ERTForward2p5D",
|
|
29
|
+
"ERT2p5DApparentResistivityFunction",
|
|
30
|
+
"ERTForwardModeling",
|
|
31
|
+
"ERTInversion",
|
|
32
|
+
"ForwardResponse",
|
|
33
|
+
"InversionConfig",
|
|
34
|
+
"Mesh",
|
|
35
|
+
"P1ElementData",
|
|
36
|
+
"Survey",
|
|
37
|
+
"TimeLapseERTInversion",
|
|
38
|
+
"WindowedTimeLapseERTInversion",
|
|
39
|
+
"apparent_resistivity_autograd",
|
|
40
|
+
"build_p1_element_data",
|
|
41
|
+
"p1_shape_functions",
|
|
42
|
+
"__version__",
|
|
43
|
+
]
|
adtlert/fem/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Finite-element basis and quadrature utilities."""
|
|
2
|
+
|
|
3
|
+
from adtlert.fem.assembly import (
|
|
4
|
+
COORouting,
|
|
5
|
+
assemble_global_bcoo,
|
|
6
|
+
assemble_helmholtz_operator,
|
|
7
|
+
assemble_local_mass,
|
|
8
|
+
assemble_local_mass_p2,
|
|
9
|
+
assemble_local_stiffness,
|
|
10
|
+
assemble_local_stiffness_p2,
|
|
11
|
+
build_coo_routing,
|
|
12
|
+
build_coo_routing_from_connectivity,
|
|
13
|
+
)
|
|
14
|
+
from adtlert.fem.boundary import (
|
|
15
|
+
BoundaryRouting,
|
|
16
|
+
assemble_boundary_bcoo,
|
|
17
|
+
assemble_local_boundary_mass,
|
|
18
|
+
assemble_local_boundary_mass_p2,
|
|
19
|
+
build_boundary_routing,
|
|
20
|
+
build_boundary_routing_from_connectivity,
|
|
21
|
+
robin_boundary_coefficients,
|
|
22
|
+
)
|
|
23
|
+
from adtlert.fem.p1 import (
|
|
24
|
+
P1ElementData,
|
|
25
|
+
build_p1_element_data,
|
|
26
|
+
p1_shape_functions,
|
|
27
|
+
reference_shape_gradients,
|
|
28
|
+
triangle_quadrature,
|
|
29
|
+
)
|
|
30
|
+
from adtlert.fem.p2 import P2ElementData, build_p2_element_data, p2_shape_functions, reference_shape_gradients_p2
|
|
31
|
+
from adtlert.fem.tet_p1 import (
|
|
32
|
+
TetrahedronP1Data,
|
|
33
|
+
TetrahedronP2Data,
|
|
34
|
+
build_tetrahedron_p1_data,
|
|
35
|
+
build_tetrahedron_p2_data,
|
|
36
|
+
p2_tetrahedron_shape_values,
|
|
37
|
+
p2_triangle_mass_template,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"BoundaryRouting",
|
|
42
|
+
"COORouting",
|
|
43
|
+
"assemble_boundary_bcoo",
|
|
44
|
+
"P1ElementData",
|
|
45
|
+
"assemble_global_bcoo",
|
|
46
|
+
"assemble_helmholtz_operator",
|
|
47
|
+
"assemble_local_boundary_mass",
|
|
48
|
+
"assemble_local_boundary_mass_p2",
|
|
49
|
+
"assemble_local_mass",
|
|
50
|
+
"assemble_local_mass_p2",
|
|
51
|
+
"assemble_local_stiffness",
|
|
52
|
+
"assemble_local_stiffness_p2",
|
|
53
|
+
"build_boundary_routing",
|
|
54
|
+
"build_boundary_routing_from_connectivity",
|
|
55
|
+
"build_p1_element_data",
|
|
56
|
+
"build_p2_element_data",
|
|
57
|
+
"build_coo_routing",
|
|
58
|
+
"build_coo_routing_from_connectivity",
|
|
59
|
+
"p1_shape_functions",
|
|
60
|
+
"p2_shape_functions",
|
|
61
|
+
"reference_shape_gradients",
|
|
62
|
+
"reference_shape_gradients_p2",
|
|
63
|
+
"robin_boundary_coefficients",
|
|
64
|
+
"triangle_quadrature",
|
|
65
|
+
"P2ElementData",
|
|
66
|
+
"TetrahedronP1Data",
|
|
67
|
+
"TetrahedronP2Data",
|
|
68
|
+
"build_tetrahedron_p1_data",
|
|
69
|
+
"build_tetrahedron_p2_data",
|
|
70
|
+
"p2_tetrahedron_shape_values",
|
|
71
|
+
"p2_triangle_mass_template",
|
|
72
|
+
]
|
adtlert/fem/assembly.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Tensorized assembly utilities for finite-element sparse operators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from adtlert.utils.torch_runtime import Array
|
|
8
|
+
from adtlert.utils.torch_runtime import torch_np
|
|
9
|
+
from adtlert.utils.torch_runtime import BCOO
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from adtlert.fem.p1 import P1ElementData
|
|
13
|
+
from adtlert.fem.p2 import P2ElementData
|
|
14
|
+
from adtlert.mesh import Mesh
|
|
15
|
+
from adtlert.utils.dtypes import FLOAT_DTYPE, INT_DTYPE, NP_FLOAT_DTYPE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class COORouting:
|
|
20
|
+
"""Reusable COO routing indices for sparse element assembly."""
|
|
21
|
+
|
|
22
|
+
indices: Array
|
|
23
|
+
shape: tuple[int, int]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _expand_coefficient(coefficients: Array | float, element_data: P1ElementData) -> Array:
|
|
27
|
+
coefficient_array = torch_np.asarray(coefficients, dtype=FLOAT_DTYPE)
|
|
28
|
+
cell_count = element_data.cell_areas.shape[0]
|
|
29
|
+
quadrature_count = element_data.quadrature_weights.shape[0]
|
|
30
|
+
|
|
31
|
+
if coefficient_array.ndim == 0:
|
|
32
|
+
return torch_np.broadcast_to(coefficient_array, (cell_count, quadrature_count))
|
|
33
|
+
if coefficient_array.shape == (cell_count,):
|
|
34
|
+
return torch_np.broadcast_to(coefficient_array[:, None], (cell_count, quadrature_count))
|
|
35
|
+
if coefficient_array.shape == (cell_count, quadrature_count):
|
|
36
|
+
return coefficient_array
|
|
37
|
+
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"coefficients must be scalar, shape (num_cells,), or shape (num_cells, num_quadrature)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _static_scalar_value(value: Array | float) -> float | None:
|
|
44
|
+
try:
|
|
45
|
+
value_array = np.asarray(value)
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
if value_array.ndim != 0:
|
|
49
|
+
return None
|
|
50
|
+
return float(value_array)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _expand_coefficient_generic(
|
|
54
|
+
coefficients: Array | float,
|
|
55
|
+
*,
|
|
56
|
+
cell_count: int,
|
|
57
|
+
quadrature_count: int,
|
|
58
|
+
) -> Array:
|
|
59
|
+
coefficient_array = torch_np.asarray(coefficients, dtype=FLOAT_DTYPE)
|
|
60
|
+
|
|
61
|
+
if coefficient_array.ndim == 0:
|
|
62
|
+
return torch_np.broadcast_to(coefficient_array, (cell_count, quadrature_count))
|
|
63
|
+
if coefficient_array.shape == (cell_count,):
|
|
64
|
+
return torch_np.broadcast_to(coefficient_array[:, None], (cell_count, quadrature_count))
|
|
65
|
+
if coefficient_array.shape == (cell_count, quadrature_count):
|
|
66
|
+
return coefficient_array
|
|
67
|
+
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"coefficients must be scalar, shape (num_cells,), or shape (num_cells, num_quadrature)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_coo_routing(mesh: Mesh) -> COORouting:
|
|
74
|
+
"""Precompute the COO routing pattern for cellwise assembly."""
|
|
75
|
+
|
|
76
|
+
cells = np.asarray(mesh.cells, dtype=np.int32)
|
|
77
|
+
local_dof = int(cells.shape[1])
|
|
78
|
+
row_indices = np.broadcast_to(cells[:, :, None], (mesh.cell_count, local_dof, local_dof)).reshape(-1)
|
|
79
|
+
col_indices = np.broadcast_to(cells[:, None, :], (mesh.cell_count, local_dof, local_dof)).reshape(-1)
|
|
80
|
+
indices = np.stack((row_indices, col_indices), axis=1).astype(np.int32)
|
|
81
|
+
return COORouting(indices=torch_np.asarray(indices, dtype=INT_DTYPE), shape=(mesh.node_count, mesh.node_count))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_coo_routing_from_connectivity(connectivity: Array, node_count: int) -> COORouting:
|
|
85
|
+
"""Precompute COO routing for arbitrary local connectivity."""
|
|
86
|
+
|
|
87
|
+
connectivity_np = np.asarray(connectivity, dtype=np.int32)
|
|
88
|
+
local_dof = int(connectivity_np.shape[1])
|
|
89
|
+
row_indices = np.broadcast_to(
|
|
90
|
+
connectivity_np[:, :, None],
|
|
91
|
+
(connectivity_np.shape[0], local_dof, local_dof),
|
|
92
|
+
).reshape(-1)
|
|
93
|
+
col_indices = np.broadcast_to(
|
|
94
|
+
connectivity_np[:, None, :],
|
|
95
|
+
(connectivity_np.shape[0], local_dof, local_dof),
|
|
96
|
+
).reshape(-1)
|
|
97
|
+
indices = np.stack((row_indices, col_indices), axis=1).astype(np.int32)
|
|
98
|
+
return COORouting(indices=torch_np.asarray(indices, dtype=INT_DTYPE), shape=(node_count, node_count))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def assemble_local_stiffness(element_data: P1ElementData, conductivity: Array | float) -> Array:
|
|
102
|
+
"""Assemble elementwise diffusion matrices with batched einsums."""
|
|
103
|
+
|
|
104
|
+
static_conductivity = _static_scalar_value(conductivity)
|
|
105
|
+
if static_conductivity is not None:
|
|
106
|
+
dtype = NP_FLOAT_DTYPE
|
|
107
|
+
gradients = np.asarray(element_data.gradients, dtype=dtype)
|
|
108
|
+
weights = (
|
|
109
|
+
2.0
|
|
110
|
+
* np.asarray(element_data.cell_areas, dtype=dtype)[:, None]
|
|
111
|
+
* np.asarray(element_data.quadrature_weights, dtype=dtype)[None, :]
|
|
112
|
+
)
|
|
113
|
+
local = np.einsum("eqid,eqjd,eq->eij", gradients, gradients, static_conductivity * weights)
|
|
114
|
+
return torch_np.asarray(local, dtype=FLOAT_DTYPE)
|
|
115
|
+
|
|
116
|
+
conductivity_values = _expand_coefficient(conductivity, element_data)
|
|
117
|
+
weights = 2.0 * element_data.cell_areas[:, None] * element_data.quadrature_weights[None, :]
|
|
118
|
+
return torch_np.einsum(
|
|
119
|
+
"eqid,eqjd,eq->eij",
|
|
120
|
+
element_data.gradients,
|
|
121
|
+
element_data.gradients,
|
|
122
|
+
conductivity_values * weights,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def assemble_local_mass(
|
|
127
|
+
element_data: P1ElementData, coefficients: Array | float = 1.0
|
|
128
|
+
) -> Array:
|
|
129
|
+
"""Assemble elementwise mass matrices with optional scalar coefficients."""
|
|
130
|
+
|
|
131
|
+
static_coefficient = _static_scalar_value(coefficients)
|
|
132
|
+
if static_coefficient is not None:
|
|
133
|
+
dtype = NP_FLOAT_DTYPE
|
|
134
|
+
shape_values = np.asarray(element_data.shape_values, dtype=dtype)
|
|
135
|
+
weights = (
|
|
136
|
+
2.0
|
|
137
|
+
* np.asarray(element_data.cell_areas, dtype=dtype)[:, None]
|
|
138
|
+
* np.asarray(element_data.quadrature_weights, dtype=dtype)[None, :]
|
|
139
|
+
)
|
|
140
|
+
local = np.einsum("qi,qj,eq->eij", shape_values, shape_values, static_coefficient * weights)
|
|
141
|
+
return torch_np.asarray(local, dtype=FLOAT_DTYPE)
|
|
142
|
+
|
|
143
|
+
coefficient_values = _expand_coefficient(coefficients, element_data)
|
|
144
|
+
weights = 2.0 * element_data.cell_areas[:, None] * element_data.quadrature_weights[None, :]
|
|
145
|
+
return torch_np.einsum(
|
|
146
|
+
"qi,qj,eq->eij",
|
|
147
|
+
element_data.shape_values,
|
|
148
|
+
element_data.shape_values,
|
|
149
|
+
coefficient_values * weights,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def assemble_local_stiffness_p2(
|
|
154
|
+
element_data: P2ElementData,
|
|
155
|
+
conductivity: Array | float,
|
|
156
|
+
) -> Array:
|
|
157
|
+
"""Assemble quadratic elementwise diffusion matrices."""
|
|
158
|
+
|
|
159
|
+
conductivity_values = _expand_coefficient_generic(
|
|
160
|
+
conductivity,
|
|
161
|
+
cell_count=element_data.cell_areas.shape[0],
|
|
162
|
+
quadrature_count=element_data.quadrature_weights.shape[0],
|
|
163
|
+
)
|
|
164
|
+
weights = 2.0 * element_data.cell_areas[:, None] * element_data.quadrature_weights[None, :]
|
|
165
|
+
return torch_np.einsum(
|
|
166
|
+
"eqid,eqjd,eq->eij",
|
|
167
|
+
element_data.gradients,
|
|
168
|
+
element_data.gradients,
|
|
169
|
+
conductivity_values * weights,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def assemble_local_mass_p2(
|
|
174
|
+
element_data: P2ElementData,
|
|
175
|
+
coefficients: Array | float = 1.0,
|
|
176
|
+
) -> Array:
|
|
177
|
+
"""Assemble quadratic elementwise mass matrices."""
|
|
178
|
+
|
|
179
|
+
coefficient_values = _expand_coefficient_generic(
|
|
180
|
+
coefficients,
|
|
181
|
+
cell_count=element_data.cell_areas.shape[0],
|
|
182
|
+
quadrature_count=element_data.quadrature_weights.shape[0],
|
|
183
|
+
)
|
|
184
|
+
weights = 2.0 * element_data.cell_areas[:, None] * element_data.quadrature_weights[None, :]
|
|
185
|
+
return torch_np.einsum(
|
|
186
|
+
"qi,qj,eq->eij",
|
|
187
|
+
element_data.shape_values,
|
|
188
|
+
element_data.shape_values,
|
|
189
|
+
coefficient_values * weights,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def assemble_global_bcoo(local_matrices: Array, routing: COORouting) -> BCOO:
|
|
194
|
+
"""Reduce local element matrices into a global sparse operator."""
|
|
195
|
+
|
|
196
|
+
sparse_matrix = BCOO(
|
|
197
|
+
(local_matrices.reshape(-1), routing.indices),
|
|
198
|
+
shape=routing.shape,
|
|
199
|
+
unique_indices=False,
|
|
200
|
+
)
|
|
201
|
+
return sparse_matrix.sum_duplicates()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def assemble_helmholtz_operator(
|
|
205
|
+
element_data: P1ElementData,
|
|
206
|
+
routing: COORouting,
|
|
207
|
+
conductivity: Array | float,
|
|
208
|
+
wavenumber_sq: Array | float,
|
|
209
|
+
) -> BCOO:
|
|
210
|
+
"""Assemble a sparse Helmholtz-type operator K + k^2 M."""
|
|
211
|
+
|
|
212
|
+
conductivity_values = torch_np.asarray(conductivity, dtype=FLOAT_DTYPE)
|
|
213
|
+
local_operator = assemble_local_stiffness(
|
|
214
|
+
element_data=element_data,
|
|
215
|
+
conductivity=conductivity,
|
|
216
|
+
) + assemble_local_mass(
|
|
217
|
+
element_data=element_data,
|
|
218
|
+
coefficients=conductivity_values * wavenumber_sq,
|
|
219
|
+
)
|
|
220
|
+
return assemble_global_bcoo(local_operator, routing)
|
adtlert/fem/boundary.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Boundary assembly utilities for 2.5D ERT."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.special import k0 as besselk0
|
|
8
|
+
from scipy.special import k1 as besselk1
|
|
9
|
+
|
|
10
|
+
from adtlert.utils.torch_runtime import Array
|
|
11
|
+
from adtlert.utils.torch_runtime import torch_np
|
|
12
|
+
from adtlert.utils.torch_runtime import BCOO
|
|
13
|
+
|
|
14
|
+
from adtlert.mesh import Mesh
|
|
15
|
+
from adtlert.utils.dtypes import FLOAT_DTYPE, INT_DTYPE, NP_FLOAT_DTYPE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class BoundaryRouting:
|
|
20
|
+
"""Reusable COO routing indices for boundary-edge assembly."""
|
|
21
|
+
|
|
22
|
+
indices: Array
|
|
23
|
+
shape: tuple[int, int]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_boundary_routing(mesh: Mesh) -> BoundaryRouting:
|
|
27
|
+
"""Precompute the COO routing pattern for boundary edge assembly."""
|
|
28
|
+
|
|
29
|
+
return build_boundary_routing_from_connectivity(mesh.boundary_edges, mesh.node_count)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_boundary_routing_from_connectivity(boundary_connectivity: Array, node_count: int) -> BoundaryRouting:
|
|
33
|
+
"""Precompute COO routing for arbitrary boundary connectivity."""
|
|
34
|
+
|
|
35
|
+
connectivity_np = np.asarray(boundary_connectivity, dtype=np.int32)
|
|
36
|
+
edge_count = connectivity_np.shape[0]
|
|
37
|
+
local_dof = int(connectivity_np.shape[1])
|
|
38
|
+
row_indices = np.broadcast_to(connectivity_np[:, :, None], (edge_count, local_dof, local_dof)).reshape(-1)
|
|
39
|
+
col_indices = np.broadcast_to(connectivity_np[:, None, :], (edge_count, local_dof, local_dof)).reshape(-1)
|
|
40
|
+
indices = np.stack((row_indices, col_indices), axis=1).astype(np.int32)
|
|
41
|
+
return BoundaryRouting(indices=torch_np.asarray(indices, dtype=INT_DTYPE), shape=(node_count, node_count))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _static_scalar_value(value: Array | float) -> float | None:
|
|
45
|
+
try:
|
|
46
|
+
value_array = np.asarray(value)
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
|
49
|
+
if value_array.ndim != 0:
|
|
50
|
+
return None
|
|
51
|
+
return float(value_array)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def assemble_local_boundary_mass(mesh: Mesh, coefficients: Array | float) -> Array:
|
|
55
|
+
"""Assemble midpoint-based Robin edge matrices."""
|
|
56
|
+
|
|
57
|
+
static_coefficient = _static_scalar_value(coefficients)
|
|
58
|
+
if static_coefficient is not None:
|
|
59
|
+
dtype = NP_FLOAT_DTYPE
|
|
60
|
+
reference = np.asarray([[2.0, 1.0], [1.0, 2.0]], dtype=dtype) / 6.0
|
|
61
|
+
lengths = np.asarray(mesh.boundary_edge_lengths, dtype=dtype)
|
|
62
|
+
local = static_coefficient * lengths[:, None, None] * reference[None, :, :]
|
|
63
|
+
return torch_np.asarray(local, dtype=FLOAT_DTYPE)
|
|
64
|
+
|
|
65
|
+
coefficient_array = torch_np.asarray(coefficients, dtype=FLOAT_DTYPE)
|
|
66
|
+
if coefficient_array.ndim == 0:
|
|
67
|
+
coefficient_array = torch_np.broadcast_to(coefficient_array, (mesh.boundary_edges.shape[0],))
|
|
68
|
+
elif coefficient_array.shape != (mesh.boundary_edges.shape[0],):
|
|
69
|
+
raise ValueError("boundary coefficients must be scalar or shape (num_boundary_edges,)")
|
|
70
|
+
|
|
71
|
+
reference = torch_np.asarray([[2.0, 1.0], [1.0, 2.0]], dtype=FLOAT_DTYPE) / 6.0
|
|
72
|
+
return coefficient_array[:, None, None] * mesh.boundary_edge_lengths[:, None, None] * reference
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def assemble_local_boundary_mass_p2(lengths: Array, coefficients: Array | float) -> Array:
|
|
76
|
+
"""Assemble quadratic Robin edge matrices."""
|
|
77
|
+
|
|
78
|
+
static_coefficient = _static_scalar_value(coefficients)
|
|
79
|
+
if static_coefficient is not None:
|
|
80
|
+
dtype = NP_FLOAT_DTYPE
|
|
81
|
+
length_array_np = np.asarray(lengths, dtype=dtype)
|
|
82
|
+
reference = np.asarray(
|
|
83
|
+
[
|
|
84
|
+
[4.0, 2.0, -1.0],
|
|
85
|
+
[2.0, 16.0, 2.0],
|
|
86
|
+
[-1.0, 2.0, 4.0],
|
|
87
|
+
],
|
|
88
|
+
dtype=dtype,
|
|
89
|
+
) / 30.0
|
|
90
|
+
local = static_coefficient * length_array_np[:, None, None] * reference[None, :, :]
|
|
91
|
+
return torch_np.asarray(local, dtype=FLOAT_DTYPE)
|
|
92
|
+
|
|
93
|
+
length_array = torch_np.asarray(lengths, dtype=FLOAT_DTYPE)
|
|
94
|
+
coefficient_array = torch_np.asarray(coefficients, dtype=FLOAT_DTYPE)
|
|
95
|
+
if coefficient_array.ndim == 0:
|
|
96
|
+
coefficient_array = torch_np.broadcast_to(coefficient_array, (length_array.shape[0],))
|
|
97
|
+
elif coefficient_array.shape != (length_array.shape[0],):
|
|
98
|
+
raise ValueError("boundary coefficients must be scalar or shape (num_boundary_edges,)")
|
|
99
|
+
|
|
100
|
+
reference = torch_np.asarray(
|
|
101
|
+
[
|
|
102
|
+
[4.0, 2.0, -1.0],
|
|
103
|
+
[2.0, 16.0, 2.0],
|
|
104
|
+
[-1.0, 2.0, 4.0],
|
|
105
|
+
],
|
|
106
|
+
dtype=FLOAT_DTYPE,
|
|
107
|
+
) / 30.0
|
|
108
|
+
return coefficient_array[:, None, None] * length_array[:, None, None] * reference
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def assemble_boundary_bcoo(local_matrices: Array, routing: BoundaryRouting) -> BCOO:
|
|
112
|
+
"""Reduce local boundary edge matrices into a global sparse operator."""
|
|
113
|
+
|
|
114
|
+
sparse_matrix = BCOO(
|
|
115
|
+
(local_matrices.reshape(-1), routing.indices),
|
|
116
|
+
shape=routing.shape,
|
|
117
|
+
unique_indices=False,
|
|
118
|
+
)
|
|
119
|
+
return sparse_matrix.sum_duplicates()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _expand_cell_coefficient(coefficients: Array | float, mesh: Mesh) -> Array:
|
|
123
|
+
coefficient_array = torch_np.asarray(coefficients, dtype=FLOAT_DTYPE)
|
|
124
|
+
if coefficient_array.ndim == 0:
|
|
125
|
+
return torch_np.broadcast_to(coefficient_array, (mesh.cell_count,))
|
|
126
|
+
if coefficient_array.shape == (mesh.cell_count,):
|
|
127
|
+
return coefficient_array
|
|
128
|
+
raise ValueError("cell coefficients must be scalar or shape (num_cells,)")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def robin_boundary_coefficients(
|
|
132
|
+
mesh: Mesh,
|
|
133
|
+
conductivity: Array | float,
|
|
134
|
+
source_center: Array,
|
|
135
|
+
wavenumber: float,
|
|
136
|
+
) -> Array:
|
|
137
|
+
"""Compute 2.5D Robin coefficients for the mixed boundary condition."""
|
|
138
|
+
|
|
139
|
+
static_conductivity = _static_scalar_value(conductivity)
|
|
140
|
+
if static_conductivity is None:
|
|
141
|
+
conductivity_values = _expand_cell_coefficient(conductivity, mesh)
|
|
142
|
+
boundary_sigma = conductivity_values[mesh.boundary_edge_cells]
|
|
143
|
+
else:
|
|
144
|
+
boundary_sigma = np.full(mesh.boundary_edges.shape[0], static_conductivity, dtype=NP_FLOAT_DTYPE)
|
|
145
|
+
|
|
146
|
+
centers = np.asarray(mesh.boundary_edge_centers, dtype=float)
|
|
147
|
+
normals = np.asarray(mesh.boundary_edge_normals, dtype=float)
|
|
148
|
+
source = np.asarray(source_center, dtype=float)
|
|
149
|
+
mirrored_source = np.asarray([source[0], -source[1]], dtype=float)
|
|
150
|
+
r1 = centers - source
|
|
151
|
+
r2 = centers - mirrored_source
|
|
152
|
+
r1_abs = np.linalg.norm(r1, axis=1)
|
|
153
|
+
r2_abs = np.linalg.norm(r2, axis=1)
|
|
154
|
+
top_boundary = np.asarray(mesh.surface_edge_mask, dtype=bool)
|
|
155
|
+
|
|
156
|
+
geometry = np.zeros(centers.shape[0], dtype=float)
|
|
157
|
+
valid = (r1_abs > 1e-12) & (r2_abs > 1e-12) & (~top_boundary)
|
|
158
|
+
if np.any(valid):
|
|
159
|
+
denominator = besselk0(r1_abs[valid] * wavenumber) + besselk0(r2_abs[valid] * wavenumber)
|
|
160
|
+
stable = denominator > 1e-12
|
|
161
|
+
valid_indices = np.flatnonzero(valid)
|
|
162
|
+
if np.any(stable):
|
|
163
|
+
stable_indices = valid_indices[stable]
|
|
164
|
+
numerator = (
|
|
165
|
+
np.sum(r1[stable_indices] * normals[stable_indices], axis=1)
|
|
166
|
+
/ r1_abs[stable_indices]
|
|
167
|
+
* besselk1(r1_abs[stable_indices] * wavenumber)
|
|
168
|
+
+ np.sum(r2[stable_indices] * normals[stable_indices], axis=1)
|
|
169
|
+
/ r2_abs[stable_indices]
|
|
170
|
+
* besselk1(r2_abs[stable_indices] * wavenumber)
|
|
171
|
+
)
|
|
172
|
+
geometry[stable_indices] = wavenumber * numerator / denominator[stable]
|
|
173
|
+
|
|
174
|
+
if static_conductivity is not None:
|
|
175
|
+
return torch_np.asarray(boundary_sigma * geometry, dtype=FLOAT_DTYPE)
|
|
176
|
+
return boundary_sigma * torch_np.asarray(geometry, dtype=FLOAT_DTYPE)
|
adtlert/fem/p1.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""P1 triangle basis functions and batched quadrature data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from adtlert.utils.torch_runtime import Array
|
|
8
|
+
from adtlert.utils.torch_runtime import torch_np
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from adtlert.mesh import Mesh
|
|
12
|
+
from adtlert.utils.dtypes import FLOAT_DTYPE, NP_FLOAT_DTYPE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TriangleQuadrature:
|
|
17
|
+
"""Reference triangle quadrature points and weights."""
|
|
18
|
+
|
|
19
|
+
points: Array
|
|
20
|
+
weights: Array
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class P1ElementData:
|
|
25
|
+
"""Batched P1 element tensors for tensorized assembly."""
|
|
26
|
+
|
|
27
|
+
quadrature_points: Array
|
|
28
|
+
quadrature_weights: Array
|
|
29
|
+
shape_values: Array
|
|
30
|
+
reference_gradients: Array
|
|
31
|
+
gradients: Array
|
|
32
|
+
cell_quadrature_points: Array
|
|
33
|
+
cell_areas: Array
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def triangle_quadrature(order: int = 2) -> TriangleQuadrature:
|
|
37
|
+
"""Return a low-order Gauss rule on the reference triangle."""
|
|
38
|
+
|
|
39
|
+
if order == 1:
|
|
40
|
+
points = torch_np.asarray([[1.0 / 3.0, 1.0 / 3.0]], dtype=FLOAT_DTYPE)
|
|
41
|
+
weights = torch_np.asarray([0.5], dtype=FLOAT_DTYPE)
|
|
42
|
+
return TriangleQuadrature(points=points, weights=weights)
|
|
43
|
+
|
|
44
|
+
if order == 2:
|
|
45
|
+
points = torch_np.asarray(
|
|
46
|
+
[
|
|
47
|
+
[1.0 / 6.0, 1.0 / 6.0],
|
|
48
|
+
[2.0 / 3.0, 1.0 / 6.0],
|
|
49
|
+
[1.0 / 6.0, 2.0 / 3.0],
|
|
50
|
+
],
|
|
51
|
+
dtype=FLOAT_DTYPE,
|
|
52
|
+
)
|
|
53
|
+
weights = torch_np.asarray([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0], dtype=FLOAT_DTYPE)
|
|
54
|
+
return TriangleQuadrature(points=points, weights=weights)
|
|
55
|
+
|
|
56
|
+
raise ValueError(f"unsupported triangle quadrature order: {order}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def p1_shape_functions(points: Array) -> Array:
|
|
60
|
+
"""Evaluate P1 shape functions at reference coordinates."""
|
|
61
|
+
|
|
62
|
+
point_array = torch_np.asarray(points, dtype=FLOAT_DTYPE)
|
|
63
|
+
xi = point_array[..., 0]
|
|
64
|
+
eta = point_array[..., 1]
|
|
65
|
+
return torch_np.stack((1.0 - xi - eta, xi, eta), axis=-1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def reference_shape_gradients() -> Array:
|
|
69
|
+
"""Return the constant reference gradients of the P1 basis."""
|
|
70
|
+
|
|
71
|
+
return torch_np.asarray(
|
|
72
|
+
[
|
|
73
|
+
[-1.0, -1.0],
|
|
74
|
+
[1.0, 0.0],
|
|
75
|
+
[0.0, 1.0],
|
|
76
|
+
],
|
|
77
|
+
dtype=FLOAT_DTYPE,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_p1_element_data(mesh: Mesh, quadrature_order: int = 2) -> P1ElementData:
|
|
82
|
+
"""Prepare batched element tensors for later FEM assembly."""
|
|
83
|
+
|
|
84
|
+
dtype = NP_FLOAT_DTYPE
|
|
85
|
+
if quadrature_order == 1:
|
|
86
|
+
quadrature_points = np.asarray([[1.0 / 3.0, 1.0 / 3.0]], dtype=dtype)
|
|
87
|
+
quadrature_weights = np.asarray([0.5], dtype=dtype)
|
|
88
|
+
elif quadrature_order == 2:
|
|
89
|
+
quadrature_points = np.asarray(
|
|
90
|
+
[
|
|
91
|
+
[1.0 / 6.0, 1.0 / 6.0],
|
|
92
|
+
[2.0 / 3.0, 1.0 / 6.0],
|
|
93
|
+
[1.0 / 6.0, 2.0 / 3.0],
|
|
94
|
+
],
|
|
95
|
+
dtype=dtype,
|
|
96
|
+
)
|
|
97
|
+
quadrature_weights = np.asarray([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0], dtype=dtype)
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"unsupported triangle quadrature order: {quadrature_order}")
|
|
100
|
+
|
|
101
|
+
xi = quadrature_points[..., 0]
|
|
102
|
+
eta = quadrature_points[..., 1]
|
|
103
|
+
shape_values = np.stack((1.0 - xi - eta, xi, eta), axis=-1).astype(dtype)
|
|
104
|
+
ref_gradients = np.asarray(
|
|
105
|
+
[
|
|
106
|
+
[-1.0, -1.0],
|
|
107
|
+
[1.0, 0.0],
|
|
108
|
+
[0.0, 1.0],
|
|
109
|
+
],
|
|
110
|
+
dtype=dtype,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
cell_nodes = np.asarray(mesh.nodes, dtype=dtype)[np.asarray(mesh.cells, dtype=np.int32)]
|
|
114
|
+
edge_1 = cell_nodes[:, 1] - cell_nodes[:, 0]
|
|
115
|
+
edge_2 = cell_nodes[:, 2] - cell_nodes[:, 0]
|
|
116
|
+
jacobians = np.stack((edge_1, edge_2), axis=-1)
|
|
117
|
+
inverse_jacobian_t = np.linalg.inv(jacobians).transpose((0, 2, 1))
|
|
118
|
+
cell_gradients = np.einsum("eij,nj->eni", inverse_jacobian_t, ref_gradients)
|
|
119
|
+
gradients = np.broadcast_to(
|
|
120
|
+
cell_gradients[:, None, :, :],
|
|
121
|
+
(mesh.cell_count, quadrature_points.shape[0], 3, 2),
|
|
122
|
+
)
|
|
123
|
+
cell_quadrature_points = np.einsum("qi,eid->eqd", shape_values, cell_nodes)
|
|
124
|
+
cell_areas = 0.5 * np.abs(np.linalg.det(jacobians))
|
|
125
|
+
|
|
126
|
+
return P1ElementData(
|
|
127
|
+
quadrature_points=torch_np.asarray(quadrature_points, dtype=FLOAT_DTYPE),
|
|
128
|
+
quadrature_weights=torch_np.asarray(quadrature_weights, dtype=FLOAT_DTYPE),
|
|
129
|
+
shape_values=torch_np.asarray(shape_values, dtype=FLOAT_DTYPE),
|
|
130
|
+
reference_gradients=torch_np.asarray(ref_gradients, dtype=FLOAT_DTYPE),
|
|
131
|
+
gradients=torch_np.asarray(gradients, dtype=FLOAT_DTYPE),
|
|
132
|
+
cell_quadrature_points=torch_np.asarray(cell_quadrature_points, dtype=FLOAT_DTYPE),
|
|
133
|
+
cell_areas=torch_np.asarray(cell_areas, dtype=FLOAT_DTYPE),
|
|
134
|
+
)
|