Quick start#
One problem, solved twice, deterministically and then Bayesianly, with the physics assembled from two operators. Every output below is what the script actually prints.
1. A mesh, and a chain of operators on it#
import torch
from geobrain.core import ForwardContext, ModelState
from geobrain.mesh import TensorMesh
from geobrain.physics.rock.models import GardnerOperator
from geobrain.physics.potential import Gravity2D, PotentialSurvey2D
torch.manual_seed(0) # so the numbers below reproduce
mesh = TensorMesh(shape=(12, 24), spacing=(25.0, 25.0))
ctx = ForwardContext.of(mesh=mesh)
stations = torch.stack([torch.linspace(0.0, 600.0, 25, dtype=torch.float64),
torch.full((25,), 1.0, dtype=torch.float64)], dim=1)
# vp -> rho (Gardner) -> gz (Talwani). '@' is operator composition.
forward = Gravity2D(PotentialSurvey2D(stations)) @ GardnerOperator()
print(forward.differentiability)
DifferentiabilitySpec(level=<DifferentiabilityLevel.CUSTOM_VJP: 'custom_vjp'>,
trainable_inputs=('vp',), output_keys=('gz',),
structural_params=(), input_units={})
Read that line, because it is the whole idea. You composed a rock-physics
transform with a gravity kernel; the chain worked out that what you can
train is vp, that what comes out is gz, and that the gradient will arrive
through a hand-written VJP, the weakest mechanism of the two links. Nothing
asserted it. Ask a chain that cannot be honoured and it refuses at build time.
Note
dtype=torch.float64 is not decoration. The potential-field operators are
strict about it, and a float32 survey is rejected rather than quietly losing
precision in a kernel that sums over every cell.
2. Synthetic data from a known earth#
vp_true = 1800.0 + 40.0 * torch.arange(12, dtype=torch.float64)[:, None]
vp_true = vp_true.expand(12, 24).clone()
vp_true[4:8, 8:16] += 400.0 # a fast, dense block
with torch.no_grad():
observed = forward(ModelState({"vp": vp_true}), ctx).data["gz"]
observed = observed + 2.0e-8 * torch.randn_like(observed)
A ModelState is the bag of tensors the physics reads; a ForwardContext
carries what is not a model: here, the mesh. Keeping them apart is what lets
the same operator run on a different mesh without being rebuilt.
3. One problem object#
from geobrain.inverse import GaussianLikelihood, InverseProblem
problem = InverseProblem(forward=forward, observed={"gz": observed},
likelihood=GaussianLikelihood(std=2.0e-8))
This is the object both solvers below take. It knows the forward model, the data, and how the data are distributed around it, and nothing about how you intend to search.
4a. Deterministic: the best-fitting model#
from geobrain.optim import LBFGSConfig
start = {"vp": (1800.0 + 40.0 * torch.arange(12, dtype=torch.float64)[:, None]
).expand(12, 24).clone()}
result = problem.create_inverter(
params=start, optimizer=LBFGSConfig(lr=1.0, max_iter=30),
bounds={"vp": (1500.0, 4000.0)}, ctx=ctx).run(n_iters=10)
print(f"misfit {result.best_loss:.4g} after {result.completed_iters} iters")
misfit 8.232e-09 after 10 iters
4b. Bayesian: the models still standing#
from geobrain.bayes import PositiveTransform
posterior = problem.as_posterior(transforms={"vp": PositiveTransform()})
draws = posterior.sample("nuts", params=result.best_params, n_iters=100,
ctx=ctx, warmup=50, max_depth=5)
print("posterior sd [m/s]:", draws.samples["vp"].std(0).mean().item())
posterior sd [m/s]: 29.630669631493827
The pivot is as_posterior() on the problem you already built, followed by one
sample() call. "hmc", "langevin" and "svgd" are the same call with a
different string. The PositiveTransform is a change of variable, so the
sampler explores an unbounded space while vp stays positive by construction.
The number is the point of doing it at all. The deterministic run returned one velocity model; the posterior says the data leave about 30 m/s of room around it, which is the difference between an answer and an answer you can act on. Twenty-five gravity stations over a 12x24 grid is a badly underdetermined problem, so read that spread as what this survey pins down, not as a general result.
The same pivot this page walks through, run on an AVO problem: one problem
object solved deterministically, then sampled. From
examples/00_showcase/04_deterministic_bayes_unified.py.#
Where next#
Architecture: the contracts underneath all of this.
Examples gallery: twenty-nine runnable scripts, from a three-object minimum to full multiphysics inversions.