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.0

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.0
File Size Uploaded
adtlert-0.2.0.tar.gz 122.2 kB Details

Built distribution (wheel)

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

Total release size: 248.1 kB

Release files / adtlert-0.2.0.tar.gz

Download URL adtlert-0.2.0.tar.gz
Size 122.2 kB
Tags Source
SHA-256 checksum
How to use checksums
b402f0a828d1ae1d81e3c1680e74a798235c93c3fd00c9bb38d75c44a9d0a12b
BLAKE2b-256 checksum
How to use checksums
d41b0649cb403e0317eb03dca8ab9bc860370a33ad3deab63aeb7422de489cda
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.0-py3-none-any.whl

Download URL adtlert-0.2.0-py3-none-any.whl
Size 125.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
304fe679ab4dc1fdb2b428c4d8e6c7f55a2bb0cc3055a5649c2389c42125991f
BLAKE2b-256 checksum
How to use checksums
bb494463d36420eb3981d0c7b044409def4cbfae80a331b86bf210cfb24b7bdc
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

0.2.1

2 release files

This release

0.2.0 This release

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