Model

The WaveModel class (recipes/model.py) extends Devito’s SeismicModel with production-oriented features: mixed-precision physical parameters, lazy absorbing-boundary initialisation, water/earth layer subdomains, and immersed free-surface support via signed distance functions.


1. Built-in construction

layered_model — quick stratified model

from recipes import layered_model

model = layered_model(
    'iso-acoustic',          # physics string — controls which parameters are built
    shape=(101, 101),
    spacing=(10.0, 10.0),   # meters
    nbl=40,
    space_order=8,
    nlayers=4,
)

The physics string determines which secondary parameters are generated:

String contains Extra parameters created
tti or vti epsilon, delta, theta (and phi in 3-D)
elastic vs → converts to lam and mu
visco invqp (uniform 1/Q)

Extra keyword arguments forwarded to WaveModel:

Argument Default Description
density False Derive buoyancy b = 1/ρ from Gardner’s relation
water_layer False Add an isotropic acoustic water layer subdomain at the top
smooth False Gaussian-smooth Thomsen parameters after construction
Pdtype np.float32 Storage precision for physical parameters

WaveModel — direct construction

import numpy as np
from recipes.model import WaveModel

vp  = np.ones((101, 101), dtype=np.float32) * 2.0   # km/s
eps = np.zeros((101, 101), dtype=np.float32)

model = WaveModel(
    origin=(0., 0.),
    spacing=(10., 10.),
    shape=(101, 101),
    space_order=8,
    vp=vp,
    nbl=40,
    fs=False,
    epsilon=eps,
    delta=np.zeros_like(eps),
    theta=np.zeros_like(eps),
    Pdtype=np.float32,
)

Constructor arguments

Argument Required Description
vp always P-wave velocity array (km/s)
shape always Grid dimensions without boundary padding
spacing always Grid spacing (m)
space_order always Finite-difference stencil order
nbl always Absorbing boundary layer width (cells)
origin always Physical coordinates of the first grid point
fs no Conventional flat free surface at the top
dtype no Grid floating-point type (default np.float32)
Pdtype no Physical parameter storage precision (default np.float32)
b no Buoyancy 1/ρ; scalar 1 = constant density
epsilon, delta, theta, phi no Thomsen anisotropy parameters
vs no S-wave velocity → lam and mu computed automatically
invqp no Inverse Q factor for viscoacoustic attenuation
sdf no Signed distance function for immersed free-surface topography
interp_order no Source/receiver interpolation order (default 2)
water_layer no Build water/earth subdomain partition

2. Using your own model

The solvers only require the model to expose a fixed set of attributes and methods. You can bring your own class as long as it satisfies this interface — you do not have to subclass WaveModel.

2.1 Mandatory attributes (all solvers)

These are accessed by Propagator unconditionally:

Attribute Type Description
grid devito.Grid Devito grid shared by all Functions
spacing tuple of float Physical grid spacing (m) per dimension
shape tuple of int Grid shape without boundary padding
domain_size tuple of float Physical extent: (shape[i]-1) * spacing[i]
space_order int FD stencil order
nbl int Absorbing layer width
critical_dt float Maximum stable time step (CFL condition)
spacing_map dict Maps symbolic spacings and dt to their numerical values; passed as subs to every Operator
damp Function or scalar Absorbing boundary field ('damp' convention: 0 inside, positive in layer; 'mask' convention: 1 inside, decreasing in layer). Lazily initialised — call model.initialize_damp(abc_type=...) before the first operator application
physical_params(**kwargs) method Returns a dict mapping each physical parameter Function to its data, suitable for passing to op.apply. Optional kwargs override individual values
fs bool Free-surface flag
has_sdf bool Whether an immersed boundary (SDF) is present

2.2 Physics-specific parameters

Each solver reads specific attributes from the model. The table below lists which attribute each physics requires. All of them must be either a devito.Function (array-valued) or a devito.Constant (scalar).

Attribute Solvers that use it Description
m acoustic, TTI Squared slowness 1/vp²
vp acoustic, TTI, elastic P-wave velocity (km/s)
b all Buoyancy 1/ρ; constant 1 for homogeneous density
lam elastic, elastic-TTI Lamé parameter λ
mu elastic, elastic-TTI Lamé parameter μ
vs elastic, elastic-TTI S-wave velocity (km/s)
mu0 elastic, elastic-TTI Scalar value of μ at the first grid point (used to detect fluid layers)
epsilon TTI, VTI Thomsen ε
delta TTI, VTI Thomsen δ
theta TTI (3-D and 2-D) Tilt angle (radians)
phi TTI 3-D only Azimuth angle (radians)
eps Fletcher TTI Alias for epsilon
sig Fletcher TTI Symmetry parameter σ (typically 0.5)
invqp viscoacoustic Inverse Q factor 1/Qp
is_visco acoustic, TTI Boolean — True if invqp is non-zero

2.3 Optional subdomain attributes

The following Grid subdomains are looked up by name at runtime. Provide them only when the corresponding feature is needed.

Subdomain name Accessed via Purpose
'coredomain' model.grid.subdomains Interior below an immersed free surface (SDF)
'boundarydomain' model.grid.subdomains Region near the immersed surface where modified stencils apply
'waterlayer' model.grid.subdomains Isotropic acoustic top region
'earthlayer' model.grid.subdomains Anisotropic / elastic region below the water

2.4 Immersed free surface

When has_sdf is True the solver also calls:

model._immersed_fs(boundary_subdomain, bcs, *stencil_groups)

This method returns modified stencil groups with boundary-corrected FD coefficients near the surface. Implement it only if you need immersed free-surface support; for flat free surfaces use model.fs = True and the mirror / mirror_source utilities instead.

2.5 Minimal custom model example

import numpy as np
from devito import Grid, Function, Constant
from devito.builtins import initialize_function


class MyModel:
    """
    Minimal acoustic model compatible with AcousticIsotropic.
    """

    def __init__(self, shape, spacing, vp, nbl=40, space_order=8):
        self.shape    = shape
        self.spacing  = spacing
        self.nbl      = nbl
        self.space_order = space_order
        self.fs       = False
        self.has_sdf  = False

        pad = [(nbl, nbl)] * (len(shape) - 1) + [(nbl, nbl)]
        padded = tuple(n + l + r for n, (l, r) in zip(shape, pad))
        self.grid = Grid(shape=padded,
                         extent=tuple((n-1)*d for n, d in zip(padded, spacing)),
                         dtype=np.float32)

        # P-wave velocity and derived quantities
        so = (space_order, space_order//2, space_order//2)
        self.vp  = Function(name='vp',  grid=self.grid, space_order=so)
        self.m   = Function(name='m',   grid=self.grid, space_order=so)
        self.b   = Constant(name='b',   value=1.0)
        self.damp = Function(name='damp', grid=self.grid, space_order=so)

        initialize_function(self.vp, vp, pad)
        self.m.data[:] = 1.0 / self.vp.data**2

    @property
    def domain_size(self):
        return tuple((n - 1) * d for n, d in zip(self.shape, self.spacing))

    @property
    def critical_dt(self):
        # Simple CFL estimate
        return 0.9 * min(self.spacing) / (np.sqrt(self.grid.dim) * self._vp_max)

    @property
    def spacing_map(self):
        mapper = dict(self.grid.spacing_map)
        mapper[self.grid.time_dim.spacing] = self.critical_dt
        return mapper

    def initialize_damp(self, abc_type='damp'):
        # Populate self.damp with your absorbing profile
        ...

    def physical_params(self, **kwargs):
        params = {self.vp: self.vp.data,
                  self.m:  self.m.data,
                  self.b:  float(self.b.data)}
        params.update(kwargs)
        return params

3. Absorbing boundaries

WaveModel initialises the damping field lazily: damp is allocated in __init__ but filled only the first time an operator is applied, via initialize_damp(abc_type). Two conventions are supported:

Convention _abc value on solver damp profile
Additive 'damp' 0 inside the domain, grows toward the edges
Multiplicative mask 'mask' 1 inside the domain, decreases toward the edges

The Propagator.run_propagation wrapper checks which convention the solver declares (_abc) and re-initialises damp automatically if the stored data does not match.

To skip absorbing boundaries entirely (e.g. for a pure kernel benchmark), pass nbl=0 or set model.damp = 0 (additive) / model.damp = 1 (mask).


4. Physical parameter precision

WaveModel stores parameters at the precision specified by Pdtype (default np.float32). When Pdtype=np.float16, DevitoPRO quantization is applied instead of raw float16 to maintain accuracy over the narrow value range typical of seismic parameters:

model = WaveModel(..., Pdtype=np.float16)

Wavefields use a separate precision controlled by the solver option Wdtype.

Back to top