Skip to main content

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 for commands and dependency requirements.

Install

ADTLERT is a CUDA library: every sparse solve runs on NVIDIA cuDSS and the adjoint contractions run on the GPU. It requires Linux, an NVIDIA GPU, and a driver that supports CUDA 12. cuDSS and nvmath-python are installed as regular dependencies; the fused sensitivity kernels are written in Triton, which ships with PyTorch:

python -m pip install adtlert

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

uv sync --extra examples

Tests

uv run pytest            # CPU-only tests plus, on a CUDA machine, the GPU tests
uv run pytest -m "not gpu"

The suite checks mesh/FEM building blocks, adjoint consistency (dot tests), the exact Jacobian against finite differences and the resistivity-scaling identity, the Triton sensitivity kernel against an einsum reference (bitwise identical across autotune configurations), deterministic reductions, the inversion loops, and the implicit-neural-representation training.

Core Forward API

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:

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:

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:

forward3d = ERTForward3D.from_mesh_survey(
    mesh3d,
    survey3d,
    element_order=2,
    geometric_factor_mode="auto",  # numerical for terrain, analytic when flat
)

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, 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:

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

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

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:

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

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

Metadata

Release files for adtlert 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for adtlert 0.2.1
File Size Uploaded
adtlert-0.2.1.tar.gz 125.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for adtlert 0.2.1
File Interpreter ABI Platform
adtlert-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 254.1 kB

Release files / adtlert-0.2.1.tar.gz

Download URL adtlert-0.2.1.tar.gz
Size 125.9 kB
Tags Source
SHA-256 checksum
How to use checksums
17a5bd8e32e18ec6f44931c17c93bd63b35a80dac385d08a2f8fc873ca8578c3
BLAKE2b-256 checksum
How to use checksums
fdadb34f9afc5a9689c21d04153a489412a10bcc39060b517ae4020216292c36
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / adtlert-0.2.1-py3-none-any.whl

Download URL adtlert-0.2.1-py3-none-any.whl
Size 128.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a5d0e979c007664517ace2b9bc451e71e15a71636f9128edb0f60cfad9b0b42e
BLAKE2b-256 checksum
How to use checksums
13290824d070d7c15a6b596f39c6ef890a37b2ae526e21ff46dab290105684d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page