Scripting API
This page covers the full Python API: building a model, creating a solver, running propagation, and accessing results.
1. Building a model
layered_model — quick setup
layered_model builds a stratified WaveModel and initialises all physical parameters required for the requested physics.
import numpy as np
from recipes import layered_model
# 2-D, isotropic acoustic
model = layered_model(
'iso-acoustic',
shape=(101, 101),
spacing=(10.0, 10.0), # meters
nbl=40,
space_order=8,
nlayers=4,
)The first argument is a physics string; it controls which secondary parameters are generated:
| String contains | Parameters added |
|---|---|
tti or vti |
epsilon, delta, theta (and phi in 3-D) |
elastic |
vs → internally converted to lam and mu |
visco |
invqp (1/Q, uniform) |
Additional keyword arguments:
| Argument | Default | Description |
|---|---|---|
density |
False |
Derive buoyancy b = 1/ρ from Gardner’s relation |
water_layer |
False |
Add an isotropic water layer at the top |
smooth |
False |
Gaussian-smooth Thomsen parameters after creation |
Pdtype |
np.float32 |
Physical parameter storage precision |
WaveModel — full control
For production use, construct WaveModel directly with your own velocity and parameter arrays.
from recipes.model import WaveModel
shape = (201, 201)
spacing = (10., 10.)
vp = np.ones(shape, dtype=np.float32) * 2.0 # km/s
eps = np.zeros(shape, dtype=np.float32)
dlt = np.zeros(shape, dtype=np.float32)
tht = np.zeros(shape, dtype=np.float32)
model = WaveModel(
origin=(0., 0.),
spacing=spacing,
shape=shape,
space_order=8,
vp=vp,
nbl=40,
fs=False,
dtype=np.float32,
Pdtype=np.float32,
epsilon=eps,
delta=dlt,
theta=tht,
)WaveModel constructor arguments
| Argument | Description |
|---|---|
vp |
P-wave velocity array (km/s), shape (nx, [ny,] nz) |
shape |
Grid dimensions without boundary padding |
spacing |
Grid spacing (m) |
nbl |
Absorbing boundary layer width |
fs |
Conventional free surface (True/False) |
dtype |
Grid data type |
Pdtype |
Physical parameter storage precision |
b |
Buoyancy 1/ρ; scalar 1 means constant density |
epsilon, delta, theta, phi |
Thomsen anisotropy parameters |
vs |
S-wave velocity — automatically computes lam and mu |
invqp |
Inverse Q factor for viscoacoustic attenuation |
sdf |
Signed distance function for immersed-boundary topography |
interp_order |
Point source/receiver interpolation order |
water_layer |
Build water/earth subdomain partition |
After construction, physical parameters are accessible as attributes:
model.vp # devito Function
model.m # 1/vp² (derived)
model.b # buoyancy
model.epsilon # (TTI only)
model.critical_dt # maximum stable time step
model.damp # absorbing boundary field (initialised lazily)2. Creating a solver
Pick a solver class from recipes_registry and pass the model and an options dictionary:
from recipes import recipes_registry
options = {
'space_order': 8,
'nt': 600,
'f0': 0.025, # Hz — dominant Ricker frequency
'Wdtype': np.float32,
'time_slots': 3, # rolling wavefield buffer depth
't_sub': 4, # snapshot subsampling factor
}
Solver = recipes_registry['iso-acoustic']
solver = Solver(model, options)Solver options reference
| Key | Type | Default | Description |
|---|---|---|---|
space_order |
int | 4 | Spatial FD stencil order |
nt |
int | 100 | Number of time steps |
f0 |
float | 0.010 | Dominant source frequency (Hz) |
t_sub |
int | 4 | Snapshot subsampling factor |
time_slots |
int | time_order+1 |
Rolling wavefield buffer depth |
save |
str | 'lazy' |
Storage strategy: 'lazy' (host streaming), 'disk', or None |
compression |
str|None | None | Wavefield compression ('cvxcompress', 'bitcomp') |
coefficients |
str | 'taylor' |
FD coefficient type ('taylor' or 'lsqr_drp') |
interpolation |
str | 'linear' |
Source/receiver interpolation ('linear' or 'sinc') |
transient |
bool | False | Use transient TimeFunction to reduce memory traffic |
factorization |
int | 1 | Stiffness-tensor precompute mode for TTI elastic (0/1/2) |
fd_kernel_only |
bool | False | Skip source and receivers; use Gaussian IC |
abox |
bool | False | Adaptive bounding-box subdomain (active wavefield region) |
Wdtype |
dtype | np.float32 |
Wavefield data type |
save_mode |
str | 'all' |
Elastic: 'all' saves velocity and stress; 'v' saves velocity only |
opt |
tuple | auto | Devito Operator optimisation options, e.g. ('advanced', {'openmp': True}) |
3. Running propagation
Forward pass
# Returns a Devito PerformanceSummary
summary = solver.forward()
print(summary)To save wavefields for gradient computation:
summary = solver.forward(save=True)Accessing results
# Last pressure snapshot as a numpy array
p = solver.pressure_data
# Recorded traces at receivers — shape (nt, nrec)
traces = solver.rec.dataSource and receiver objects
The default acquisition uses a single Ricker point source placed at the centre of the domain and a surface receiver array:
src = solver.src # devito RickerSource
rec = solver.rec # devito Receiver
# Reposition the source
src.coordinates.data[0] = [500., 20.] # [x, z] in meters
# Inspect the receiver layout
print(rec.coordinates.data) # (nrec, ndim) arrayBoth objects are built lazily and cached. If you need non-default acquisition geometries, override acquisition_eq — see Extending the Framework.
Gradient computation
After a forward pass with save=True, inject residuals into rec.data and call jacobian_adjoint:
solver.rec.data[:] = observed - solver.rec.data # residuals
summary = solver.jacobian_adjoint(params='vp')
# Gradient is accumulated in a persistent Function
dm = solver.perturbation(param='vp')
print(dm.data.shape)Multiple parameters in one adjoint pass:
solver.jacobian_adjoint(params=('vp', 'epsilon'))
dm_vp = solver.perturbation(param='vp')
dm_eps = solver.perturbation(param='epsilon')4. Wavefield compression
When compression is set, saved wavefields are stored in compressed form. The compression ratio is available after the forward pass:
import numpy as np
options = {
'compression': 'cvxcompress',
'save': 'lazy',
'Wdtype': np.float16,
}
solver = recipes_registry['iso-acoustic'](model, options)
solver.forward(save=True)
cr = solver.saved_state['u_save'].compression_ratio
print(f"Compression ratio: {float(np.mean(cr)):.2f}")5. Per-operator optimisation
Pass an opt dictionary keyed by operator name to apply different optimisation levels to the forward and gradient passes:
opt = {
'ForwardAcousticIsotropic': ('advanced', {'openmp': True}),
'JacobianAdjAcousticIsotropic': ('advanced', {}),
}
solver.forward(save=True, opt=opt)
solver.jacobian_adjoint(params='vp', opt=opt)