adtlert 0.1.0__tar.gz
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-0.1.0/.gitignore +26 -0
- adtlert-0.1.0/LICENSE +21 -0
- adtlert-0.1.0/PKG-INFO +262 -0
- adtlert-0.1.0/README.md +220 -0
- adtlert-0.1.0/adtlert/__init__.py +43 -0
- adtlert-0.1.0/adtlert/fem/__init__.py +72 -0
- adtlert-0.1.0/adtlert/fem/assembly.py +220 -0
- adtlert-0.1.0/adtlert/fem/boundary.py +176 -0
- adtlert-0.1.0/adtlert/fem/p1.py +134 -0
- adtlert-0.1.0/adtlert/fem/p2.py +120 -0
- adtlert-0.1.0/adtlert/fem/tet_p1.py +141 -0
- adtlert-0.1.0/adtlert/forward/__init__.py +22 -0
- adtlert-0.1.0/adtlert/forward/autograd.py +149 -0
- adtlert-0.1.0/adtlert/forward/ert2p5d.py +4694 -0
- adtlert-0.1.0/adtlert/forward/ert3d.py +603 -0
- adtlert-0.1.0/adtlert/forward/integration.py +56 -0
- adtlert-0.1.0/adtlert/forward/modeling.py +469 -0
- adtlert-0.1.0/adtlert/inversion/__init__.py +101 -0
- adtlert-0.1.0/adtlert/inversion/core.py +3142 -0
- adtlert-0.1.0/adtlert/inversion/misfit.py +290 -0
- adtlert-0.1.0/adtlert/inversion/optimizers.py +106 -0
- adtlert-0.1.0/adtlert/inversion/petrophysics.py +484 -0
- adtlert-0.1.0/adtlert/inversion/regularization.py +715 -0
- adtlert-0.1.0/adtlert/mesh/__init__.py +27 -0
- adtlert-0.1.0/adtlert/mesh/core.py +1000 -0
- adtlert-0.1.0/adtlert/mesh/core3d.py +264 -0
- adtlert-0.1.0/adtlert/survey/__init__.py +5 -0
- adtlert-0.1.0/adtlert/survey/core.py +87 -0
- adtlert-0.1.0/adtlert/utils/__init__.py +8 -0
- adtlert-0.1.0/adtlert/utils/dtypes.py +22 -0
- adtlert-0.1.0/adtlert/utils/progress.py +227 -0
- adtlert-0.1.0/adtlert/utils/torch_runtime.py +337 -0
- adtlert-0.1.0/adtlert/workflows/__init__.py +47 -0
- adtlert-0.1.0/adtlert/workflows/terrain.py +1173 -0
- adtlert-0.1.0/pyproject.toml +81 -0
- adtlert-0.1.0/tests/test_autograd_2p5d.py +249 -0
- adtlert-0.1.0/tests/test_ert3d.py +212 -0
adtlert-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.DS_Store
|
|
9
|
+
._*
|
|
10
|
+
*:Zone.Identifier
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
.venv-resipy/
|
|
15
|
+
.cache/
|
|
16
|
+
.tmp_wine_bin/
|
|
17
|
+
|
|
18
|
+
2d_resistivity_model/
|
|
19
|
+
models_1year_1day/
|
|
20
|
+
.codex/
|
|
21
|
+
.agents/
|
|
22
|
+
result/
|
|
23
|
+
ProcessedData/
|
|
24
|
+
|
|
25
|
+
# Local research workspace; publish curated scripts from example/ instead.
|
|
26
|
+
paper/
|
adtlert-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pu Yang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
adtlert-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adtlert
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Differentiable 2.5D and 3D electrical resistivity tomography on PyTorch
|
|
5
|
+
Project-URL: Homepage, https://github.com/Vcholerae1/AD-TLERT
|
|
6
|
+
Project-URL: Documentation, https://github.com/Vcholerae1/AD-TLERT#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/Vcholerae1/AD-TLERT
|
|
8
|
+
Project-URL: Issues, https://github.com/Vcholerae1/AD-TLERT/issues
|
|
9
|
+
Author-email: Pu Yang <165551159+Vcholerae1@users.noreply.github.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: electrical resistivity tomography,geophysics,inverse problems,pytorch,scientific computing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: meshio>=5.3.5
|
|
22
|
+
Requires-Dist: numpy<3,>=1.26
|
|
23
|
+
Requires-Dist: scipy<2,>=1.16
|
|
24
|
+
Requires-Dist: torch<3,>=2.10
|
|
25
|
+
Provides-Extra: cuda12
|
|
26
|
+
Requires-Dist: cupy-cuda12x<15,>=14; (sys_platform == 'linux') and extra == 'cuda12'
|
|
27
|
+
Requires-Dist: nvidia-cudss-cu12<1,>=0.7.1.6; (sys_platform == 'linux') and extra == 'cuda12'
|
|
28
|
+
Requires-Dist: nvmath-python[cu12]<2,>=0.8; (sys_platform == 'linux') and extra == 'cuda12'
|
|
29
|
+
Provides-Extra: examples
|
|
30
|
+
Requires-Dist: ipykernel>=7.2; extra == 'examples'
|
|
31
|
+
Requires-Dist: joblib>=1.5; extra == 'examples'
|
|
32
|
+
Requires-Dist: matplotlib>=3.10; extra == 'examples'
|
|
33
|
+
Requires-Dist: pandas>=2.2; extra == 'examples'
|
|
34
|
+
Requires-Dist: pftools>=1.3.14; extra == 'examples'
|
|
35
|
+
Requires-Dist: pygimli<2,>=1.6; extra == 'examples'
|
|
36
|
+
Requires-Dist: pyhydrogeophysx>=0.1; extra == 'examples'
|
|
37
|
+
Requires-Dist: triangle>=20250106; extra == 'examples'
|
|
38
|
+
Provides-Extra: terrain
|
|
39
|
+
Requires-Dist: pftools>=1.3.14; extra == 'terrain'
|
|
40
|
+
Requires-Dist: triangle>=20250106; extra == 'terrain'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# AD-TLERT
|
|
44
|
+
|
|
45
|
+
Differentiable 2.5D ERT and tetrahedral 3D DC forward tooling on Torch.
|
|
46
|
+
|
|
47
|
+
The repository contains:
|
|
48
|
+
|
|
49
|
+
- `adtlert/`: the core numerical implementation.
|
|
50
|
+
- `example/single_time/`: ADTLERT/pyGIMLi forward examples and
|
|
51
|
+
ADTLERT/pyGIMLi/ResIPy single-time inversion examples.
|
|
52
|
+
- `example/window_363/`: ADTLERT/pyGIMLi/ResIPy inversion examples for 365
|
|
53
|
+
time steps using 3-step sliding windows (363 windows).
|
|
54
|
+
- `parflow_models/` and `resistivity_models_2d/`: input data used by the
|
|
55
|
+
examples.
|
|
56
|
+
|
|
57
|
+
See [`example/README.md`](example/README.md) for commands and backend
|
|
58
|
+
requirements.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
Install the CPU/SciPy build from PyPI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m pip install adtlert
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
CUDA 12 acceleration through NVIDIA cuDSS is an optional Linux extra:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python -m pip install "adtlert[cuda12]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The CUDA driver must support CUDA 12, and only one CuPy CUDA variant may be
|
|
75
|
+
installed in an environment. CPU-only installations do not install CuPy,
|
|
76
|
+
cuDSS, or nvmath-python.
|
|
77
|
+
|
|
78
|
+
For development from a checkout, use `uv`. The terrain examples read ParFlow
|
|
79
|
+
PFB files and build Triangle inversion meshes:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv sync --extra cuda12 --extra examples
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Core Forward API
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import torch
|
|
89
|
+
|
|
90
|
+
from adtlert.forward import ERTForward2p5D
|
|
91
|
+
|
|
92
|
+
forward = ERTForward2p5D.from_mesh_survey(mesh, survey)
|
|
93
|
+
conductivity = torch.as_tensor(1.0 / resistivity, dtype=torch.float32)
|
|
94
|
+
response = forward.solve(conductivity)
|
|
95
|
+
|
|
96
|
+
rhoa = response.apparent_resistivity
|
|
97
|
+
jacobian = forward.jacobian(conductivity)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For matrix-free differentiation, use the PyTorch autograd bridge. Its reverse
|
|
101
|
+
pass calls `forward.vjp(...)`, while PyTorch forward AD calls
|
|
102
|
+
`forward.jvp(...)`; no explicit Jacobian is materialized:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from adtlert.forward import apparent_resistivity_autograd
|
|
106
|
+
|
|
107
|
+
conductivity = conductivity.requires_grad_()
|
|
108
|
+
rhoa = apparent_resistivity_autograd(conductivity, forward, currents=1.0)
|
|
109
|
+
loss = data_misfit(rhoa, observed_rhoa)
|
|
110
|
+
loss.backward()
|
|
111
|
+
conductivity_gradient = conductivity.grad
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
True 3D DC forward modelling uses a tetrahedral mesh and three-coordinate
|
|
115
|
+
electrodes. It follows pyGIMLi's 3D singularity-removal formulation, with an
|
|
116
|
+
analytic half-space primary field and a mixed far-field boundary:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from adtlert.forward import ERTForward3D
|
|
120
|
+
from adtlert.mesh import Mesh3D
|
|
121
|
+
from adtlert.survey import Survey
|
|
122
|
+
|
|
123
|
+
mesh3d = Mesh3D.from_file("mesh3d.vtu")
|
|
124
|
+
survey3d = Survey.from_arrays(electrode_xyz, abmn)
|
|
125
|
+
forward3d = ERTForward3D.from_mesh_survey(mesh3d, survey3d)
|
|
126
|
+
response3d, jacobian3d = forward3d.solve_with_jacobian(conductivity3d)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For topography, use quadratic tetrahedra and numerical geometric factors. The
|
|
130
|
+
surface is inferred from upward-facing exterior triangles; pass
|
|
131
|
+
`surface_face_mask=` to `Mesh3D.from_arrays` when boundary classification must
|
|
132
|
+
be explicit:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
forward3d = ERTForward3D.from_mesh_survey(
|
|
136
|
+
mesh3d,
|
|
137
|
+
survey3d,
|
|
138
|
+
element_order=2,
|
|
139
|
+
geometric_factor_mode="auto", # numerical for terrain, analytic when flat
|
|
140
|
+
linear_solver_backend="cudss", # or "scipy"; "auto" selects an available GPU
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For large explicit sensitivities, `jacobian(..., batch_size=32,
|
|
145
|
+
cell_batch_size=20000)` bounds temporary field memory. Assembly routing,
|
|
146
|
+
active AB/MN electrode sets, numerical geometric factors, sparse plans, and
|
|
147
|
+
fixed GPU right-hand sides are cached across inversion iterations.
|
|
148
|
+
|
|
149
|
+
The initial 3D implementation supports flat-surface half-space models,
|
|
150
|
+
four-node tetrahedral geometry, P1 or ten-node P2 basis functions, SciPy or
|
|
151
|
+
NVIDIA cuDSS sparse factorization, analytic or terrain-aware numerical
|
|
152
|
+
geometric factors, exact adjoint cell sensitivities, first-order face-neighbor
|
|
153
|
+
regularization, and the existing single-time inversion API. Large-scale
|
|
154
|
+
matrix-free time-lapse inversion remains on the 3D roadmap.
|
|
155
|
+
|
|
156
|
+
`ERTForwardModeling` provides a small wrapper for mesh/data-like objects:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from adtlert.forward import ERTForwardModeling
|
|
160
|
+
|
|
161
|
+
modeling = ERTForwardModeling(mesh=mesh, data=scheme)
|
|
162
|
+
log_rhoa, jac = modeling.forward_and_jacobian(log_resistivity, log_transform=True)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Terrain Workflow
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import numpy as np
|
|
169
|
+
|
|
170
|
+
from adtlert.workflows import (
|
|
171
|
+
build_terrain_forward_case,
|
|
172
|
+
discover_resistivity_slices,
|
|
173
|
+
parse_pftcl,
|
|
174
|
+
read_slope_x,
|
|
175
|
+
run_terrain_forward,
|
|
176
|
+
run_terrain_forward_series,
|
|
177
|
+
save_terrain_forward_dat,
|
|
178
|
+
save_terrain_forward_npz,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
grid = parse_pftcl("parflow_models/sc2d_6.out.pftcl")
|
|
182
|
+
slope_x = read_slope_x("parflow_models/sc2d_6.out.slope_x.pfb", y_index=2)
|
|
183
|
+
rho_2d = np.load("resistivity_models_2d/resistivity2d_y2_t04536.npy")
|
|
184
|
+
|
|
185
|
+
case = build_terrain_forward_case(rho_2d, grid, slope_x, y_index=2)
|
|
186
|
+
rhoa = run_terrain_forward(case)
|
|
187
|
+
save_terrain_forward_dat("synthetic_ert_terrain_vardz_t04536.dat", case, rhoa)
|
|
188
|
+
save_terrain_forward_npz("synthetic_ert_terrain_vardz_t04536.npz", case, rhoa)
|
|
189
|
+
|
|
190
|
+
pairs = discover_resistivity_slices("resistivity_models_2d", y_index=2)
|
|
191
|
+
manifest, failures = run_terrain_forward_series(
|
|
192
|
+
pairs,
|
|
193
|
+
grid,
|
|
194
|
+
slope_x,
|
|
195
|
+
"result/timelapsedERT_forward",
|
|
196
|
+
y_index=2,
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Inversion API
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import numpy as np
|
|
204
|
+
|
|
205
|
+
from adtlert.inversion import ERTInversion, InversionConfig
|
|
206
|
+
|
|
207
|
+
config = InversionConfig(
|
|
208
|
+
max_iterations=8,
|
|
209
|
+
data_std=0.05,
|
|
210
|
+
regularization=1.0e-2,
|
|
211
|
+
spatial_regularization="first_order",
|
|
212
|
+
model_bounds=(10.0, 20000.0),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
initial_model = np.full(forward.mesh.cell_count, np.median(observed_rhoa))
|
|
216
|
+
result = ERTInversion(
|
|
217
|
+
forward=forward,
|
|
218
|
+
observed_data=observed_rhoa,
|
|
219
|
+
config=config,
|
|
220
|
+
).setup().run(initial_model)
|
|
221
|
+
|
|
222
|
+
final_model = result.final_model
|
|
223
|
+
predicted_rhoa = result.predicted_data
|
|
224
|
+
coverage = result.coverage
|
|
225
|
+
chi2_history = result.iteration_chi2
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Windowed time-lapse inversion:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from adtlert.inversion import WindowedTimeLapseERTInversion
|
|
232
|
+
|
|
233
|
+
config = InversionConfig(
|
|
234
|
+
max_iterations=6,
|
|
235
|
+
data_std=np.log1p(relative_error_by_time),
|
|
236
|
+
regularization=50.0,
|
|
237
|
+
temporal_regularization=10.0,
|
|
238
|
+
temporal_regularization_mode="separate",
|
|
239
|
+
spatial_regularization="first_order",
|
|
240
|
+
model_bounds=(0.001, 1.0e4),
|
|
241
|
+
max_log_step=None,
|
|
242
|
+
line_search=True,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
result = WindowedTimeLapseERTInversion(
|
|
246
|
+
forward=forward,
|
|
247
|
+
observed_data=observed_rhoa_by_time,
|
|
248
|
+
config=config,
|
|
249
|
+
window_size=3,
|
|
250
|
+
window_step=1,
|
|
251
|
+
).setup().run(initial_model)
|
|
252
|
+
|
|
253
|
+
final_models = result.final_models
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Validation
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
uv run pytest
|
|
260
|
+
uv run python -m compileall adtlert
|
|
261
|
+
uv build
|
|
262
|
+
```
|
adtlert-0.1.0/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# AD-TLERT
|
|
2
|
+
|
|
3
|
+
Differentiable 2.5D ERT and tetrahedral 3D DC forward tooling on Torch.
|
|
4
|
+
|
|
5
|
+
The repository contains:
|
|
6
|
+
|
|
7
|
+
- `adtlert/`: the core numerical implementation.
|
|
8
|
+
- `example/single_time/`: ADTLERT/pyGIMLi forward examples and
|
|
9
|
+
ADTLERT/pyGIMLi/ResIPy single-time inversion examples.
|
|
10
|
+
- `example/window_363/`: ADTLERT/pyGIMLi/ResIPy inversion examples for 365
|
|
11
|
+
time steps using 3-step sliding windows (363 windows).
|
|
12
|
+
- `parflow_models/` and `resistivity_models_2d/`: input data used by the
|
|
13
|
+
examples.
|
|
14
|
+
|
|
15
|
+
See [`example/README.md`](example/README.md) for commands and backend
|
|
16
|
+
requirements.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
Install the CPU/SciPy build from PyPI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m pip install adtlert
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
CUDA 12 acceleration through NVIDIA cuDSS is an optional Linux extra:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -m pip install "adtlert[cuda12]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The CUDA driver must support CUDA 12, and only one CuPy CUDA variant may be
|
|
33
|
+
installed in an environment. CPU-only installations do not install CuPy,
|
|
34
|
+
cuDSS, or nvmath-python.
|
|
35
|
+
|
|
36
|
+
For development from a checkout, use `uv`. The terrain examples read ParFlow
|
|
37
|
+
PFB files and build Triangle inversion meshes:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync --extra cuda12 --extra examples
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Core Forward API
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import torch
|
|
47
|
+
|
|
48
|
+
from adtlert.forward import ERTForward2p5D
|
|
49
|
+
|
|
50
|
+
forward = ERTForward2p5D.from_mesh_survey(mesh, survey)
|
|
51
|
+
conductivity = torch.as_tensor(1.0 / resistivity, dtype=torch.float32)
|
|
52
|
+
response = forward.solve(conductivity)
|
|
53
|
+
|
|
54
|
+
rhoa = response.apparent_resistivity
|
|
55
|
+
jacobian = forward.jacobian(conductivity)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For matrix-free differentiation, use the PyTorch autograd bridge. Its reverse
|
|
59
|
+
pass calls `forward.vjp(...)`, while PyTorch forward AD calls
|
|
60
|
+
`forward.jvp(...)`; no explicit Jacobian is materialized:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from adtlert.forward import apparent_resistivity_autograd
|
|
64
|
+
|
|
65
|
+
conductivity = conductivity.requires_grad_()
|
|
66
|
+
rhoa = apparent_resistivity_autograd(conductivity, forward, currents=1.0)
|
|
67
|
+
loss = data_misfit(rhoa, observed_rhoa)
|
|
68
|
+
loss.backward()
|
|
69
|
+
conductivity_gradient = conductivity.grad
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
True 3D DC forward modelling uses a tetrahedral mesh and three-coordinate
|
|
73
|
+
electrodes. It follows pyGIMLi's 3D singularity-removal formulation, with an
|
|
74
|
+
analytic half-space primary field and a mixed far-field boundary:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from adtlert.forward import ERTForward3D
|
|
78
|
+
from adtlert.mesh import Mesh3D
|
|
79
|
+
from adtlert.survey import Survey
|
|
80
|
+
|
|
81
|
+
mesh3d = Mesh3D.from_file("mesh3d.vtu")
|
|
82
|
+
survey3d = Survey.from_arrays(electrode_xyz, abmn)
|
|
83
|
+
forward3d = ERTForward3D.from_mesh_survey(mesh3d, survey3d)
|
|
84
|
+
response3d, jacobian3d = forward3d.solve_with_jacobian(conductivity3d)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For topography, use quadratic tetrahedra and numerical geometric factors. The
|
|
88
|
+
surface is inferred from upward-facing exterior triangles; pass
|
|
89
|
+
`surface_face_mask=` to `Mesh3D.from_arrays` when boundary classification must
|
|
90
|
+
be explicit:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
forward3d = ERTForward3D.from_mesh_survey(
|
|
94
|
+
mesh3d,
|
|
95
|
+
survey3d,
|
|
96
|
+
element_order=2,
|
|
97
|
+
geometric_factor_mode="auto", # numerical for terrain, analytic when flat
|
|
98
|
+
linear_solver_backend="cudss", # or "scipy"; "auto" selects an available GPU
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For large explicit sensitivities, `jacobian(..., batch_size=32,
|
|
103
|
+
cell_batch_size=20000)` bounds temporary field memory. Assembly routing,
|
|
104
|
+
active AB/MN electrode sets, numerical geometric factors, sparse plans, and
|
|
105
|
+
fixed GPU right-hand sides are cached across inversion iterations.
|
|
106
|
+
|
|
107
|
+
The initial 3D implementation supports flat-surface half-space models,
|
|
108
|
+
four-node tetrahedral geometry, P1 or ten-node P2 basis functions, SciPy or
|
|
109
|
+
NVIDIA cuDSS sparse factorization, analytic or terrain-aware numerical
|
|
110
|
+
geometric factors, exact adjoint cell sensitivities, first-order face-neighbor
|
|
111
|
+
regularization, and the existing single-time inversion API. Large-scale
|
|
112
|
+
matrix-free time-lapse inversion remains on the 3D roadmap.
|
|
113
|
+
|
|
114
|
+
`ERTForwardModeling` provides a small wrapper for mesh/data-like objects:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from adtlert.forward import ERTForwardModeling
|
|
118
|
+
|
|
119
|
+
modeling = ERTForwardModeling(mesh=mesh, data=scheme)
|
|
120
|
+
log_rhoa, jac = modeling.forward_and_jacobian(log_resistivity, log_transform=True)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Terrain Workflow
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import numpy as np
|
|
127
|
+
|
|
128
|
+
from adtlert.workflows import (
|
|
129
|
+
build_terrain_forward_case,
|
|
130
|
+
discover_resistivity_slices,
|
|
131
|
+
parse_pftcl,
|
|
132
|
+
read_slope_x,
|
|
133
|
+
run_terrain_forward,
|
|
134
|
+
run_terrain_forward_series,
|
|
135
|
+
save_terrain_forward_dat,
|
|
136
|
+
save_terrain_forward_npz,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
grid = parse_pftcl("parflow_models/sc2d_6.out.pftcl")
|
|
140
|
+
slope_x = read_slope_x("parflow_models/sc2d_6.out.slope_x.pfb", y_index=2)
|
|
141
|
+
rho_2d = np.load("resistivity_models_2d/resistivity2d_y2_t04536.npy")
|
|
142
|
+
|
|
143
|
+
case = build_terrain_forward_case(rho_2d, grid, slope_x, y_index=2)
|
|
144
|
+
rhoa = run_terrain_forward(case)
|
|
145
|
+
save_terrain_forward_dat("synthetic_ert_terrain_vardz_t04536.dat", case, rhoa)
|
|
146
|
+
save_terrain_forward_npz("synthetic_ert_terrain_vardz_t04536.npz", case, rhoa)
|
|
147
|
+
|
|
148
|
+
pairs = discover_resistivity_slices("resistivity_models_2d", y_index=2)
|
|
149
|
+
manifest, failures = run_terrain_forward_series(
|
|
150
|
+
pairs,
|
|
151
|
+
grid,
|
|
152
|
+
slope_x,
|
|
153
|
+
"result/timelapsedERT_forward",
|
|
154
|
+
y_index=2,
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Inversion API
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import numpy as np
|
|
162
|
+
|
|
163
|
+
from adtlert.inversion import ERTInversion, InversionConfig
|
|
164
|
+
|
|
165
|
+
config = InversionConfig(
|
|
166
|
+
max_iterations=8,
|
|
167
|
+
data_std=0.05,
|
|
168
|
+
regularization=1.0e-2,
|
|
169
|
+
spatial_regularization="first_order",
|
|
170
|
+
model_bounds=(10.0, 20000.0),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
initial_model = np.full(forward.mesh.cell_count, np.median(observed_rhoa))
|
|
174
|
+
result = ERTInversion(
|
|
175
|
+
forward=forward,
|
|
176
|
+
observed_data=observed_rhoa,
|
|
177
|
+
config=config,
|
|
178
|
+
).setup().run(initial_model)
|
|
179
|
+
|
|
180
|
+
final_model = result.final_model
|
|
181
|
+
predicted_rhoa = result.predicted_data
|
|
182
|
+
coverage = result.coverage
|
|
183
|
+
chi2_history = result.iteration_chi2
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Windowed time-lapse inversion:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from adtlert.inversion import WindowedTimeLapseERTInversion
|
|
190
|
+
|
|
191
|
+
config = InversionConfig(
|
|
192
|
+
max_iterations=6,
|
|
193
|
+
data_std=np.log1p(relative_error_by_time),
|
|
194
|
+
regularization=50.0,
|
|
195
|
+
temporal_regularization=10.0,
|
|
196
|
+
temporal_regularization_mode="separate",
|
|
197
|
+
spatial_regularization="first_order",
|
|
198
|
+
model_bounds=(0.001, 1.0e4),
|
|
199
|
+
max_log_step=None,
|
|
200
|
+
line_search=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
result = WindowedTimeLapseERTInversion(
|
|
204
|
+
forward=forward,
|
|
205
|
+
observed_data=observed_rhoa_by_time,
|
|
206
|
+
config=config,
|
|
207
|
+
window_size=3,
|
|
208
|
+
window_step=1,
|
|
209
|
+
).setup().run(initial_model)
|
|
210
|
+
|
|
211
|
+
final_models = result.final_models
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Validation
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
uv run pytest
|
|
218
|
+
uv run python -m compileall adtlert
|
|
219
|
+
uv build
|
|
220
|
+
```
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
]
|