Metadata-Version: 2.4
Name: adtlert
Version: 0.1.0
Summary: Differentiable 2.5D and 3D electrical resistivity tomography on PyTorch
Project-URL: Homepage, https://github.com/Vcholerae1/AD-TLERT
Project-URL: Documentation, https://github.com/Vcholerae1/AD-TLERT#readme
Project-URL: Repository, https://github.com/Vcholerae1/AD-TLERT
Project-URL: Issues, https://github.com/Vcholerae1/AD-TLERT/issues
Author-email: Pu Yang <165551159+Vcholerae1@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: electrical resistivity tomography,geophysics,inverse problems,pytorch,scientific computing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.11
Requires-Dist: meshio>=5.3.5
Requires-Dist: numpy<3,>=1.26
Requires-Dist: scipy<2,>=1.16
Requires-Dist: torch<3,>=2.10
Provides-Extra: cuda12
Requires-Dist: cupy-cuda12x<15,>=14; (sys_platform == 'linux') and extra == 'cuda12'
Requires-Dist: nvidia-cudss-cu12<1,>=0.7.1.6; (sys_platform == 'linux') and extra == 'cuda12'
Requires-Dist: nvmath-python[cu12]<2,>=0.8; (sys_platform == 'linux') and extra == 'cuda12'
Provides-Extra: examples
Requires-Dist: ipykernel>=7.2; extra == 'examples'
Requires-Dist: joblib>=1.5; extra == 'examples'
Requires-Dist: matplotlib>=3.10; extra == 'examples'
Requires-Dist: pandas>=2.2; extra == 'examples'
Requires-Dist: pftools>=1.3.14; extra == 'examples'
Requires-Dist: pygimli<2,>=1.6; extra == 'examples'
Requires-Dist: pyhydrogeophysx>=0.1; extra == 'examples'
Requires-Dist: triangle>=20250106; extra == 'examples'
Provides-Extra: terrain
Requires-Dist: pftools>=1.3.14; extra == 'terrain'
Requires-Dist: triangle>=20250106; extra == 'terrain'
Description-Content-Type: text/markdown

# AD-TLERT

Differentiable 2.5D ERT and tetrahedral 3D DC forward tooling on Torch.

The repository contains:

- `adtlert/`: the core numerical implementation.
- `example/single_time/`: ADTLERT/pyGIMLi forward examples and
  ADTLERT/pyGIMLi/ResIPy single-time inversion examples.
- `example/window_363/`: ADTLERT/pyGIMLi/ResIPy inversion examples for 365
  time steps using 3-step sliding windows (363 windows).
- `parflow_models/` and `resistivity_models_2d/`: input data used by the
  examples.

See [`example/README.md`](example/README.md) for commands and backend
requirements.

## Install

Install the CPU/SciPy build from PyPI:

```bash
python -m pip install adtlert
```

CUDA 12 acceleration through NVIDIA cuDSS is an optional Linux extra:

```bash
python -m pip install "adtlert[cuda12]"
```

The CUDA driver must support CUDA 12, and only one CuPy CUDA variant may be
installed in an environment. CPU-only installations do not install CuPy,
cuDSS, or nvmath-python.

For development from a checkout, use `uv`. The terrain examples read ParFlow
PFB files and build Triangle inversion meshes:

```bash
uv sync --extra cuda12 --extra examples
```

## Core Forward API

```python
import torch

from adtlert.forward import ERTForward2p5D

forward = ERTForward2p5D.from_mesh_survey(mesh, survey)
conductivity = torch.as_tensor(1.0 / resistivity, dtype=torch.float32)
response = forward.solve(conductivity)

rhoa = response.apparent_resistivity
jacobian = forward.jacobian(conductivity)
```

For matrix-free differentiation, use the PyTorch autograd bridge. Its reverse
pass calls `forward.vjp(...)`, while PyTorch forward AD calls
`forward.jvp(...)`; no explicit Jacobian is materialized:

```python
from adtlert.forward import apparent_resistivity_autograd

conductivity = conductivity.requires_grad_()
rhoa = apparent_resistivity_autograd(conductivity, forward, currents=1.0)
loss = data_misfit(rhoa, observed_rhoa)
loss.backward()
conductivity_gradient = conductivity.grad
```

True 3D DC forward modelling uses a tetrahedral mesh and three-coordinate
electrodes. It follows pyGIMLi's 3D singularity-removal formulation, with an
analytic half-space primary field and a mixed far-field boundary:

```python
from adtlert.forward import ERTForward3D
from adtlert.mesh import Mesh3D
from adtlert.survey import Survey

mesh3d = Mesh3D.from_file("mesh3d.vtu")
survey3d = Survey.from_arrays(electrode_xyz, abmn)
forward3d = ERTForward3D.from_mesh_survey(mesh3d, survey3d)
response3d, jacobian3d = forward3d.solve_with_jacobian(conductivity3d)
```

For topography, use quadratic tetrahedra and numerical geometric factors. The
surface is inferred from upward-facing exterior triangles; pass
`surface_face_mask=` to `Mesh3D.from_arrays` when boundary classification must
be explicit:

```python
forward3d = ERTForward3D.from_mesh_survey(
    mesh3d,
    survey3d,
    element_order=2,
    geometric_factor_mode="auto",  # numerical for terrain, analytic when flat
    linear_solver_backend="cudss",  # or "scipy"; "auto" selects an available GPU
)
```

For large explicit sensitivities, `jacobian(..., batch_size=32,
cell_batch_size=20000)` bounds temporary field memory. Assembly routing,
active AB/MN electrode sets, numerical geometric factors, sparse plans, and
fixed GPU right-hand sides are cached across inversion iterations.

The initial 3D implementation supports flat-surface half-space models,
four-node tetrahedral geometry, P1 or ten-node P2 basis functions, SciPy or
NVIDIA cuDSS sparse factorization, analytic or terrain-aware numerical
geometric factors, exact adjoint cell sensitivities, first-order face-neighbor
regularization, and the existing single-time inversion API. Large-scale
matrix-free time-lapse inversion remains on the 3D roadmap.

`ERTForwardModeling` provides a small wrapper for mesh/data-like objects:

```python
from adtlert.forward import ERTForwardModeling

modeling = ERTForwardModeling(mesh=mesh, data=scheme)
log_rhoa, jac = modeling.forward_and_jacobian(log_resistivity, log_transform=True)
```

## Terrain Workflow

```python
import numpy as np

from adtlert.workflows import (
    build_terrain_forward_case,
    discover_resistivity_slices,
    parse_pftcl,
    read_slope_x,
    run_terrain_forward,
    run_terrain_forward_series,
    save_terrain_forward_dat,
    save_terrain_forward_npz,
)

grid = parse_pftcl("parflow_models/sc2d_6.out.pftcl")
slope_x = read_slope_x("parflow_models/sc2d_6.out.slope_x.pfb", y_index=2)
rho_2d = np.load("resistivity_models_2d/resistivity2d_y2_t04536.npy")

case = build_terrain_forward_case(rho_2d, grid, slope_x, y_index=2)
rhoa = run_terrain_forward(case)
save_terrain_forward_dat("synthetic_ert_terrain_vardz_t04536.dat", case, rhoa)
save_terrain_forward_npz("synthetic_ert_terrain_vardz_t04536.npz", case, rhoa)

pairs = discover_resistivity_slices("resistivity_models_2d", y_index=2)
manifest, failures = run_terrain_forward_series(
    pairs,
    grid,
    slope_x,
    "result/timelapsedERT_forward",
    y_index=2,
)
```

## Inversion API

```python
import numpy as np

from adtlert.inversion import ERTInversion, InversionConfig

config = InversionConfig(
    max_iterations=8,
    data_std=0.05,
    regularization=1.0e-2,
    spatial_regularization="first_order",
    model_bounds=(10.0, 20000.0),
)

initial_model = np.full(forward.mesh.cell_count, np.median(observed_rhoa))
result = ERTInversion(
    forward=forward,
    observed_data=observed_rhoa,
    config=config,
).setup().run(initial_model)

final_model = result.final_model
predicted_rhoa = result.predicted_data
coverage = result.coverage
chi2_history = result.iteration_chi2
```

Windowed time-lapse inversion:

```python
from adtlert.inversion import WindowedTimeLapseERTInversion

config = InversionConfig(
    max_iterations=6,
    data_std=np.log1p(relative_error_by_time),
    regularization=50.0,
    temporal_regularization=10.0,
    temporal_regularization_mode="separate",
    spatial_regularization="first_order",
    model_bounds=(0.001, 1.0e4),
    max_log_step=None,
    line_search=True,
)

result = WindowedTimeLapseERTInversion(
    forward=forward,
    observed_data=observed_rhoa_by_time,
    config=config,
    window_size=3,
    window_step=1,
).setup().run(initial_model)

final_models = result.final_models
```

## Validation

```bash
uv run pytest
uv run python -m compileall adtlert
uv build
```
