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 backend requirements.

Install

Install the CPU/SciPy build from PyPI:

python -m pip install adtlert

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

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:

uv sync --extra cuda12 --extra examples

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

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.1.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.1.0
File Size Uploaded
adtlert-0.1.0.tar.gz 114.4 kB Details

Built distribution (wheel)

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

Total release size: 240.8 kB

Release files / adtlert-0.1.0.tar.gz

Download URL adtlert-0.1.0.tar.gz
Size 114.4 kB
Tags Source
SHA-256 checksum
How to use checksums
938e482b5e8170f29e8de9660fdedc63250d05f43c68be36fd86a0bf182ecdce
BLAKE2b-256 checksum
How to use checksums
066001382318f38e3e614cc5dab5b6ec38d3c3c908df4bf244324fe54ca4a9b7
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 Aug 2, 2026.

Transparency log

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

Download URL adtlert-0.1.0-py3-none-any.whl
Size 126.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1a4edf403096f9e953c64c87b78140878eca5e3cab18d3614837859b04aa3685
BLAKE2b-256 checksum
How to use checksums
a90d9ccbf8b15fcb447b5b6fa14042c3c4827ffb0c134aaf108fae8ecf182650
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 Aug 2, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.1

2 release files

0.2.0

2 release files

This release

0.1.0 This release

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