Utilities reference¶
The shared machinery every propagator uses or returns: the output and callback types, the type aliases, checkpointing, storage, planning and diagnostics, the discretisation helpers, off-grid source and receiver locations, wavelets, transducer arrays, misfit functions, wavefield-space tools, and the parameter converters. Usage explains when to reach for each of them.
Anisotropy parameter converters¶
Each anisotropic propagator takes the parameterisation it actually solves in, which is not the one models are usually stored or inverted in. deepwave.elastic_vti() takes the VTI stiffnesses c11, c13, c33, c55 (and c66 in 3-D) together with the buoyancy \(1/\rho\); deepwave.acoustic_vti() takes the same stiffnesses less the shear ones (c11, c13, c33) and the buoyancy. Practitioners, by contrast, reason in Thomsen’s \((v_{P0}, v_{S0}, \rho, \epsilon, \delta, \gamma)\) parameters, or – when the model came from surface P-wave velocity analysis, which cannot resolve \(\epsilon\) and \(\delta\) separately – in Alkhalifah and Tsvankin’s \((v_{P0}, v_{nmo}, \eta)\). The deepwave.anisotropy module holds the exact (not weak-anisotropy linearised) conversions between them, plus the near-source taper deepwave.acoustic_vti() sometimes needs. Every one of them is ordinary differentiable tensor arithmetic, so inverting in Thomsen or Alkhalifah-Tsvankin space is just a matter of building the propagator’s inputs inside the loss and letting autograd chain through (see Model parameterisations and Thomsen-space FWI with the pseudo-acoustic VTI propagator).
thomsen_to_cij returns the buoyancy alongside the stiffnesses, so its whole return value unpacks straight into a propagator call, and a vacuum cell (\(\rho = 0\)) becomes buoyancy = 0, the sentinel the propagators expect, rather than the infinity a plain 1 / rho would give.
Converter | From | To |
|---|---|---|
|
| |
|
| |
|
| |
|
| |
| a modified |
The isotropic pair deepwave.vpvsrho_to_lambmubuoyancy() / deepwave.lambmubuoyancy_to_vpvsrho() stays at the package root: it is not anisotropic, and it shipped at that spelling.
- deepwave.anisotropy.thomsen_to_cij(vp: Tensor, vs: Tensor, rho: Tensor, epsilon: Tensor, delta: Tensor, gamma: Tensor | None = None) tuple[Tensor, Tensor, Tensor, Tensor, Tensor | None, Tensor]¶
Converts Thomsen (1986) parameters to VTI stiffnesses.
Uses the exact (not weak-anisotropy linearised) relations. With the symmetry axis as the Voigt “3” direction:
c33 = rho * vp**2 c55 = rho * vs**2 c11 = c33 * (1 + 2 * epsilon) c66 = c55 * (1 + 2 * gamma) c13 = sqrt(2 * delta * c33 * (c33 - c55) + (c33 - c55)**2) - c55 buoyancy = 1 / rho
where
vpandvsare the P and S wave speeds along the symmetry axis. Thec13expression inverts Thomsen’s exact definitiondelta = ((c13 + c55)**2 - (c33 - c55)**2) / (2 c33 (c33 - c55)), taking the positive square-root branch, which corresponds toc13 + c55 > 0. This branch holds for virtually all real materials; “anomalous” media withc13 + c55 < 0cannot be represented in Thomsen parameters unambiguously and are not supported. The argument of the square root is non-negative iffdelta >= -(c33 - c55) / (2 c33); more negativedeltavalues are rejected with an error. Cells withrho = 0(orvp = vs = 0), the vacuum convention of the elastic propagators, map to all-zero stiffnesses (the square root is masked there so no NaN gradients are produced) and tobuoyancy = 0exactly, which is the vacuum sentinel the propagators themselves expect.The buoyancy (the reciprocal of the density) is returned alongside the stiffnesses because it, not
rho, is whatdeepwave.elastic_vti()anddeepwave.acoustic_vti()take, so the returned tuple can be passed straight to them. It is computed with the same zero-guarded reciprocal asdeepwave.vpvsrho_to_lambmubuoyancy(), so a vacuum cell yields exactly0rather thaninf.The function is differentiable, so gradients with respect to the Thomsen parameters may be obtained by chaining autograd through it (inverting in Thomsen space).
- Parameters:
vp – P-wave speed along the symmetry axis.
vs – S-wave speed along the symmetry axis.
rho – Density.
epsilon – Thomsen’s epsilon,
(c11 - c33) / (2 c33).delta – Thomsen’s delta (exact definition, see above).
gamma – Thomsen’s gamma,
(c66 - c55) / (2 c55). Required to buildc66, which is only used in 3-D. Optional, default None.
- Returns:
(c11, c13, c33, c55, c66, buoyancy), withc66set toNonewhengammaisNone. In 2-D, wherec66does not enter the equations, unpack it into_; in 3-D pass it to the propagator’sc66keyword argument.- Return type:
Tuple[torch.Tensor, …]
- Raises:
DeepwaveValueError – If
vp,vsorrhocontains negative values, ifc33 < c55(vp < vs) anywhere, or ifdeltais more negative than-(c33 - c55) / (2 c33)anywhere.
- deepwave.anisotropy.cij_to_thomsen(c11: Tensor, c13: Tensor, c33: Tensor, c55: Tensor, c66: Tensor | None, buoyancy: Tensor) tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor | None]¶
Converts VTI stiffnesses to Thomsen (1986) parameters.
The exact inverse of
deepwave.anisotropy.thomsen_to_cij()(not the weak-anisotropy linearised approximation). With the symmetry axis as the Voigt “3” direction, this inverts the relations documented indeepwave.anisotropy.thomsen_to_cij():rho = 1 / buoyancy vp = sqrt(c33 * buoyancy) vs = sqrt(c55 * buoyancy) epsilon = (c11 - c33) / (2 * c33) delta = ((c13 + c55)**2 - (c33 - c55)**2) / (2 * c33 * (c33 - c55)) gamma = (c66 - c55) / (2 * c55)
The arguments are in the order
deepwave.anisotropy.thomsen_to_cij()returns them, and the return values are in the orderdeepwave.anisotropy.thomsen_to_cij()takes them, so the two compose directly in both directions:cij_to_thomsen(*thomsen_to_cij(vp, vs, rho, epsilon, delta, gamma))round-trips to the inputs (to floating-point precision) whenevervp > vs(soc33 != c55anddeltais not degenerate) andrho > 0, andthomsen_to_cij(*cij_to_thomsen(...))likewise returns the stiffnesses it was given. Note that this function takesbuoyancy, notrho: that is what the propagators use, and whatdeepwave.anisotropy.thomsen_to_cij()now returns.A vacuum cell (
buoyancy == 0, equivalentlyrho == 0, the vacuum convention of the elastic propagators) has no recoverable Thomsen parameters at all:deepwave.anisotropy.thomsen_to_cij()maps every such cell to all-zero stiffnesses whatever itsvp/vs/epsilon/delta/gammawere. It is mapped here tovp = vs = rho = epsilon = delta = gamma = 0, which is the canonical vacuum representative: feeding it back todeepwave.anisotropy.thomsen_to_cij()reproduces exactly the all-zero stiffnesses andbuoyancy = 0it came from. The masking also keeps the gradients there finite.The
vp == vs(isotropic,c33 == c55) case is also singular –deepwave.anisotropy.thomsen_to_cij()maps everydeltato the samec13there – but, unlike vacuum, it has no canonical representative to collapse to, so it is deliberately not special-cased: the formula above is evaluated directly and produces a division by zero (nan/inf) for such cells, loudly, rather than inventing a value. The same applies togammawhenc55 == 0(a pseudo-acoustic,vs = 0model).- Parameters:
c11 – The c11 stiffness.
c13 – The c13 stiffness.
c33 – The c33 stiffness.
c55 – The c55 stiffness (shear modulus along the symmetry axis).
c66 – The c66 stiffness (horizontal-plane shear modulus), or
Noneif it is not available (in 2-D it does not enter the equations).gammais thenNonetoo.buoyancy – The reciprocal of density, as the propagators take it.
- Returns:
(vp, vs, rho, epsilon, delta, gamma), withgammaset toNonewhenc66isNone.- Return type:
Tuple[torch.Tensor, …]
- Raises:
DeepwaveValueError – If
buoyancycontains negative values.
- deepwave.anisotropy.alkhalifah_to_thomsen(vp: Tensor, vnmo: Tensor, eta: Tensor) tuple[Tensor, Tensor]¶
Converts Alkhalifah-Tsvankin (1995) parameters to Thomsen (1986) epsilon/delta.
P-wave surface seismic data alone cannot resolve the full Thomsen parameter set: time processing (NMO, DMO, time migration) in a VTI medium is controlled only by the P-wave NMO velocity and the anellipticity parameter
eta, not byepsilon/deltaindividually (Alkhalifah and Tsvankin, “Velocity analysis for transversely isotropic media”, Geophysics 60, 1995, 1550-1566). This helper converts that P-wave-derivable parameterisation back to Thomsen’sepsilon/deltausing the exact relations:delta = (vnmo**2 / vp**2 - 1) / 2 epsilon = eta * (1 + 2 * delta) + delta
which invert Alkhalifah and Tsvankin’s defining pair
vnmo = vp * sqrt(1 + 2*delta)andeta = (epsilon - delta) / (1 + 2*delta).The function is plain differentiable PyTorch arithmetic (no in-place ops), so gradients with respect to
vp/vnmo/etamay be obtained by chaining autograd through it – e.g. inverting directly in Alkhalifah-Tsvankin parameter space, then continuing the chain throughdeepwave.anisotropy.thomsen_to_cij()to build a VTI stiffness model (see that function, anddeepwave.elastic_vti()/deepwave.acoustic_vti()for the propagators that consume the resulting Cij).- Parameters:
vp – P-wave velocity along the symmetry axis (the vertical P-wave velocity).
vnmo – The short-spread P-wave normal-moveout (NMO) velocity, the quantity conventional velocity analysis of surface seismic data actually estimates.
eta – Alkhalifah and Tsvankin’s anellipticity parameter, which controls P-wave time processing beyond the NMO velocity (dip moveout, time migration); zero for elliptical anisotropy (
epsilon == delta).
- Returns:
(epsilon, delta), Thomsen’s parameters, suitable as input todeepwave.anisotropy.thomsen_to_cij()(with a chosenvs, since shear information is not recoverable from P-wave-onlyvnmo/eta).- Return type:
Tuple[torch.Tensor, torch.Tensor]
- deepwave.anisotropy.thomsen_to_alkhalifah(vp: Tensor, epsilon: Tensor, delta: Tensor) tuple[Tensor, Tensor]¶
Converts Thomsen (1986) epsilon/delta to Alkhalifah-Tsvankin (1995) parameters.
The exact inverse of
alkhalifah_to_thomsen(), using Alkhalifah and Tsvankin’s defining pair directly:vnmo = vp * sqrt(1 + 2 * delta) eta = (epsilon - delta) / (1 + 2 * delta)
so
alkhalifah_to_thomsen(vp, *thomsen_to_alkhalifah(vp, epsilon, delta))returns(epsilon, delta)andthomsen_to_alkhalifah(vp, *alkhalifah_to_thomsen(vp, vnmo, eta))returns(vnmo, eta), both to floating-point precision. It is the conversion to use when a model is held in Thomsen parameters but the P-wave-derivable quantities are wanted – to compare with, or initialise from, a conventional NMO-velocity/anellipticity model, or to invert in the parameterisation that surface P-wave data actually constrains (seealkhalifah_to_thomsen()for why that is the NMO velocity andeta, notepsilonanddeltaindividually).The function is plain differentiable PyTorch arithmetic (no in-place ops), so gradients with respect to
vp/epsilon/deltamay be obtained by chaining autograd through it.- Parameters:
vp – P-wave velocity along the symmetry axis (the vertical P-wave velocity).
epsilon – Thomsen’s epsilon,
(c11 - c33) / (2 c33).delta – Thomsen’s delta (exact definition, see
deepwave.anisotropy.thomsen_to_cij()). Must be greater than-0.5everywhere, since1 + 2 deltais the squared ratio of the NMO to the vertical velocity.
- Returns:
(vnmo, eta), the short-spread P-wave normal-moveout velocity and the anellipticity parameter, suitable as input toalkhalifah_to_thomsen().- Return type:
Tuple[torch.Tensor, torch.Tensor]
- Raises:
DeepwaveValueError – If
delta <= -0.5anywhere, which would make1 + 2 deltanon-positive andvnmoimaginary.
References
T. Alkhalifah, I. Tsvankin, “Velocity analysis for transversely isotropic media,” Geophysics, vol. 60, no. 5, 1995.
- deepwave.anisotropy.source_taper(c11: Tensor, c13: Tensor, c33: Tensor, source_locations: Tensor, radius: float) Tensor¶
Locally elliptical
c13around sources to suppress pseudo-shear.This returns a modified model, not a mask or a window. The
c13it gives back has been blended towardssqrt(c11 c33)withinradiuscells of every source, so a propagator run on it is running on a medium that differs from the one you passed in, near the sources: the anisotropy there has been made elliptical. That is the point – it is what removes the pseudo-shear artefact – but it means the simulated wavefield is not the wavefield of your own model in that neighbourhood, and a gradient computed with respect to the taperedc13is not the gradient with respect to yours there either. Keepradiussmall (a few cells), and if the near-source medium matters to you, do not use this.A point source in a region where
eps != delta(i.e.c13 != sqrt(c11 c33)) emits a diamond/rhombic pseudo-shear wave, theV_S = 0kinematic remnant. Following Duveneck et al. (2008), this helper blendsc13towards the locally elliptical valuesqrt(c11 c33)(delta -> eps, for which no pseudo-shear artifact is emitted) inside aradius(in grid cells) of each source location, with a raised-cosine taper:w(r) = 0.5 (1 + cos(pi r / radius)) for r < radius, else 0 c13_tapered = w sqrt(c11 c33) + (1 - w) c13
so
c13equalssqrt(c11 c33)exactly at a source (r = 0,w = 1) and is returned unchanged at and beyondradius(w = 0), varying smoothly in between. The blend perturbs the model near the source; keepradiussmall (a few cells).The taper is the union over every location in
source_locations(the nearest source governs each cell), and is applied on the model’s trailingndimspatial dimensions (broadcasting over any leading shot dimension of the models). Distances are measured in grid cells (Euclidean).- Parameters:
c11 – The c11 stiffness model.
c13 – The c13 stiffness model (the one that is tapered).
c33 – The c33 stiffness model.
source_locations – Integer source locations, shape
[..., ndim](e.g.[n_shots, n_sources, ndim]); the leading dimensions are flattened and every listed location contributes. This helper does not interpretdeepwave.IGNORE_LOCATION: pass only the locations you want tapered.radius – The taper radius in grid cells (
> 0).
- Returns:
A new
c13tensor (same shape/dtype/device as the inputc13) with the near-source blend applied. Differentiable with respect toc11,c13, andc33.
PropagatorOutput, AuxiliaryState and RunInfo¶
- class deepwave.PropagatorOutput(names: Sequence[str], values: Sequence[Any] | None = None, *, info: RunInfo | None = None)¶
Named, attribute-accessible view of a propagator’s outputs.
Returned by every propagator. Each final wavefield and receiver-amplitude tensor is accessible by name (e.g.
out.receiver_amplitudes), which removes the most common indexing mistake: the position ofreceiver_amplitudesin the positional return shifts with the number of spatial dimensions, so code written for 2-D silently grabs the wrong tensor in 3-D.The object is a
tuple(it subclassestuplerather than merely implementingcollections.abc.Sequence): iterating, unpacking, and integer or slice indexing yield the values in the current positional return order. Positional unpacking therefore remains available, though it should be reviewed against the compatibility policy when upgrading:out = scalar(...) out.receiver_amplitudes # by name (ndim-independent) *state, receivers = out # by position, as before
For a fixed propagator and dimensionality, the current field names and tuple order follow the stable compatibility policy (see Stability policy). Future metadata is added as attributes, not tuple elements. Named access is recommended: a positive index can silently mean a different field on another propagator or in another number of dimensions. For example,
out[-2]is the opaque auxiliary state onscalar, but a receiver field onscalar_born.Exactly one element is not a
torch.Tensor:auxiliary_state, aAuxiliaryStateholding the absorbing boundary’s memory variables and any other auxiliary state, which sits at the index the first of those fields used to occupy. It is a real tuple element (solen(out), iteration and unpacking see it) and also a typed property, and it is whatinitial_state=consumes to continue a simulation. Code that iterates the whole tuple expecting tensors –sum(o.sum() for o in out)and the like – must skip it or iterate its tensors.Subclassing
tuple(notabc.Sequence) is deliberate and load-bearing:torch.autograd.Functionandtorch.utils.checkpointpropagaterequires_gradto an output only when it is aTensoror a genuinetuple. Anabc.Sequencethat is not atupleis silently detached (the root cause of issue #120), so returning aPropagatorOutputstraight throughtorch.utils.checkpointwould break gradients if it were not a realtuple. Being a realtuplemakes the named return compose with checkpointing exactly like the plain tuple return would; the non-Tensorauxiliary_stateelement does not change that for the tensor elements, and outside a checkpoint the tensors it holds keep theirgrad_fn.torch.utils.checkpoint(..., use_reentrant=True)is the exception, and the reason to preferuse_reentrant=False(which PyTorch also recommends) when a checkpointed call’s state is fed onward. The reentrant implementation runs the wrapped function undertorch.no_grad()and re-attaches only theTensorelements of what it returns, so:the return is degraded to a plain
tuple, losing the field names andinfo– pre-existing, and independent ofauxiliary_state; andevery tensor held inside
auxiliary_statecomes back detached, so continuing a simulation from it gives no gradient through the absorbing boundary.use_reentrant=Falsepreserves both the subclass and those gradients. A function wrapped in a reentrant checkpoint that needs the auxiliary state downstream must return its tensors as top-level elements of its own return value, which is whatdeepwave.checkpointing.run_segments()does internally.
The public state fields use the same names as the propagator’s initial-state arguments (
wavefield_0,wavefield_m1, …), so an output can be fed straight back in to continue a simulation. Fields that do not exist for the given number of dimensions (e.g. thezcomponents in a 2-D simulation) are simply absent.final_stateis the mapping of exactly the fieldsinitial_state=consumes.infois aRunInfocarrying non-differentiable metadata about how the call actually ran (resolvedstep_ratio, internaldt, Courant number,nt, PML width, PML frequency, survey origin/extent and more). It is plain attribute metadata, not a tuple element: it does not appear in iteration, unpacking,len(), or indexing. A propagator always supplies one. It is unreachable in exactly two situations: undertorch.utils.checkpoint(..., use_reentrant=True), where the return is degraded to a plaintuplethat has no attributes at all, and undertorch.nn.DataParallel.gather, which does not reach the point of building an output.Registered as a pytree node, so
torch.funcandtorch.compilesee the tensors inside it rather than one opaque object: a function that returns a whole output can be wrapped intorch.func.vmap(), carried as the auxiliary value oftorch.func.grad()ortorch.func.jacrev()(has_aux=True), compiled, or walked withtorch.utils._pytree’stree_map. The children are the tuple elements in return order (withauxiliary_state, itself a pytree node, expanding into the tensors it holds, so the leaves are exactly the output’s tensors); the field names andinfotravel as the node’s context, because the names vary per instance and the object cannot be rebuilt without them. A rebuilt output therefore still carries the names and theinfoof the run it came from, and itsauxiliary_statestill carries the recorded run parameters –format_versionincluded – that make it checkable as aninitial_state=.Not usable as a full ``torch.nn.DataParallel`` return value on more than one GPU (
DistributedDataParallelis unaffected). Aforward()wrapping a propagator should return a single tensor (e.g.out.receiver_amplitudes) or a small tuple of plain tensors instead; see Distributed (multi-GPU) execution for the pattern and PropagatorOutput: DataParallel and pytrees for why this is unsupported by design.Builds the tuple of
valuesand records the fieldnames.- property auxiliary_state: AuxiliaryState¶
The opaque absorbing-boundary and auxiliary state of this run.
Raises
AttributeErroron a propagator that has no auxiliary-state field. Declared as a property, rather than left to__getattr__, because it is the one element of the tuple that is not atorch.Tensorand so needs a static type of its own.
- property bg_receivers: Mapping[str, Tensor]¶
The recorded background data of a Born propagator, keyed by channel.
As
receivers, for the background (unperturbed) wavefield’s receivers. RaisesAttributeErroron the eight propagators that have no background receivers, rather than returning an empty mapping: an empty mapping would read as “the background data was not requested”, which is a different thing from “this propagator has none”.
- detach() PropagatorOutput¶
A copy of this output with every field detached from the graph.
Truncated backpropagation through a long record – running many segments but backpropagating through only the most recent ones – means cutting the autograd graph at a segment join deliberately. Every state field has to be cut for the graph to actually be cut: detaching only the
auxiliary_stateleaves the physical wavefields attached, so the join still carries the whole history and the gradient is the untruncated one:out = scalar(v, dx, dt, source_amplitudes=chunk) loss(out.receiver_amplitudes).backward() state = out.detach() # the whole join, cut out = scalar(v, dx, dt, source_amplitudes=next_chunk, initial_state=state)
The
auxiliary_stateof the copy is detached throughAuxiliaryState.detach(), so it records that the detachment was deliberate and the next call does not warn about a lost gradient.infoand the field names are carried over unchanged.
- property final_state: Mapping[str, Tensor | AuxiliaryState]¶
The fields that
initial_state=consumes, as a read-only mapping.The public state fields plus
auxiliary_state, excluding the receiver amplitudes.prop(..., initial_state=out)andprop(..., initial_state=out.final_state)are equivalent; the mapping form is the one to build by hand when only part of the state is being replaced. Mutating it raises.
- property receivers: Mapping[str, Tensor]¶
The recorded receiver data, keyed by channel, as a read-only mapping.
The same vocabulary the
receivers=argument uses, so a wrapper can round-tripreceivers={channel: locations}intoout.receivers[channel]without knowing how this propagator spells its output fields."u"on the two scalar propagators;"p","y","x"(and"z"in 3-D) on the acoustic and elastic families;"sigma_v"/"sigma_h"as well onacoustic_vti.Every channel of the propagator is present, whether or not it was requested: a channel with no receiver locations holds the same empty tensor the returned tuple holds for it. An unrequested channel raising
KeyErrorwould be a worse interface than a documented empty tensor, because a caller could not tell it apart from a typo. That empty tensor is shaped like the data it would have held –[n_shots, 0, nt], the same shot count and the same number of time samples as a requested channel – sotorch.cat(list(out.receivers.values()), dim=1)is a valid multichannel gather however few channels were asked for.On a Born propagator this is the scattered data, matching the
sc_receiver_*field names; the background data isbg_receivers.Mutating the mapping raises.
- class deepwave.AuxiliaryState(_tensors: Mapping[str, Tensor], propagator: str | None = None, ndim: int | None = None, accuracy: int | None = None, pml_width: tuple[int, ...] | None = None, boundary: tuple[str, ...] | None = None, n_mechanisms: int | None = None, mpml: bool | None = None, mpml_fraction: float | None = None, grid_spacing: tuple[float, ...] | None = None, surface_method: str | None = None, q_freq: float | None = None, q_band: tuple[float, float] | None = None, fit_convention: str | None = None, field_names: tuple[str, ...] | None = None, field_shapes: tuple[tuple[int, ...], ...] | None = None, shape: tuple[int, ...] | None = None, dtype: dtype | None = None, device: device | None = None, origin: tuple[int, ...] | None = None, pml_freq: float | None = None, scheme: str | None = 'cfs-cpml', detached_deliberately: bool = False, max_vel: float | None = None, internal_dt: float | None = None, format_version: int = 2)¶
Opaque absorbing-boundary and auxiliary state of one propagation.
A propagator’s absorbing boundary (a CFS-CPML) carries memory variables from one time step to the next, and continuing a simulation exactly requires carrying them across the join. They are implementation state, not physics: this object holds them so that a continuation is a matter of passing one value back (
prop(..., initial_state=out)) rather than of naming fields whose number and shape are properties of the absorbing-boundary implementation.The bundle is opaque as an output element: do not index it positionally, and do not build on how many tensors it holds, on their order or on their shapes (see Stability policy). Its keys are named – they are the provisional internal diagnostic names listed in Internal callback names. The mapping surface exists for inspection and debugging; the supported continuation use is to hand an
AuxiliaryStateback to a propagator unchanged.It is a read-only mapping from name to
torch.Tensor(names(),bs[name],name in bs,len(bs), iteration,keys(),items(),values(),as_dict()), plusto()for moving it between devices and dtypes anddetach()for cutting the autograd graph at a join on purpose. It is not atorch.Tensorand never becomes one; the tensors it holds keep theirgrad_fn, so a continuation is differentiable end to end.==is identity comparison, deliberately: a generated__eq__would compare the tensors and return aTensor, makingbs1 == bs2raise inside anyiforin. Usecompatible_with()to ask whether one auxiliary state may be reused by another call.Registered as a pytree node, so
torch.funcandtorch.compilereach the tensors it holds (it is one element of every propagator’s output, so without that the output could not be flattened to tensors at all). The children are the tensors; the tensor names and every recorded run parameter,format_versionincluded, travel as the node’s context and come back unchanged, so a state that has been through a transform is still one a continuation checks itself against rather than an unrecorded bundle.- propagator¶
The name of the propagator that produced it (e.g.
"scalar"), orNonefor a state that was assembled by hand rather than returned by a run.- Type:
str | None
- boundary¶
The resolved per-face boundary conditions, length
2 * ndim, from{"pml", "free", "rigid", "periodic"}.
- n_mechanisms¶
The number of relaxation mechanisms (1 except for the viscoacoustic propagator).
- Type:
int | None
- shape¶
The shape of the tensors,
[n_shot, *spatial], where the spatial part is the extracted survey region plus the PML. Checked: a continuation whose resolved simulation domain is a different shape is not continuing the same simulation.
- dtype¶
The dtype of the tensors.
- Type:
torch.dtype | None
- device¶
The device the tensors are on.
- Type:
torch.device | None
- origin¶
The index, in the full model, of cell 0 of the region the run extracted, length
ndim. Used (not checked) to restore the same survey region on a continuation. The region’s size is checked, throughshape.
- max_vel¶
The maximum wavespeed, in m/s, that run used to build the PML profiles and choose its internal time step. A changed resolved value is incompatible with checked continuation.
- Type:
float | None
- internal_dt¶
The internal (sub-stepped) time step, in seconds, that run took. Checked: it is the condition every propagator’s
initial_statedocumentation names for a continuation that exactly reproduces a single combined run.- Type:
float | None
- scheme¶
Names the absorbing-boundary implementation the tensors belong to, so that a future change of scheme cannot silently reuse state from the old one.
- Type:
str | None
- detached_deliberately¶
Whether the tensors were detached from the autograd graph on purpose, by
detach(). A state that arrived detached by accident (seedetach()) is warned about; one marked this way is accepted silently. It is not part ofcompatible_with(): it says nothing about what the tensors mean.- Type:
- format_version¶
The version of the tensor bundle’s layout and numerical interpretation. A correction can require a new version, including in a patch release. Saved states require a compatible implementation; they are not a long-term interchange format. Handing back a state whose
format_versiondiffers from the one this release writes raisesdeepwave.DeepwaveValueError, naming both versions, rather than reading the tensors as something they are not.- Type:
A field recorded as
Nonewas not supplied by a run and is neither reported nor checked.- compatible_with(other: AuxiliaryState) bool¶
Whether a call described by
othermay reuse this auxiliary state.Compares
format_version– a bundle written to a different layout is not readable as this one whatever else matches – and then the recorded run parameters that determine what the tensors mean: propagator, ndim, accuracy, pml_width, boundary, n_mechanisms, mpml, scheme, shape and internal_dt, as well as the resolved grid, attenuation and absorbing-boundary profile.dtypeanddeviceare deliberately excluded (seeto()). A changedpml_freqormax_velis incompatible with checked continuation.shapeis included – althoughto()cannot change it – because it is what pins the size of the survey region the tensors describe.originpins where that region starts; withoutshapea continuation whose sources or receivers have moved outside the previous region can have the state re-read at a larger extent and silently zero-padded, displacing every field.
- detach() AuxiliaryState¶
A copy whose tensors are detached from the autograd graph.
Cutting the graph at a segment join deliberately – truncated backpropagation through a long simulation – is indistinguishable, from inside a propagator, from having lost the gradient by accident, so this records the intent: a state detached this way is accepted silently, while one that arrived detached is warned about.
This detaches the auxiliary state alone, which is not by itself enough to truncate anything: the physical wavefields of the same join are separate fields and stay attached, so the graph is not cut, no memory is freed, and the gradient is still the full one. Use
PropagatorOutput.detach(), which cuts every field of the join, unless you are assembling the continuation state field by field and detaching the rest yourself.
- to(device: str | device | None = None, dtype: dtype | None = None) AuxiliaryState¶
A copy with the tensors moved to
deviceand/or cast todtype.The recorded
device/dtypeare updated to match. Neither is part of the compatibility check (compatible_with()), so a round trip through another device – which is whatdeepwave.checkpointing.run_segments()’scheckpoint_devicedoes – is legitimate.
- values() ValuesView[Tensor]¶
The tensors, as a mapping view.
Every PropagatorOutput’s .info attribute is a deepwave.RunInfo carrying non-differentiable metadata about how the call actually ran: the user dt and grid_spacing, the accuracy, the per-face boundary, pml_width, pml_freq, mpml_fraction and surface_method, the attenuation model’s q_freq/q_n_mechanisms/q_band, the resolved internal step_ratio, the internal dt, the targeted Courant number, nt, max_vel, and the survey origin/extent. Together these are the numerical configuration of the run, so out.info is enough to set up an identical one. pml_freq is the frequency the run actually used – the argument you gave, or, if you gave none, the value inferred from your source amplitudes (see Choosing pml_freq); pass it back in as pml_freq= to pin a later run to the same absorbing boundary. It is plain attribute metadata, not a tuple element, so it never affects iteration, unpacking, len(), or indexing.
- class deepwave.RunInfo(step_ratio: int, internal_dt: float, courant_number: float, nt: int, pml_width: tuple[int, ...], pml_freq: float, origin: tuple[int, ...], extent: tuple[int, ...], dt: float = 0.0, grid_spacing: tuple[float, ...] = (), accuracy: int = 0, boundary: tuple[str, ...] = (), mpml_fraction: float = 0.0, surface_method: str = '', q_freq: float | None = None, q_n_mechanisms: int = 1, q_band: tuple[float, float] | None = None, max_vel: float | None = None, ndim: int = 0, dtype: dtype | None = None, device: device | None = None, model_batched: tuple[bool, ...] = (), wavefield_names: tuple[str, ...] = (), receiver_names: tuple[str, ...] = (), location_interpolation_used: str = '', backend: str = '', backward_backend: str = '', storage: Storage | None = None, lossy_compression_used: bool = False, model_gradient_sampling_interval: int = 1, model_gradient_stride: int | None = None)¶
Non-differentiable metadata about how a propagator call ran.
Every propagator attaches one to its return, as
out.info. It records what the call resolved – the values the engine actually used, which are not always the ones the caller asked for (dtmay be sub-stepped to satisfy the CFL condition,pml_freqmay be inferred from the source spectrum, the survey region may be cut down around the sources and receivers) – so that a later call can be pinned to the same choices.Existing fields follow the stable compatibility policy (see Stability policy). New resolved metadata may be added as attributes; an existing field is changed only through that policy.
- dt¶
The user-level time step, in the caller’s own time unit, exactly as passed.
internal_dtis this divided bystep_ratio.- Type:
- grid_spacing¶
The cell size along each dimension, in the caller’s own length unit, one entry per dimension (a scalar argument is expanded). Resolved.
- boundary¶
The per-face boundary condition, length
2 * ndim, in the face order(begin dim 0, end dim 0, begin dim 1, ...), spelled as theboundary=argument spells them ("pml","free","rigid","periodic"). Resolved, including the faces a zeropml_widthturned into something else.
- mpml_fraction¶
The multiaxial-PML cross-damping fraction used (see
deepwave.elastic()). 0.0 on a standard C-PML run.- Type:
- surface_method¶
How the free surface implied by elastic vacuum cells was discretised (see
deepwave.elastic()).- Type:
- q_freq¶
The reference frequency of the attenuation model, or
Noneon a propagator without one.- Type:
float | None
- q_n_mechanisms¶
The number of relaxation mechanisms in the attenuation model (1 where there is none).
- Type:
- step_ratio¶
The number of internal time steps taken per user-level time sample. 1 unless the user
dtexceeded the CFL limit. Resolved.- Type:
- model_gradient_sampling_interval¶
The positive integer requested by the caller, including when no model gradient is active.
- Type:
- model_gradient_stride¶
The effective native history/correlation stride in internal steps (
step_ratio * model_gradient_sampling_interval), 1 for active eager model differentiation, orNonewhen no model gradient is active. It describes sampling, not the accuracy of the complete parameter derivative.- Type:
int | None
- internal_dt¶
The internal time step, in the time units of
dt: the userdtdivided bystep_ratio. Resolved.- Type:
- courant_number¶
The Courant number the internal time step was chosen to satisfy. The user’s
courant_numberargument when one was given, otherwise the propagator’s conservative default. Resolved.- Type:
- nt¶
The number of user-level time samples propagated. The caller’s
ntwhen one was given (the source amplitudes are padded or truncated to it), otherwise the length of the source amplitudes. Resolved.- Type:
- pml_width¶
The per-face PML widths, in cells, length
2 * ndim, in the face order(begin dim 0, end dim 0, begin dim 1, ...). Resolved (a scalar argument is expanded to every face).
- pml_freq¶
The PML frequency, in the reciprocal of
dt’s time unit (Hz whendtis in seconds): thepml_freqargument when one was given, otherwise the value inferred from the source amplitudes’ spectrum (or0.5 / dt / 5when there were no sources). Pass it back in aspml_freq=to pin a later run to the same absorbing boundary. Resolved.- Type:
- origin¶
The index, in cells, in the full model, of cell 0 of the region the run extracted, one entry per dimension. Resolved (from the caller’s
origin, fromsurvey_pad, or from the source and receiver locations).
- extent¶
The spatial shape of the region extracted from the model, in cells, before FD and PML padding, one entry per dimension. Resolved.
- max_vel¶
The conservative speed bound, including density/stiffness weighting of staggered operators, in
grid_spacingunits perdtunit, used to choose the internal time step and to build the PML profiles: themax_velargument when one was given, otherwise the maximum over the model. Resolved.Noneif the propagator did not record it.- Type:
float | None
- ndim¶
The number of spatial dimensions. Resolved (from the model and location shapes when it was not given explicitly). 0 if the propagator did not record it.
- Type:
- dtype¶
The floating-point dtype the simulation ran in. Resolved from the models.
Noneif the propagator did not record it.- Type:
torch.dtype | None
- device¶
The device the simulation ran on. Resolved from the models.
Noneif the propagator did not record it.- Type:
torch.device | None
- model_batched¶
One flag per model, in the propagator’s model order, saying whether that model was given per shot (an extra leading batch dimension) rather than shared by every shot. Resolved. Empty if the propagator did not record it.
- wavefield_names¶
The public state field names of this call, in return order – exactly the keys of
PropagatorOutput.final_state, and therefore the namesinitial_stateaccepts. Depends onndimand, for the attenuating propagators, on the number of relaxation mechanisms. Empty if the propagator did not record it.
- receiver_names¶
The receiver-amplitude field names of this call, in return order. Empty if the propagator did not record it.
- location_interpolation_used¶
The location interpolation actually used for this call’s source and receiver coordinates:
"none"for the exact (on-cell) path,"hicks"for Kaiser-windowed sinc interpolation, or"mixed"when one kind of location was split across the two paths – some of a propagator’s source channels interpolated and others not, say. Sources are compared with sources and receivers with receivers, never with each other, so an off-node source with on-node receivers is"hicks", not"mixed"; only a propagator with several channels of one kind can report"mixed". A channel with no locations (None, an empty Tensor, or nothing butdeepwave.IGNORE_LOCATION) has taken neither path and is not counted."auto"resolves to one of those. The empty string if the propagator did not record it.This is how to tell whether
location_interpolation="auto"routed your coordinates through Hicks – in particular for on-node positions withrequires_grad=True, which are always interpolated (only that path carries gradients back to the positions), and so give different forward values from the same positions withoutrequires_grad.- Type:
- backend¶
The backend that ran the forward pass, as one of
"compiled","eager"or"torch_compile". Resolved, so it is never"auto": that is a request, and this is the answer to it. This is how to find out whatbackend="auto"chose – including inside a distributed worker, where a warning would not have been seen. The empty string if the propagator did not record it.- Type:
- backward_backend¶
The backend that would run the backward pass, in the same vocabulary. It is reported separately because a propagator is permitted to pair a compiled forward with an adjoint that runs on a different backend, but no configuration reachable today does: on every propagator and every backend it is currently equal to
backend. (The one near-miss isdeepwave.elastic_born()under M-PML, whose adjoint would use an eager-recompute bridge – but its compiled forward raises formpml_fraction > 0before anyRunInfois built.) Compare the two rather than assuming they agree; do not rely on their being equal. The empty string if the propagator did not record it.- Type:
- storage¶
The fully resolved storage policy, as a
deepwave.Storage.pathis the directory that was actually chosen (mode="disk"only), so a run that fell back to the system temporary directory can be read back off here rather than guessed at.modeis the resolved mode, which records the"cpu"-to-"device"downgrade the engine performs when the compute device is already the CPU – otherwise invisible.Noneif the propagator did not record it.- Type:
deepwave.Storage | None
Attenuation response¶
- deepwave.attenuation.response(q: Tensor, frequencies: Tensor, *, q_freq: float | None = None, q_n_mechanisms: int = 1, q_band: tuple[float, float] | None = None) Tensor¶
Return the dimensionless complex modulus
M(omega)/K.The output has shape
(*q.shape, frequencies.numel())and complex64 or complex128 dtype matchingq. Its real and imaginary parts are storage and positive loss. At DC the result is one. For multiple mechanisms,qis the weak-loss band target;real/imagis the actual effective Q and generally differs from that target, especially at strong loss. Gradients with respect toqare preserved. Positive infiniteqgives the exact lossless response and the limiting zero Q derivative.
deepwave.checkpointing¶
- deepwave.checkpointing.run_segments(propagator: Callable[[...], Any], model_args: Sequence[Tensor], grid_spacing: float | int | Sequence[float], dt: float, source_amplitudes: Mapping[str, Tensor], n_segments: int | None = None, *, seg_lengths: Sequence[int] | None = None, initial_state: PropagatorOutput | Mapping[str, Tensor | AuxiliaryState] | None = None, segment_callback: Callable[[int, PropagatorOutput], None] | None = None, storage: str | Storage | None = None, storage_mode: Literal['device', 'cpu', 'disk', 'none'] | None = None, storage_path: str | PathLike[str] | None = None, storage_compression: bool | None = None, combine: bool = True, optional_model_args: Mapping[str, Tensor] | None = None, checkpoint: bool = False, checkpoint_device: str | device | None = None, use_reentrant: bool = True, _slab_pml: bool = True, **kwargs: Any) PropagatorOutput¶
Runs a propagator segment-by-segment, threading state via
initial_state.Stability: provisional (see Stability policy).
This is the general driver underlying
snapshots()anddeepwave.wavefields.measure(); it is also reusable on its own (e.g. to emit a wavefield snapshot at each segment boundary viasegment_callback, or to bound backward-pass memory withcheckpoint=True). Apart from the optional per-segmenttorch.utils.checkpointwrapping it performs no gradient bookkeeping of its own – whether the result is differentiable is governed entirely by whether the inputs require grad and bystorage_mode(as for a normal propagator call).This takes
model_argsas a positional sequence andsource_amplitudesas a name->tensor mapping. That generic-wrapper convention is what lets one implementation drive any current or future Deepwave propagator, but it is provisional and it is not the ergonomic way to segment a run: most users should reach forcheckpoint_segments=on the propagator itself, which builds this call for them.Internal sub-stepping (``step_ratio > 1``) is handled automatically. When the user
dtdoes not satisfy the CFL condition, Deepwave runs internally at a smaller time step, resampling the source amplitudes up and the receiver amplitudes down with a global FFT. Because that resampling does not decompose across a segment boundary, a run of two or more segments atstep_ratio > 1instead resamples the whole trace once: every source tensor is upsampled a single time, every segment is driven at the internal time step (so each segment’s ownstep_ratiois 1),model_gradient_sampling_intervalis scaled so the model gradient is sampled at the same steps a direct call samples it, and the time-concatenated receiver output is downsampled once at the end. Forward values and gradients then match an unsegmented direct call within rounding, given the same numerical setup. An omittedmax_velis resolved once through the propagator’s own preparation, including its heterogeneous and attenuation CFL bounds, before the record is split. This is behaviour, not an option: a caller wanting raw segments at the internal rate simply passes the internaldtitself, makingstep_ratio1.Combining with ``model_gradient_sampling_interval > 1``: the model-gradient match above holds only if every segment boundary falls on a multiple of the sampling interval, because each segment’s sampling phase restarts at 0 and a boundary that splits a sampling period would sample the gradient at the wrong steps. Rather than leave that to the caller, a misaligned split raises
DeepwaveValueErrorup front, in the forward pass, naming the offending segment and a segment count that would work. The check is skipped when no model gradient will be computed, since there is then nothing for the interval to sample. This is unrelated tostep_ratioand applies whether or not internal sub-stepping is happening.- Parameters:
propagator – A Deepwave propagator function – any propagator that
deepwave.spec()accepts, currently every propagator deepwave provides (e.g.deepwave.scalar,acoustic,elastic,elastic_vti,acoustic_vti,scalar_born,viscoacoustic,acoustic_born,elastic_born). Every propagator (including the Born family) takes its required models positional (beforegrid_spacing/dt) and its optional scattering models (dv/drho/dlamb/dmu/dbuoyancy/c66) keyword-only (afterdt); sincerun_segmentscalls the propagator directly (not through atorch.autograd.Functionboundary), those optional models may simply be passed in**kwargslike any other keyword argument and remain fully differentiable – unlesscheckpoint=True, which does cross such a boundary, and so needs them inoptional_model_argsinstead (see below).model_args – The model tensors, in the order the propagator takes them positionally (e.g.
(v,)for scalar;(lamb, mu, buoyancy)for elastic).grid_spacing – The grid spacing (as passed to the propagator).
dt – The user time step.
source_amplitudes – Mapping from source-amplitude argument name (e.g.
"source_amplitudes", or"source_amplitudes_y") to its tensor of shape[shot, source, time]. All must share the same time length, which definesnt.n_segments – The number of near-equal contiguous time segments to split into. Exactly one of
n_segmentsorseg_lengthsmust be given.seg_lengths – Explicit per-segment step counts (must sum to
nt), for splits that are not near-equal (e.g. snapshot boundaries). Exactly one ofn_segmentsorseg_lengthsmust be given.initial_state – An optional starting state (a
PropagatorOutputor a name->tensor mapping), passed to the first segment.segment_callback –
An optional
segment_callback(cumulative_step, output)invoked after each segment completes, withcumulative_stepthe number of user time steps run so far (running total of the segment lengths, ending atnt) andoutputthat segment’sPropagatorOutput. Segment boundaries always fall on user steps, so this count is unaffected by automatic resampling; the per-segmentoutput’s receiver fields, however, are at the internal sample rate while resampling is active (they are downsampled only once, after the last segment) – detect that case by comparing the per-segmentoutput.info.internal_dtwith thedtyou passed.This is distinct from a per-step
forward_callback/backward_callbackforwarded inkwargs, whichkwargspasses through to each segment’s own propagator call. Two things about those are easy to get wrong.callback_frequencycounts the steps of each segment’s call. When whole-trace resampling is active, these are internal steps, with intervaloutput.info.internal_dt; otherwise they are user steps. Multiply the desired user-step frequency by the whole-runstep_ratiowhen resampling is active. And each segment is a fresh propagator call, soCallbackState.steprestarts at 0 in every segment: a callback that indexes a whole-run array bystate.stepwill overwrite the first segment’s entries with every later segment’s. During the original forward pass, a whole-run time is the preceding segment’scumulative_step * dtplusstate.step * state.dt. Do not use a mutable segment offset for backward callbacks or checkpoint recomputation: they execute after that offset has advanced. Forward callbacks can run again during checkpoint recomputation, so their side effects must be safe to repeat. Usesegment_callbackfor observations made once per segment in the original forward pass.storage – The storage policy passed to each segment, as one object: either a mode string or a
deepwave.Storage. An alternative spelling for the three individualstorage_*arguments below, which are permanent and not deprecated; passing both spellings raisesDeepwaveValueError. Resolved once, here, so that every segment runs the same policy.storage_mode – The
storage_modepassed to each segment. Defaults to"device", matching the propagators’ own default (so a segmented run is differentiable by default, the same as a direct call).storage_path – The
storage_pathpassed to each segment (used only bystorage_mode="disk"). Defaults to the system temporary directory (tempfile.gettempdir()).storage_compression – The
storage_compressionpassed to each segment. Lossy – changes the model gradient by about 0.2-4% in 2-D and 0.2-8% in 3-D. Seedeepwave.elastic(). Defaults to False.combine – Which sample rate the returned receiver amplitudes are at, and how much is retained to produce them. If True (default) return a combined
PropagatorOutputfor the whole run – final wavefield state plus time-concatenated receiver amplitudes, downsampled once as a whole to the user sample rate if the run was internally sub-stepped. If False return the last segment’s output directly, at the internal sample rate, and do not retain the per-segment receiver traces, which keeps a purely streaming driver (used bydeepwave.wavefields.measure(), whosesegment_callbackreduces each segment on the fly). Because the downsampling FFT is global in time and cannot be applied per segment,combine=Falsetogether with active resampling (two or more segments atstep_ratio > 1) raisesDeepwaveValueErrorrather than quietly returning internal-rate data.optional_model_args – The keyword-only optional model tensors (
dv,drho,dlamb,dmu,dbuoyancy,c66) as a name->tensor mapping. Withoutcheckpointthese are simply merged into**kwargs(passing them inkwargsdirectly remains supported and equivalent). Withcheckpoint=Truethey must be given here rather than inkwargs, because they are then threaded positionally through each checkpoint boundary so they receive gradients (closure-captured tensors get none under reentrant checkpointing). A name given both here and inkwargsis aValueError.checkpoint – If True, wrap each segment except the last in
torch.utils.checkpoint.checkpoint, so that segment’s forward activations (the stored wavefield history) are freed after it runs and recomputed on demand during the backward pass. This is the standard way to bound backward-pass memory: peak backward memory becomes roughly one segment’s history plus the auxiliary states, instead of the whole run’s history, at the cost of one extra forward pass per checkpointed segment. The last segment is deliberately not checkpointed (recomputing it would cost time and save no peak memory, since its history is needed immediately). A pass with nothing to differentiate – no model, source, optional-model, initial-state or keyword tensor requires grad, or grad mode is off – skips the wrapper entirely and calls each segment directly, since recomputation buys nothing without a backward pass. That is bit-exact (the wrapper runs the same code, only undertorch.no_grad()), marginally cheaper, and free of the per-segment “None of the inputs have requires_grad=True” warning torch would otherwise emit, socheckpoint=Trueis safe to leave set on a forward-only run. The PML memory-variable fields of the state threaded between checkpointed segments are stored as their two per-axis boundary slabs (thicknesspml_width + accuracyper side) rather than full-size, and rebuilt inside the segment: those fields are exactly zero outside the slabs, so this is bitwise-exact in both the forward values and every gradient, and it shrinks then_segments x auxiliary-stateterm that dominates checkpointed peak memory by roughly 2-3x, growing with grid size. It applies to the threaded state only –segment_callbackand every returned output still see full-size tensors – and falls back silently and exactly to full-size states for propagators that do not declarePropagatorSpec.pml_slab_axes, for backends other than the compiled one, formpml_fraction > 0, for a non-defaultboundary, and for grids too small for a slab to be smaller than the field. Take the gradient of a checkpointed run withloss.backward(): the checkpointing is reentrant by default, andtorch.utils.checkpointrefusestorch.autograd.grad()(and aninputs=argument to.backward()) in that mode, raising rather than returning a wrong number. Setuse_reentrant=Falseiftorch.autograd.gradis needed. Defaults to False.checkpoint_device – If given (with
checkpoint=True), the state tensors threaded between segments are moved to this device after each checkpointed segment and moved back to the compute device inside the next segment, freeing compute-device memory between segments (e.g."cpu"). Device transfers are differentiable, so this does not change gradients. It is not applied after the final segment, so the returned state is on the compute device exactly as without it. Giving it withoutcheckpointis aValueError.use_reentrant – The
use_reentrantvalue passed to everytorch.utils.checkpoint.checkpointcall. Defaults to True, which saves native wavefield history between segments. Set False to usetorch.autograd.gradorbackward(inputs=...)with checkpointing; this can retain a history buffer per segment and substantially increase memory use._slab_pml – Set False to thread the PML memory-variable fields of the checkpointed state full-size instead of as their two per-axis boundary slabs. Underscored and outside the stability policy, for Deepwave’s own tests.
**kwargs – Remaining keyword arguments forwarded unchanged to every segment (
source_locations,receiver_locations,pml_freq,max_vel,accuracy,origin, …). The survey may be given in any call form the propagator itself accepts: asources=/receivers=/bg_receivers=mapping is resolved into the flat keywords before the first segment runs, and a flatsource_amplitudes*keyword is lifted into thesource_amplitudesmapping this function slices per segment. Neither is forwarded whole, which would give every segment the entire record.nt, if supplied, pads or truncates the whole source record before splitting it, as on a direct propagator call. Giving one channel’s amplitudes both ways raises.storage_mode,storage_path, andstorage_compressionmust not appear here (use the parameters above). Undercheckpoint=Truea tensor here that requires grad is rejected, sincekwargsare captured by the checkpointed closure and would silently receive no gradient (only top-level tensor values are checked). Ifpml_freqis absent, it is resolved once, from the whole source record (not per segment, which would give the segments different PML profiles while one set of boundary memory variables is threaded through them all) and then passed to every segment explicitly.
- Returns:
A combined
PropagatorOutput(combine=True) or the last segment’s output (combine=False).
- deepwave.checkpointing.snapshots(propagator: Callable[[...], Any], model_args: Sequence[Tensor], grid_spacing: float | int | Sequence[float], dt: float, source_amplitudes: Mapping[str, Tensor], snapshot_steps: Sequence[int], *, initial_state: PropagatorOutput | Mapping[str, Tensor | AuxiliaryState] | None = None, max_vel: float | None = None, courant_number: float | None = None, **kwargs: Any) SnapshotResult¶
Differentiable intermediate wavefield snapshots of a propagation.
Stability: provisional (see Stability policy).
Runs
propagatoronce, from step 0 tont, but breaks the run into contiguous segments at the requestedsnapshot_steps(outer-step indices) and returns the full wavefield state at each of those steps inside the autograd graph. A loss built on any returned snapshot is differentiable w.r.t. the model and source tensors, exactly as a loss on the final output is (the segments are chained throughinitial_state, so autograd threads the adjoint state across every boundary).This is the read-write counterpart to the read-only
forward_callbackwavefield surface: instead of observing a detached wavefield mid-run, the returned snapshots are live graph nodes you can put a loss on (e.g. to match a measured wavefield at an intermediate time, to regularise the field, or to build a wavefield-space objective for WRI-style inversion).Implemented as a thin, propagator-agnostic layer over
run_segments()(each segment is a normal, differentiable propagator call withstorage_mode="device"). It therefore inherits that function’s exactness behaviour, and the samemodel_args(positional)/source_amplitudes(name->tensor mapping) generic-wrapper convention: what lets one implementation drive any propagator, and provisional along with the rest of this module. Users who only want to bound backward-pass memory, rather than to differentiate a mid-run wavefield, should reach forcheckpoint_segments=on the propagator itself instead.Snapshots are supported at any
step_ratio. When the userdtrequires internal sub-stepping (step_ratio > 1), the driver resolvesmax_velfrom the whole call if omitted and resamples the whole propagation once, so a snapshot still equals the wavefield a single unsegmented run would have reached; seerun_segments().The snapshot at outer step
sis the wavefield state aftersouter (user) steps have been applied – i.e. afters * step_ratiointernal steps, the same wavefield aforward_callbackobserves when it is invoked atstep == s(the forward callback fires before the chunk starting atstep). Snapshots are returned in the propagator’s “pml” view (model region plus PML, finite-difference padding removed), matchingCallbackState.get_wavefield(name, view="pml").- Parameters:
propagator – A Deepwave propagator function (
deepwave.scalaretc.).model_args – The model tensors in positional order (
(v,)for scalar;(lamb, mu, buoyancy)for elastic). Those withrequires_gradreceive a gradient from any loss on the snapshots or final output.grid_spacing – Grid spacing, as passed to the propagator.
dt – The user time step.
source_amplitudes – Mapping from source-amplitude argument name to tensor (
{"source_amplitudes": src}for scalar), definingnt.snapshot_steps – The outer-step indices at which to return a snapshot. Each must satisfy
1 <= step <= nt. Duplicates are ignored and the order does not matter (the returned mapping is keyed by step).initial_state – Optional starting state (PropagatorOutput or mapping), threaded into the first segment.
max_vel – Maximum wave speed, used to resolve the internal time step for the whole-trace resampling and passed to the propagator. If omitted, the propagator’s preparation resolves its conservative bound.
courant_number – Optional Courant number, affecting the resolved
step_ratioand passed to the propagator.**kwargs – Remaining propagator keyword arguments forwarded to every segment (
source_locations,receiver_locations,pml_freq,accuracy,origin, …).storage_modeandrun_segments()’s checkpointing parameters must not appear.
- Returns:
A
SnapshotResult, a named(output, snapshots)tuple.outputis the whole propagation’sPropagatorOutput(receiver amplitudes over allntsteps and the final wavefield state), as for a direct propagator call.snapshotsis adictmapping each requested step to a{wavefield_name: tensor}dict of that step’s wavefield state (differentiable).
- class deepwave.checkpointing.SnapshotResult(output: PropagatorOutput, snapshots: dict[int, dict[str, Tensor]])¶
The result of
deepwave.checkpointing.snapshots().A named two-tuple, so
combined, snaps = snapshots(...)andresult.snapshotsboth read naturally.Stability: provisional (see Stability policy).
- output¶
The whole propagation’s
PropagatorOutput, as a direct call would return it.
- snapshots¶
A mapping from each requested outer step to that step’s
{wavefield_name: tensor}wavefield state.- Type:
dict[int, dict[str, torch.Tensor]]
Create new instance of SnapshotResult(output, snapshots)
CallbackState¶
Every accessor below takes an internal name – the kernel’s own spelling of a model or wavefield, not the argument you passed the propagator. Internal callback names lists them all, per propagator and per dimensionality, and distinguishes stable physical fields from provisional internal diagnostics.
- class deepwave.CallbackState(dt: float, step: int, wavefields: dict[str, Tensor], models: dict[str, Tensor], gradients: dict[str, Tensor], fd_pad: list[int], pml_width: list[int], forward_wavefields: dict[str, Tensor] | None = None, *, direction: Literal['forward', 'backward'], n_steps: int, internal_dt: float | None = None)¶
State provided to user callbacks.
What you may write to¶
Three separate promises, deliberately not one:
get_gradient()returns a live view into the gradient accumulator, and mutating it in place is supported. This accumulator is intermediate, even at the last callback. To scale the complete model gradient, use a PyTorch tensor gradient hook or transformmodel.gradafter.backward(); see Custom imaging condition. The internal accumulator is available only on the compiled backend, which is the only backend that populates the gradient dictionary at all: the eager backward path and every forward callback are given an empty one, so there is nothing to mutate there. Askwritable()rather than assuming.get_wavefield()andget_model()return, in the defaultmode="write", the tensor Deepwave itself holds, and writing into it in place changes the running simulation in inference calls. During differentiable runs a forward callback may observe these tensors but may not mutate them: an arbitrary edit has no defined adjoint and raisesdeepwave.DeepwaveRuntimeError.set_wavefield()is the checked route into the wavefields – it validates shape, dtype and device instead of letting a mismatch become a silent broadcast or a host-to-device copy in the middle of the time loop – and is what to prefer where it applies. There is noset_model; a model is written in place. A name that is derived from the internal state rather than being part of it is read-only, andwritable()is how to ask which a name is.Nothing you keep past the end of the callback is stable.
"read"mode gives a detached view, not a snapshot: the buffer it points at is overwritten at the next time step. If you keep a tensor past the end of the callback, ``.clone()`` it – Visualising wavefields and gradients with callbacks is the modelled idiom.
When PyTorch invokes a backward node with an undefined adjoint, the callback receives a zero tensor for that field rather than
None. PyTorch can also prune an unused backward node entirely, in which case its callback does not fire. A callback is not a finalisation hook.- step¶
User time-step index. Forward callbacks observe
0, f, 2*f, ... < n_stepsforcallback_frequency=f; backward callbacks traverse that same set in reverse at the matching state boundary. There is no extra callback atn_steps; the return value carries the final state.
- direction¶
"forward"or"backward".
- n_steps¶
The number of user time steps in this call. PyTorch may prune backward callback nodes that are not on the requested gradient path. In particular, eager
torch.autograd.gradcan prune the step-zero hook when initial wavefields are not requested; a backward callback is not a guaranteed finalisation hook.
- dt¶
The caller’s time step.
time == step * dtis measured from the start of this call.
- internal_dt¶
The CFL-resolved time step used inside propagation.
- time¶
Time of this state boundary in the caller’s units. Pressure and stress fields are at integer boundaries while staggered velocity components have their usual half-step offsets;
timedoes not erase those component-specific offsets.
In a
forward_callbackthe wavefields are the forward wavefields; in abackward_callbackthey are the adjoint wavefields, and the time-coincident forward wavefields may additionally be available throughrole="forward"– seeget_wavefield(),has_forward_wavefieldandavailable_roles.Initialise the state.
Deepwave constructs this; a caller normally only receives one. A test double that constructs its own must supply every argument below.
- param dt:
The user time step size.
- param step:
The current user time-step index.
- param wavefields:
A dictionary of wavefields.
- param models:
A dictionary of models.
- param gradients:
A dictionary of gradients.
- param fd_pad:
The padding used for the finite difference stencil.
- param pml_width:
The width of the PML.
- param forward_wavefields:
Optionally, in a
backward_callback, a dictionary of the forward wavefields reconstructed at the same time step as the adjoint wavefields inwavefields.- param direction:
Which pass this state belongs to.
- param n_steps:
The number of user time steps in this pass.
- param internal_dt:
The CFL-resolved time step.
- property available_roles: tuple[str, ...]¶
The
rolevaluesget_wavefield()will accept here.("adjoint",), or("adjoint", "forward")when the backend supplied the forward wavefield.None, which means the same as"adjoint", is not listed: it is not a distinct capability to branch on.
- get_gradient(name: str, view: Literal['full', 'pml', 'inner'] = 'inner', mode: Literal['read', 'write'] = 'write') Tensor¶
Get a gradient.
- Parameters:
name – The name of the gradient.
view – The part of the gradient to return. One of ‘inner’ (the model region, default), ‘pml’ (the model region plus the PML), or ‘full’ (the entire gradient, including finite difference padding).
mode –
"write"(the default) returns the live accumulator;"read"returns.detach()of it. Unlikeget_wavefield()andget_model(), this one is never a detached snapshot of a buffer that is about to be overwritten: mutating the gradient is the point. As on those two,"write"is the permanent default.
- Returns:
The specified part of the gradient, as a live view into the intermediate gradient accumulator: mutating it in place changes the gradient Deepwave ultimately returns, and the changed result need not be a derivative of the unchanged forward loss. This is not the completed gradient with respect to the caller’s model. Only the compiled backend populates gradients;
writable()reports whether this one does.
- get_model(name: str, view: Literal['full', 'pml', 'inner'] = 'inner', mode: Literal['read', 'write'] = 'write') Tensor¶
Get a model.
- Parameters:
name – The name of the model.
view – The part of the model to return. One of ‘inner’ (the model region, default), ‘pml’ (the model region plus the PML), or ‘full’ (the entire model, including finite difference padding).
mode –
"write"(the default) returns exactly the tensor Deepwave holds;"read"returns.detach()of it. As forget_wavefield(),"read"is a detached view, not a copy – ``.clone()`` it if you keep it past the end of the callback – and"write"is the permanent default, not scheduled to flip in any release (see Stability policy).
- Returns:
The specified part of the model. For an internal model name, writing into it in place is supported and changes the running simulation, as it did in
v0.0.27; there is noset_modelbecause writing in place is the route.writable(name, "model")reports whether that holds for this name.
- get_wavefield(name: str, view: Literal['full', 'pml', 'inner'] = 'inner', role: Literal['adjoint', 'forward'] | None = None, mode: Literal['read', 'write'] = 'write') Tensor¶
Get a wavefield.
- Parameters:
name – The name of the wavefield.
view – The part of the wavefield to return. One of ‘inner’ (the model region, default), ‘pml’ (the model region plus the PML), or ‘full’ (the entire wavefield, including finite difference padding).
role – Which wavefield to return, when both are available inside a
backward_callback.None(default) or"adjoint"returns the primary wavefield of the callback (the forward field in a forward callback, the adjoint field in a backward callback)."forward"returns the reconstructed forward wavefield time-coincident with the adjoint field, for building custom imaging conditions; it is available only when the backend supplied forward wavefields, whichhas_forward_wavefieldandavailable_rolesreport. Understorage_compression=Trueit is reconstructed from the lossy 8-bit history, so it is an approximation of the forward field (of order 0.1% of the local block maximum) rather than the field the forward pass propagated.mode –
"write"(the default) returns exactly the tensor Deepwave holds."read"returns.detach()of it, removing the autograd edge and stating that you do not intend to write through it."read"is a detached view, not a copy: it does not make the tensor immutable and it does not stop the buffer being overwritten at the next time step. If you keep it past the end of the callback, ``.clone()`` it."write"is the permanent default: it is not scheduled to flip in any release (see Stability policy). Writing into the returned tensor in place is supported for an internal wavefield name, exactly as inv0.0.27;writable()reports which names those are, andset_wavefield()is the checked route that validates shape, dtype and device for you.
- Returns:
The specified part of the wavefield.
- Raises:
NotImplementedError – If
role="forward"is asked for where the backend did not supply the forward wavefield.
- property has_forward_wavefield: bool¶
Whether
get_wavefield(..., role="forward")will work here.The branch-on-able form of the
NotImplementedErrorget_wavefield()raises forrole="forward":Trueexactly when the backend supplied the reconstructed forward wavefield for this callback. Lets one callback be written that computes an imaging condition where the forward field is available and falls back to inspecting the adjoint field where it is not, without atry/except.
- set_wavefield(name: str, tensor: Tensor, view: Literal['full', 'pml', 'inner'] = 'inner') None¶
Write into a wavefield of the running simulation.
The supported way to alter the simulation state from a callback.
viewmirrorsget_wavefield()’s default, sostate.set_wavefield(n, state.get_wavefield(n) + 1)round-trips.The value is copied into Deepwave’s own tensor;
tensoritself is not retained, and no broadcasting, casting or device transfer is performed. A mismatch raises rather than silently doing the conversion, because a silent host-to-device copy in the middle of a time loop is a performance trap and a silent cast loses precision where it is least visible.- Parameters:
name – The name of the wavefield to write into.
tensor – The values to write. Must match the target’s shape, dtype and device exactly.
view – The part of the wavefield
tensorcorresponds to, as forget_wavefield().
- Raises:
DeepwaveValueError – If
tensor’s shape or device differs from the target’s.DeepwaveTypeError – If
tensor’s dtype differs from the target’s.KeyError – If
nameis not a wavefield of this callback, listing the names that are.
- writable(name: str, kind: Literal['wavefield', 'model', 'gradient'] = 'wavefield') bool¶
Whether writing to
nameis supported in this callback.The branch-on-able form of the class docstring’s three promises, so one callback can be written against all nine propagators and both passes without a
try/exceptaround every write. It reports whether a write is supported, not merely whether one would happen to land somewhere today.- Parameters:
name – The name to ask about.
kind – Which name space
namebelongs to. The same string can be both a model and a gradient name ("v"is both), so the space has to be named for the question to be well posed.
- Returns:
For
kind="gradient", whether that gradient is present and so can be mutated in place throughget_gradient()(true only on the compiled backend’s backward pass). Forkind="wavefield", whetherset_wavefield()will accept that name, which is also whetherget_wavefield(name)may be written into in place. Forkind="model", whetherget_model(name)may be written into in place: true for an internal model name this callback holds, false for a name that is merely derived from the internal state (there is nothing behind such a name to write into). An unknown name returnsFalserather than raising – this is a query.A deprecated name that the accessors still resolve (the
DEPRECATED_CALLBACK_NAMESaliases) answers for the name it resolves to, so this agrees with the accessor it predicts. Being a query, it does not warn about the alias; the accessor does.During a differentiable forward pass, model and wavefield writes are unsupported and return
Falseeven when the name is present.
MemoryEstimate¶
- class deepwave.MemoryEstimate¶
The dict returned by
estimate_memory().
Type aliases¶
The closed sets of values Deepwave’s arguments accept, named so that they can be used in your own annotations. boundary entries are deliberately plain str rather than a Literal; see Stability policy for why.
- deepwave.InitialState¶
alias of
PropagatorOutput|Mapping[str,Tensor|AuxiliaryState]
- deepwave.ON_NODE_TOLERANCE¶
How close to a grid node a coordinate must be, in cells, for Deepwave to treat it as being on that node.
This one number decides three things:
location_interpolation="auto"builds adeepwave.Hicksinterpolator only if some coordinate is further than this from its component’s node (but see that argument’s documentation for therequires_gradexception, which always interpolates);location_interpolation="none"rejects a locations Tensor that has any coordinate further than this off-node;deepwave.to_cells()withsnap=Truecleans up floating-point error out to this distance.
It is the single source for all three:
Hicks’ defaultepsis this value, and so is the propagators’ internal node test. Changing it changes which calls are interpolated, and therefore changes numbers.Stability: stable (see Stability policy).
boundaries Function¶
- deepwave.boundaries(ndim: int, *, default: str = 'pml', width: int | Sequence[int] = 20, top: str | None = None, dim0_begin: str | None = None, dim0_end: str | None = None, dim1_begin: str | None = None, dim1_end: str | None = None, dim2_begin: str | None = None, dim2_end: str | None = None) tuple[list[str], list[int]]¶
Builds a matching
(boundary, pml_width)pair.The two arguments have to agree – a named boundary face must have
pml_width0, and a zero-width face that is not named is an implicit reflector (see Reflecting edges). Building them together makes it impossible to get that wrong:boundary, pml_width = deepwave.boundaries(2, top="free") out = deepwave.scalar( v, dx, dt, ..., boundary=boundary, pml_width=pml_width )
In 3-D, and with a per-face width:
boundary, pml_width = deepwave.boundaries( 3, top="free", width=[0, 20, 20, 20, 20, 20] )
Faces are named by axis, in the same order and with the same words the
pml_widthargument uses:beginning of first (slowest) dimension –
dim0_beginend of first dimension –
dim0_endbeginning of second dimension –
dim1_beginend of second dimension –
dim1_end…
topis an alias fordim0_begin, provided because the beginning of dimension 0 is conventionally the free surface in seismic models; Deepwave itself gives no axis a physical meaning, so usedim0_begin…``dim2_end`` if your model is oriented differently. There are deliberately nobottom/left/right/front/backspellings: they cannot be defined without inventing a handedness convention that the rest of the library refuses to fix (see Units and source conventions).- Parameters:
ndim – The number of spatial dimensions: 1, 2 or 3.
default – The condition for every face not named by a keyword. One of
"pml"(the default),"free","rigid","periodic".width – The PML width in cells, applied only to
"pml"faces: a single non-negative int, or a sequence of2 * ndimof them. Every non-"pml"face gets width 0 automatically, which is what keeps the two returned lists consistent.top – An alias for
dim0_begin. Giving both raises.dim0_begin – The condition for the beginning of dimension 0.
dim0_end – The condition for the end of dimension 0.
dim1_begin – The condition for the beginning of dimension 1.
dim1_end – The condition for the end of dimension 1.
dim2_begin – The condition for the beginning of dimension 2.
dim2_end – The condition for the end of dimension 2.
- Returns:
a list of
2 * ndimcondition names and a list of2 * ndimwidths, ready to pass straight to any propagator’sboundary=andpml_width=arguments. This helper does not know which propagator the pair is for, so it builds any combination of names; a propagator that does not accept one of them (see Which faces each propagator accepts) raises when it is given the pair.- Return type:
(boundary, pml_width)- Raises:
DeepwaveValueError – If
ndimis not 1, 2 or 3; if adim{k}_*keyword is given for akthatndimdoes not have; if bothtopanddim0_beginare given; if a condition name is not recognised; ifwidthis negative or the wrong length; or if a"pml"face would end up with width 0, which is the implicit reflector this helper exists to replace.
Stability: provisional (see Stability policy).
Callback¶
- class deepwave.Callback(*args, **kwargs)¶
A propagator callback: called with the live
CallbackState.Anything callable with one positional argument satisfies it – a
def, alambda, a bound method, afunctools.partial, or an object with a__call__. The parameter is positional-only (the/in the signature below), so your callback’s parameter may be named anything; a protocol without it would silently reject every callback whose parameter is not calledstate.runtime_checkableis set, soisinstance(cb, deepwave.Callback)works – but be aware that, as for every runtime-checkable protocol, it checks only for the presence of__call__, not the signature. It will therefore accept a callable of the wrong arity; use it as a sanity check, not as validation.Example:
def report(state: deepwave.CallbackState) -> None: print(state.step, state.get_wavefield("wavefield_0").norm()) out = deepwave.scalar(v, dx, dt, ..., forward_callback=report)
Stability: stable (see Stability policy).
- __call__(state: CallbackState, /) None¶
Receives the live state for one time step.
cosine_taper_end Function¶
- deepwave.discretisation.cosine_taper_end(signal: Tensor, n_taper: int) Tensor¶
Tapers the end of the final dimension of a torch.Tensor using a cosine.
A half period, shifted and scaled to taper from 1 to 0, is used. The taper is dimensionless, so the result is in the same units as signal, whatever they are, and the sample interval of the final dimension is unchanged.
- Parameters:
signal – The torch.Tensor that will have its final dimension tapered. Its units are the caller’s own and are carried through.
n_taper – The length of the cosine taper, in number of samples of the final dimension.
- Returns:
The tapered signal, in the units of signal.
upsample Function¶
- deepwave.discretisation.upsample(signal: Tensor, step_ratio: int, freq_taper_frac: float = 0.0, time_pad_frac: float = 0.0, time_taper: bool = False) Tensor¶
Upsamples the final dimension of a torch.Tensor by a factor.
Low-pass upsampling is used to produce an upsampled signal without introducing higher frequencies than were present in the input. The Nyquist frequency of the input will be zeroed.
Units are the caller’s own and are carried through: the result is in the same units as signal, and its sample interval is signal’s divided by step_ratio. step_ratio is a count and the two fractions are dimensionless.
- Parameters:
signal – The torch.Tensor that will have its final dimension upsampled. Its final dimension is time, uniformly sampled; its units are the caller’s own.
step_ratio – The integer factor by which the signal will be upsampled. The input signal is returned if this is 1 (freq_taper_frac, time_pad_frac, and time_taper will be ignored).
freq_taper_frac – A float specifying the fraction of the end of the signal amplitude in the frequency domain to cosine taper. This might be useful to reduce ringing. A value of 0.1 means that the top 10% of frequencies will be tapered before upsampling. Defaults to 0.0 (no tapering).
time_pad_frac – A float specifying the amount of zero padding that will be added to the signal before upsampling and removed afterwards, as a fraction of the length of the final dimension of the input signal. This might be useful to reduce wraparound artifacts. A value of 0.1 means that zero padding of 10% of the length of the signal will be used. Defaults to 0.0.
time_taper – A bool specifying whether to force the upsampled signal to zero at both edges (with a Hann window) after its inverse FFT. The FFT-based resampling treats the time axis as periodic. A signal that does not already decay to (near) zero at its edges – e.g. real/field data cropped from a longer recording – otherwise produces wraparound ringing in the resampled result. Leave as False if the signal already tapers to (near) zero at its edges, since the window also attenuates genuine signal there.
- Returns:
The signal after upsampling, in the units of signal, at a sample interval of signal’s divided by step_ratio.
downsample Function¶
- deepwave.discretisation.downsample(signal: Tensor, step_ratio: int, freq_taper_frac: float = 0.0, time_pad_frac: float = 0.0, time_taper: bool = False, shift: float = 0.0) Tensor¶
Downsamples the final dimension of a torch.Tensor by a factor.
Frequencies higher than or equal to the Nyquist frequency of the downsampled signal will be zeroed before downsampling.
The input time length must be divisible by
step_ratio. Pad or trim the signal explicitly when a different output length is needed.Units are the caller’s own and are carried through: the result is in the same units as signal, and its sample interval is signal’s multiplied by step_ratio. step_ratio is a count, the two fractions are dimensionless, and shift is in samples of signal.
- Parameters:
signal – The torch.Tensor that will have its final dimension downsampled. Its final dimension is time, uniformly sampled; its units are the caller’s own. Its length must be divisible by
step_ratio.step_ratio – The integer factor by which the signal will be downsampled. The input signal is returned if this is 1 and shift is 0 (freq_taper_frac, time_pad_frac, and time_taper will be ignored).
freq_taper_frac – A float specifying the fraction of the end of the signal amplitude in the frequency domain to cosine taper. This might be useful to reduce ringing. A value of 0.1 means that the top 10% of frequencies will be tapered after downsampling. Defaults to 0.0 (no tapering).
time_pad_frac – A float specifying the amount of zero padding that will be added to the signal before downsampling and removed afterwards, as a fraction of the length of the final dimension of the output signal. This might be useful to reduce wraparound artifacts. A value of 0.1 means that zero padding of 10% of the length of the output signal will be used. Defaults to 0.0.
time_taper – A bool specifying whether to force the signal to zero at both edges (with a Hann window) before this function’s FFT-based resampling, which treats the time axis as periodic. A signal that does not already decay to (near) zero at its edges – e.g. real/field data cropped from a longer recording – otherwise produces wraparound ringing in the resampled result. Leave as False if the signal already tapers to (near) zero at its edges, since the window also attenuates genuine signal there.
shift – Amount, in samples of the input signal, to shift the data in time before downsampling. Defaults to 0.0.
- Returns:
The signal after downsampling, in the units of signal, at a sample interval of signal’s multiplied by step_ratio.
cfl_condition Function¶
- deepwave.discretisation.cfl_condition(dy: float, dx: float, *args: Any, **kwargs: Any) tuple[float, int]¶
Calculates the time step interval for 2D models.
This is a convenience wrapper around cfl_condition_n for 2D models, and takes its units: any length unit and any time unit, as long as the grid spacings, dt and max_vel share the same pair.
- Parameters:
dy – The grid spacing in the y-dimension, in the length unit max_vel is expressed in.
dx – The grid spacing in the x-dimension, in the same unit as dy.
*args – Additional positional arguments to pass to cfl_condition_n.
**kwargs – Additional keyword arguments to pass to cfl_condition_n.
- Returns:
inner_dt: A time step interval that obeys the CFL condition, in the same time unit as dt.
step_ratio: The integer dt / inner_dt, dimensionless.
- Return type:
Tuple
cfl_condition_n Function¶
- deepwave.discretisation.cfl_condition_n(grid_spacing: float | Sequence[float], dt: float, max_vel: float, eps: float = 1e-15, courant_number: float | None = None) tuple[float, int]¶
Calculates the time step interval to obey the CFL condition.
The output time step will be a factor of the input time step.
We use the maximum dt to calculate the integer factor (step_ratio) we need to divide the provided dt by to ensure it is less than or equal to this value.
Units are the caller’s own: any length unit and any time unit will do, as long as grid_spacing, dt and max_vel are expressed in the same pair. step_ratio and the Courant number are dimensionless, and inner_dt comes back in the unit dt was given in.
- Parameters:
grid_spacing – The grid spacing in each spatial dimension, in the length unit max_vel is expressed in. Can be a single float (applied as the only entry) or a sequence of floats.
dt – The time step interval, in the time unit max_vel is expressed in.
max_vel – The maximum absolute wavespeed in the model, in grid_spacing units per dt unit.
eps – Retained for call compatibility. Zero speed is handled explicitly, so this parameter no longer regularises the speed or changes the selected time step.
courant_number – An optional Courant number to use instead of the conservative default (0.6). If None (the default), 0.6 is used. If given, it is the actual dimensionless Courant number the time step will target; a larger value gives a larger
inner_dt(fewer internal steps). This function does not itself enforce an upper bound: a caller that needs to guarantee stability should compare it againstdeepwave.discretisation.critical_courant_number()first and raise if it is exceeded, which is what the propagators do while setting up a call.
- Returns:
inner_dt: A time step interval that obeys the CFL condition, in the same time unit as dt.
step_ratio: The integer dt / inner_dt, dimensionless.
- Return type:
Tuple
critical_courant_number Function¶
- deepwave.discretisation.critical_courant_number(accuracy: Literal[2, 4, 6, 8], *, propagator: str | Callable[[...], Any] | type | None = None) float¶
Returns the maximum stable Courant number for a leapfrog scheme.
These are the homogeneous critical Courant numbers C_crit re-derived from the actual finite-difference stencil coefficients (see the appendix of Discretisation: the exact scheme). A propagation is stable when
dt * v_max * sqrt(sum_i 1/dx_i^2) <= C_crit. Staggered propagators also check the density-weighted material operator;v_maxmust cover that conservative bound for heterogeneous models.The critical number depends on whether the propagator uses a staggered grid, so name the propagator and let Deepwave answer that:
>>> import deepwave >>> deepwave.discretisation.critical_courant_number( ... 4, propagator=deepwave.elastic ... ) 0.857142857142857...
- Parameters:
accuracy – The spatial finite-difference accuracy order (2, 4, 6, or 8).
propagator – The propagator the number is for, as the functional (
deepwave.elastic), the Module class (deepwave.Elastic) or the name ("elastic").
- Returns:
The critical (maximum stable) Courant number.
- Raises:
DeepwaveValueError – If
propagatoris not given, or ifaccuracyis not one of 2, 4, 6, 8.DeepwaveTypeError – If
propagatordoes not name a propagator.
suggest_discretisation Function¶
- deepwave.suggest_discretisation(min_vel: float, max_freq: float, ndim: int, accuracy: Literal[2, 4, 6, 8] | None = None, *, propagator: str | Callable[[...], Any] | type | None = None, courant_number: float | None = None, max_vel: float | None = None, dispersion_error: float = 0.01, min_points_per_wavelength: float = 6.0) Discretisation¶
Suggests a grid spacing and time step for a wave you want to model.
Given the slowest wavespeed in your model and the highest frequency you need to propagate, this returns a cell spacing fine enough to carry that wave without excessive numerical dispersion, and the largest time step that is then stable on it. It is a starting point to sanity-check your own numbers against, not an oracle: it knows nothing about your geometry, your PML, or how far your waves have to travel.
Where the numbers come from, none of which is re-derived here:
The shortest wavelength is \(\lambda_\mathrm{min} = v_\mathrm{min}/f_\mathrm{max}\) and the cells per wavelength are \(\lambda_\mathrm{min}/\Delta x\) (see Accuracy and grid design).
The minimum points per wavelength for each order is the tabulated “Minimum points-per-wavelength (axial)” table in Discretisation: the exact scheme, which is the smallest sampling at which that stencil’s axial phase-velocity error falls to
dispersion_error. Axial is the conservative direction: dispersion is not isotropic, and at 5 points per wavelength the regular 2-D order-2 axial phase velocity is \(0.936\,v\) while the diagonal is \(0.967\,v\), so a diagonally travelling wave is slightly better resolved than this suggestion promises.dtis the CFL limit fromdeepwave.discretisation.cfl_condition_n(), atDEFAULT_COURANT_NUMBERunless you passcourant_number. The number of dimensions enters only through that condition’s \(\sqrt{\sum_i 1/\Delta x_i^2}\) term.
Two criteria, and which one binds. Deepwave’s own runtime check warns when a propagator is given fewer than six cells per wavelength, and that single number is calibrated for the default
accuracy=4. It is stricter than the 1% axial-dispersion criterion for orders 4, 6 and 8 (which ask for only 5.3, 3.9 and 3.4), because it is also standing in for everything the plane-wave dispersion analysis leaves out: how a point-source wavefront, a sharp model contrast and the PML are sampled. The suggestion therefore respects both, taking whichever asks for more cells, so that what it hands you never immediately provokes Deepwave’s own warning.notesays which one bound. The consequence at the default 1% target is that orders 4, 6 and 8 all land on the same six-cell grid, and the coarser grids the higher orders could otherwise run on are not offered; lowermin_points_per_wavelengthto see them, but expect the warning when you then call a propagator.Choosing the accuracy. With
accuracy=Nonethe order that minimises a rough estimate of the total work is chosen. Cells scale as \((1/\Delta x)^\mathrm{ndim}\) and time steps as \(1/\Delta t \propto 1/\Delta x\), so the work goes aspoints_per_wavelength ** (ndim + 1)times a relative cost per cell of(ndim * accuracy + 3) / (ndim * 4 + 3)– the multiply-adds a stencil of that order needs, normalised to 1 ataccuracy=4. That weight is a rough relative figure and nothing more: it counts arithmetic and ignores cache behaviour, vector width and the wider halo, all of which make high orders dearer in practice than it says. Treat the choice as a suggestion to time, not a measurement. The chosen order and the runner-up, with the ratio between their estimates, are reported innote.- Parameters:
min_vel – The slowest (non-zero) wavespeed in the model, in the same length unit as the grid spacing you want back, per second.
max_freq – The highest frequency, in Hz, that must be propagated without excessive dispersion. This is the top of the wavelet’s significant band, not its dominant frequency.
ndim – The number of spatial dimensions (1, 2 or 3).
accuracy – The spatial finite-difference order (2, 4, 6 or 8), or
None(the default) to choose the cheapest by the cost model above.propagator – The propagator the suggestion is for, as the functional (
deepwave.elastic), the Module class (deepwave.Elastic) or the name ("elastic"). It selects the staggered or regular-grid column of the dispersion table and the matching critical Courant number. WithNone(the default) the conservative value of each is used: the regular-grid dispersion numbers, which ask for slightly more cells, and the staggered critical Courant number, which is slightly lower.courant_number – The Courant number to compute
dtat.None(the default) usesDEFAULT_COURANT_NUMBER. A value above the critical Courant number for the order is rejected; whenaccuracyisNone, orders whose critical number it exceeds are simply not considered.max_vel – The fastest wavespeed in the model, in the same length unit per second as
min_vel, which is what the CFL condition is set by. Defaults tomin_vel, i.e. to assuming a homogeneous model – pass it for any model with faster material in it, or the suggesteddtwill be too large for the grid and a propagator will have to sub-step it.dispersion_error – The axial phase-velocity error to design the grid for:
0.01(1%, the default) or0.001(0.1%). The error is per wavelength travelled and accumulates, so a 1% error slips a full cycle over ~100 wavelengths of propagation; use 0.001 for long models.min_points_per_wavelength – The floor, in cells per wavelength, applied to the dispersion criterion, defaulting to the six cells per wavelength that Deepwave’s own runtime check wants (see above). Pass
0to follow the dispersion table alone.
- Returns:
A
Discretisationnaming the suggestedgrid_spacing,accuracy,dt, thepoints_per_wavelengthandcourant_numberthey correspond to, and anote.- Raises:
DeepwaveValueError – If
min_vel,max_freq,max_velorcourant_numberis not a finite positive number, ifmin_points_per_wavelengthis not a finite non-negative number, ifmax_velis belowmin_vel, ifndimis not 1, 2 or 3, ifaccuracyis not 2, 4, 6, 8 orNone, ifdispersion_erroris neither 0.01 nor 0.001, or ifcourant_numberexceeds the critical Courant number of the requested order (or of every order, when one is being chosen).DeepwaveTypeError – If one of the numeric arguments is not a number, or
propagatordoes not name a propagator.
Examples
>>> import deepwave >>> suggestion = deepwave.suggest_discretisation(1500, 25, 2) >>> suggestion.grid_spacing (10.0, 10.0) >>> suggestion.accuracy 4
Discretisation¶
- class deepwave.Discretisation(grid_spacing: tuple[float, ...], accuracy: Literal[2, 4, 6, 8], dt: float, points_per_wavelength: float, courant_number: float, note: str)¶
What
suggest_discretisation()returns.Stability: provisional (see Stability policy).
- grid_spacing¶
The suggested cell spacing, one entry per dimension (equal in every dimension), in the model’s length unit. Pass it straight to a propagator’s
grid_spacing.
- accuracy¶
The spatial finite-difference order the suggestion is for (the one that was requested, or the one that was chosen).
- Type:
Literal[2, 4, 6, 8]
- dt¶
The largest time step, in seconds, that satisfies the CFL condition on this grid at
courant_number.- Type:
- note¶
A human-readable sentence saying which constraint was binding, what the runner-up accuracy was, and what was assumed.
- Type:
Create new instance of Discretisation(grid_spacing, accuracy, dt, points_per_wavelength, courant_number, note)
DEFAULT_COURANT_NUMBER¶
- deepwave.DEFAULT_COURANT_NUMBER¶
The Courant number Deepwave targets when
courant_numberis not given.Every propagator, and
deepwave.discretisation.cfl_condition_n(), uses this value to choose the internal time step when the caller leavescourant_number=None. It is deliberately conservative – the same number for everyaccuracyorder and every dimensionality, well below the critical Courant numberdeepwave.discretisation.critical_courant_number()reports for any of them – because it is the value Deepwave has always used, and a run that does not setcourant_numberis reproduced bit-for-bit by passing this constant explicitly.It is exported so that the default can be named rather than hard-coded:
courant_number=1.2 * deepwave.DEFAULT_COURANT_NUMBERsays what it means, andRunInfo.courant_numbercan be compared against it to tell a defaulted run from an explicit one. Raisingcourant_numberabove it permits a larger internal time step (fewer steps, faster) at the cost of stability margin; the value is validated against the critical Courant number and rejected if it would be unstable.Stability: stable (see Stability policy).
check_finite Function¶
- deepwave.check_finite(x: Tensor, name: str = 'tensor') Tensor¶
Raises if x contains a NaN or Inf, otherwise returns x unchanged.
A cheap (one reduction over x) sanity check for a diverged simulation, for the tensors Deepwave does not check itself. Every propagator now checks its own outputs – the final wavefields and the receiver amplitudes – before returning them, and raises this same error type if any is non-finite; that is what
check_output_finite=True(the default) does. What Deepwave still does not check is every step, which would cost a synchronisation per step and is prohibitively slow, nor any tensor that is not a propagator output: a gradient, a loss, or a model an optimiser has just updated. Call this on those (or, for early detection during a long run, from inside aforward_callback/backward_callbackon the wavefield – see the “Detecting divergence (NaN) early” recipe in Troubleshooting) to fail fast with a clear message instead of discovering theNaNmuch later.- Parameters:
x – The tensor to check.
name – A label for x, used in the error message (e.g.
"receiver data","v.grad").
- Returns:
x, unchanged, so the call can be chained inline.
- Raises:
deepwave.DeepwaveFloatingPointError – If x contains any non-finite (
NaNorInf) value. This is also a FloatingPointError (the type this function raised historically), so an existingexcept FloatingPointErrorstill catches it, and it is a deepwave.DeepwaveError, soexcept deepwave.DeepwaveErrorcatches it alongside Deepwave’s other errors.
Example
>>> import deepwave >>> out = deepwave.scalar(v, grid_spacing, dt, ...) >>> receiver_data = deepwave.check_finite( ... out.receiver_amplitudes, "receiver data" ... )
Stability: provisional (see Stability policy).
estimate_memory Function¶
- deepwave.estimate_memory(propagator: Callable[[...], Any] | str, model_shape: Sequence[int], nt: int, n_shots: int, dtype: dtype = torch.float32, *, accuracy: int = 4, pml_width: int | Sequence[int] = 20, model_gradient_sampling_interval: int = 1, storage: str | Storage | None = None, storage_mode: Literal['device', 'cpu', 'disk', 'none'] | None = None, storage_path: str | PathLike[str] | None = None, storage_compression: bool | None = None, n_models_requiring_grad: int | None = None, models_requiring_grad: Sequence[str] | None = None, q_n_mechanisms: int = 1, device: str | device = 'cpu', n_segments: int | None = None) MemoryEstimate¶
Rough byte-size estimate for a propagator call, before running it.
This is meant to answer “will this run fit in memory” for a proposed
model_shape/nt/n_shots/storage_modebefore spending the time to build the tensors and start propagating. It mirrors, as closely as is practical without actually allocating anything, the two dominant sources of memory use identified for the backward pass:StorageManager(the sampled wavefield history kept for the model gradient) andallocate_model_gradients(the per-thread/per-shot scratch buffers used to accumulate that gradient without a race). It also gives a (much rougher) figure for the wavefields alive during the forward time loop itself.Approximations (read before trusting the numbers):
The number of same-shaped buffers alive at once during the forward time loop is equation- and dimensionality-specific (each block below is the total wavefield count returned by that propagator, both PML memory variables included); this is the single biggest approximation. Pass
propagatorto fill it in from the table below (usinglen(model_shape)as the number of dimensions); without it, a placeholder of 3 is used:scalar/scalar_bornbackground:2 + 2 * ndim(2-D: 6, 3-D: 8);scalar_born(background + scattered): double that (2-D: 12, 3-D: 16).acoustic:1 + 3 * ndim(2-D: 7, 3-D: 10).elastic: 13 (2-D), 27 (3-D).
The number of stored wavefield histories is not the number of models you ask for a gradient for: the engine allocates one
StorageManagerslot per stored spatial derivative, which foracousticandelasticis several slots per model. Name the models withmodels_requiring_gradand this is filled in exactly, from the propagator’s own slot function (the one its forward pass uses). Give onlyn_models_requiring_gradand a worst case over that many models is used instead; give neither a propagator nor names and the old one-slot-per-model assumption remains, which under-predictsacoustic’s density gradient by1 + ndimtimes andelastic’s three-parameter gradient by 9/3 in 3-D.scalarwith abackward_callbackkeeps a second full wavefield history (the raw forward field the callback is given); that extra copy is not modelled here.Like
StorageManageritself, any dtype other thantorch.float32is costed as 8 bytes/element (i.e. as if it weretorch.float64); this matches current Deepwave behaviour but is a simplification for exotic dtypes.model_shapeshould be your unpadded model’s spatial shape; this function adds the finite-difference (accuracy // 2per side) and PML padding itself, the same wayextract_surveydoes. The staggered-grid propagators (acoustic,elastic) really pad one cell less on the far side of each axis, so for them every per-model term here is slightly (about 0.7% per axis at 140 padded cells) generous.storage_compression=Trueis modelled fromStorageManager’s own block-quantisation formula (8-per-side blocks, 1 byte/element plus onedtype_size-wide scale per block): it shrinks the sampled-history term, butStorageManageralways keeps two extra full-size single-shot buffers alive alongside the compressed history (for building the next block before it is written), so compression’s net saving is smaller than the raw quantisation ratio, especially at smalln_shots. Disk-mode host/device splits are not modelled;storage_mode="disk"is costed the same as"device", an upper bound on total (device + host/disk) footprint.This is a static estimate: it does not know about
survey_padshrinking the model per shot, per-shot-batched models, or checkpointing schemes external to Deepwave.The unmodelled fixed per-call overhead is the propagator’s other full-size buffers: the backward pass’s own set of padded wavefields (a second copy of the forward set counted above), the padding-removed wavefields returned to the caller (alive across
.backward()), the padded/staggered internal model copies, and one gradient buffer per internal model requiring a gradient. It scales with the propagator’s wavefield and model counts, not withnt, so it matters most for 3-D elastic at smallnt: measured at 96^3, 1 shot,nt=40, it is about 0.9 GiB (of which 0.27 GiB is the backward wavefield set, 0.24 GiB the returned wavefields, 0.19 GiB the internal model copies, 0.09 GiB the ping-pong buffers and 0.08 GiB the model gradients).Measured against real propagator runs (see Troubleshooting’s “Measured accuracy of estimate_memory” table), this estimate under-predicts real peak memory at every configuration tested, never over-predicts: by roughly 4-20% for the default
storage_mode="device"uncompressed with the storage slots named exactly (models_requiring_grad), growing markedly worse (up to ~70%) as the modelled terms (sampled history, gradient scratch) shrink relative to that fixed per-call overhead – i.e.storage_compression=True, a largemodel_gradient_sampling_interval, andstorage_mode="cpu"/"disk"are all directions where the gap widens, as is a shortnton a many-wavefield propagator. Treat the returned figure as a lower bound and leave headroom, not a ceiling.n_segments=is part of that guarantee rather than an exception to it. Every term it adds is a floor on what it stands for: the boundary states are sized field by field, without the finite-difference halo and with the PML memory variables costed as the boundary slabs they are stored as, and they are counted once per segment only where a model gradient is being computed – a forward-only segmented run holds one state, whatever the segment count. Measured against real peaks on a 64-cubedscalarrun at segment counts from 2 to 120, with and without a model gradient, the figure stayed below the measured peak in every case.
- Parameters:
propagator – The propagator this call would use: its name, its function, or its
Moduleclass. Any of the nine is accepted. It is required, because every number below depends on it: the forward wavefield count comes from its spec, and the backward-storage slot count from its own storage-slot rule. (It used to be optional, substituting a hard-coded guess of three forward wavefields – a figure no propagator’s spec agreed with.) RaisesValueErrorif it is not one of the recognised propagators.model_shape – The spatial shape of one (unpadded) model, e.g.
v.shape.nt – The number of user-facing time steps (as passed to the propagator, i.e. matching
source_amplitudes.shape[-1]).n_shots – The number of shots propagated together.
dtype – The dtype of the model/computation.
accuracy – The finite-difference accuracy order.
pml_width – The PML width (int, or one value per face as accepted by the propagators).
model_gradient_sampling_interval – As passed to the propagator.
storage – The storage policy, as one object: a mode string or a
deepwave.Storage. An alternative spelling for the three individualstorage_*arguments, which are permanent and not deprecated; passing both spellings raises.storage_mode – One of
"device","cpu","disk", or"none"("none"estimates 0 for both backward-storage and gradient-scratch, matching whatStorageManageractually allocates in that mode).storage_path – Accepted so that one dict of storage arguments can be passed to both this function and the propagator. It does not affect any byte count: disk mode is costed identically to device mode (see the approximations above).
storage_compression – As passed to the propagator (
device/cpu/diskmodes only; ignored, matchingStorageManager, whenstorage_mode="none"). Lossy – changes the model gradient. Seedeepwave.elastic().n_models_requiring_grad – How many model tensors require a gradient, when you do not want to say which (defaults to 1). With
propagator, the storage-slot and gradient-buffer counts are the worst case over any choice of that many of that propagator’s models, i.e. an upper bound. Prefermodels_requiring_grad, which is exact. RaisesValueErrorif negative, if it exceeds the propagator’s model count, or if it contradictsmodels_requiring_grad.models_requiring_grad – Which models require a gradient, by their user-facing argument names:
"v"forscalar;"v"and"dv"forscalar_born;"v"and"rho"foracoustic;"lamb","mu"and"buoyancy"forelastic. Any propagator’s names aredeepwave.PropagatorSpec.model_names(). This is the accurate option on the four propagators that carry storage-slot tables (scalar,scalar_born,acoustic,elastic): the number of stored wavefield histories is then taken from the propagator’s own storage-slot function, so it matches what the engine really allocates (e.g.["v", "rho"]in 3-Dacousticstores four histories, not two). On the other seven the same conservative one-slot-per-model fallback asn_models_requiring_gradis used. RaisesValueErrorfor an unrecognised or repeated name, or a repeated name.storage_compression – If True, the stored wavefield history is compressed, which changes the arithmetic this function models. A
deepwave.Storagepassed here contributes itsmodeand itslossy_compression, which change the arithmetic.pathhas no bearing on a byte count.q_n_mechanisms – The number of GSLS relaxation mechanisms, for the viscoacoustic propagator, whose wavefield count depends on it. Ignored by the other families. Defaults to 1.
device – The device the propagation would run on (only its type, cpu vs cuda, matters here).
n_segments –
If given, the number of time segments the run would be split into by
deepwave.checkpointing.run_segments()(or by the propagator’s owncheckpoint_segments=). The stored history is then divided byn_segmentsand an auxiliary-state term added for the states held alive between segments. Without it, the estimate has no model of checkpointing at all and over-predicts a checkpointed run’s peak substantially. Held to the same standard as the propagator’s own spelling of it: a bool or a non-int raisesDeepwaveTypeError, and a value below 1 or aboventraisesDeepwaveValueError(a run cannot have more segments than time steps, and the propagator refuses that too).How many auxiliary states this costs depends on whether a model gradient is being computed. With one, each segment’s state is held from the forward pass until that segment is recomputed, so the term scales with
n_segments. Without one – no model named inmodels_requiring_grad, orn_models_requiring_grad=0– there is nothing to checkpoint: the segmented driver threads a single state forward and drops the previous one, so the term is the size of one state however many segments there are.
- Returns:
a dict (see that TypedDict) with the following keys:
padded_model_shape: model_shape after adding finite-difference and PML padding.padded_model_elements: number of elements in one padded model.n_threads: the thread count used for the gradient-scratch estimate (torch.get_num_threads()on CPU, 1 on CUDA).forward_wavefields_bytes: estimated bytes for the forward time-loop wavefields.backward_storage_bytes: estimated bytes for the sampled wavefield history kept for the model gradient (StorageManager).gradient_scratch_bytes: estimated bytes for the per-thread/per-shot gradient accumulation buffers (allocate_model_gradients), one per internal model requiring a gradient (again more than one per user-facing model foracoustic/elastic; the gradient buffers themselves are part of the unmodelled overhead above).auxiliary_state_bytes: estimated bytes for the segment auxiliary states held alive between checkpointed segments. Zero unlessn_segmentswas given and is greater than 1. Each state is sized field by field, with the finite-difference halo removed and the PML memory variables costed as the boundary slabs they are actually stored as, so it is a lower bound on the state rather than a full-grid over-estimate of it.total_bytes_lower_bound: the sum of the four components above.
- Return type:
Example
>>> import deepwave >>> est = deepwave.estimate_memory( ... model_shape=(2301, 751), nt=750, n_shots=115, accuracy=4, ... pml_width=20, propagator="scalar", ... models_requiring_grad=["v"], ... ) >>> est["total_bytes_lower_bound"] / 1e9 648.41...
(That figure is dominated by
backward_storage_bytes, since it stores 750 full-model snapshots per shot for the gradient – exactly the cost thatmodel_gradient_sampling_intervalor a smallerstorage_modelets you trade against recompute or disk I/O; see Troubleshooting.)
Storage¶
Where and how the wavefield history that the model gradient is computed from is kept. Every propagator accepts the policy as one storage= object, or as the individual storage_mode, storage_path and storage_compression arguments, which remain supported and are not deprecated. Passing both spellings in one call raises, because there would be no honest way to merge them. storage_compression=True (Storage.lossy_compression) is lossy: it changes the model gradient, by about 0.2-4% in 2-D and 0.2-8% in 3-D in relative L2 error, with the largest errors in the model’s outermost cells (measured on scalar_born’s dv gradient, normalised by its own maximum: 0.03-0.07 in 1-D, about 1e-4 in 2-D and about 1e-5 in 3-D at the edge, against below 1e-3, 2e-5 and 1e-5 in the interior), because an edge cell’s gradient sums large, largely cancelling contributions from the absorbing layer. Without compression the stored history is lossless; exact model gradients also require internal-step sampling and fixed numerical parameters. The keyword’s name does not say any of this, which is why its documentation does:
out = deepwave.scalar(
v, dx, dt, source_amplitudes=s, source_locations=sl,
storage=deepwave.Storage(mode="disk", path="/scratch/history"),
)
- class deepwave.Storage(mode: _StorageModeStr = 'device', path: StoragePath | None = None, lossy_compression: bool = False)¶
Where and how the wavefield history for the model gradient is kept.
One object carrying the whole storage policy, accepted as
storage=by every propagator (functional andModule, on both the constructor andforward), bydeepwave.checkpointing.run_segments(), bydeepwave.estimate_memory(). It is an alternative spelling, never a requirement: the individualstorage_mode/storage_path/storage_compressionarguments are permanent and are not deprecated. Passing both spellings in one call raises, because there would be no honest way to merge them.It is plain data – it carries no device, no time-step count and no shapes, so one object can be reused across a sweep. Derive a variant with
dataclasses.replace(storage, mode="disk").Stability: provisional (see Stability policy) – fields may be added as more of a storage policy becomes worth recording. The fields it has are not renamed or withdrawn without a release’s notice under
deepwave.DeepwaveFutureWarning, and the individualstorage_mode/storage_path/storage_compressionarguments it is an alternative spelling for are stable, as is passing a plain mode string tostorage=.- Parameters:
mode – One of
"device"(keep the history on the compute device, the default and the fastest),"cpu"(offload it to host memory),"disk"(write it to files underpath), or"none"(keep nothing, which makes the call forward-only with respect to the model gradient). On a CPU device"cpu"and"device"are the same thing and the engine resolves the former to the latter;deepwave.RunInfo.storagerecords which was used.path – The directory
mode="disk"writes into.None(the default) means the system temporary directory, resolved when you call, soTMPDIRset afterimport deepwaveis honoured. Acceptsstror anyos.PathLike(apathlib.Path). Ignored, with a warning, by every other mode. Each run makes its owndeepwave_tmp_<pid>_<random hex>subdirectory here when it allocates its wavefield history, and removes the whole subdirectory once nothing refers to that history any more (afterbackward(), or when the output and its graph go out of scope). Only a process killed outright leaves one behind, since no destructor runs then.lossy_compression – If True, store the history as 8-bit block max-abs quantisation. This is lossy and it changes the model gradient – see
deepwave.scalar()’sstorage_compressionentry for the measured error and what is unaffected. Defaults to False.
plan Function¶
deepwave.plan() answers “what will this run do, and what will it cost” without running it. It is not a model of a propagator call: it is the call, stopped before propagation and gradient-history allocation, so plan(...).info is the very deepwave.RunInfo the run would return and cannot drift from it:
p = deepwave.plan(
deepwave.scalar, v, dx, dt,
source_amplitudes=s, source_locations=sl, receiver_locations=rl,
)
print(p.summary())
Plan.warnings contains only warnings emitted by Deepwave itself. It does not capture or suppress warnings from PyTorch or other dependencies.
- deepwave.plan(propagator: str | Callable[[...], Any], *model_args: Any, **call_kwargs: Any) Plan¶
Resolves a propagator call without running it, and reports what it costs.
Call it exactly as you would call the propagator:
p = deepwave.plan( deepwave.scalar, v, dx, dt, source_amplitudes=s, source_locations=sl, receiver_locations=rl, ) print(p.summary())
The propagator’s name works too (
deepwave.plan("scalar", ...)), so a plan can be driven from a configuration file.This is not a model of a propagator call: it is the call, stopped at the moment before propagation and gradient-history allocation. The resolution – the internal time step, the step ratio, the inferred PML frequency, the survey extraction, the backend choice, the storage policy – is done by the propagator’s own code, so
Plan.infois the verydeepwave.RunInfothe run would return, and cannot drift from it.What it raises. Every error your call would raise about its arguments: a bad model, mismatched source and location shapes, receivers outside the model, a
courant_numberabove critical, amodel_gradient_sampling_intervalthat does not divide the time steps, an unknownstorage_mode,storage_mode="none"with a model requiring a gradient, and an unsupported option for the requested backend.What it cannot raise, because these depend on the run rather than on its arguments: an out-of-memory condition; a compiled-backend failure (including a disk write failure, and the missing-symbol error for an
(ndim, accuracy, dtype, device)combination that was not built, since which kernel variant is needed is settled after the point this stops at); and the refusals that atorch.functransform or a double backward triggers on the result.What it costs. It does the propagator’s setup, so it pads the models and resamples the source amplitudes just as the run would. Front-end preprocessing can allocate tensors proportional to the source time length; planning never allocates the propagation or gradient history.
Warnings.
Plan.warningscollects Deepwave’s own warnings on this execution context. It does not alter Python’s process warning filters, so unrelated threads emit warnings normally. Third-party warnings raised during setup follow the caller’s ordinary warning policy and are not added to the plan. A Deepwave warning promoted to an error by the caller’s filter is still collected as text during planning; the real call raises as usual.- Parameters:
propagator – The propagator to plan: the function itself (
deepwave.scalar), its name as a string ("scalar"), or a constructedModuleinstance (Scalar(v, dx)), which is planned exactly as it would run. TheModuleclass is refused, because it is not something that can be called and the plan for it would differ from the plan for any instance built from it – see_resolve_callable.*model_args – The positional arguments of that propagator, unchanged.
**call_kwargs – Its keyword arguments, unchanged.
checkpoint_segments=is understood here as well: the arguments a segmented call refuses are refused here too, and then the resolution reported describes the whole run while the memory estimate accounts for the segmentation.
- Returns:
the resolved call and its estimated cost.
- Return type:
- Raises:
DeepwaveRuntimeError – If the propagator returned without reaching the point a plan stops at, which means it is not instrumented.
- class deepwave.Plan(info: RunInfo, memory: MemoryEstimate, resampling: bool, warnings: tuple[str, ...] = (), checkpoint_segments: int | None = None)¶
What a propagator call would resolve to, and what it would cost.
Returned by
deepwave.plan(). Every field is produced by the propagator’s own resolution code, so it is what the call would really do rather than a second opinion about it.Stability: provisional (see Stability policy) – fields may be added, and
summary()may be reworded, as more of a run’s resolved configuration becomes worth reporting. The fields it has are not renamed or withdrawn without a release’s notice underdeepwave.DeepwaveFutureWarning.- info¶
Exactly the
deepwave.RunInfothe call would attach to its output, built by the same code. Read the resolved internal time step, step ratio, PML width and frequency, survey origin and extent, backend and storage policy off it.- Type:
- memory¶
The call’s estimate of its own footprint, in the same shape
deepwave.estimate_memory()returns. It is more accurate than the parametric form: the padded shape, the sampling interval, the storage-slot count and the wavefield count are read off the resolved run rather than derived from arguments. It remains a lower bound, including undercheckpoint_segments=– seedeepwave.estimate_memory()for what it does not model.
- resampling¶
Whether the call would sub-step, i.e. whether
info.step_ratiois greater than 1. The receiver amplitudes are resampled back to the requested rate, andinfo.internal_dtgives the rate they were computed at.- Type:
- warnings¶
Deepwave warnings the call would emit, as text. Collected on this execution context without changing process warning filters or spending the once-per-process warning budget.
- checkpoint_segments¶
The number of checkpointed time segments this plan costed, or
Nonefor an unsegmented call. The resolution ininfoalways describes the whole run; onlymemoryreflects the segmentation.- Type:
int | None
- summary() str¶
A short human-readable block describing the planned call.
- Returns:
A multi-line block covering the resolved grid and padded shape, the timing (the user’s
ntanddt, and the internal step count andinternal_dtthey resolve to), the backend, the storage policy, the memory breakdown, and any warnings the call would emit.
deepwave.diagnostics¶
- deepwave.diagnostics.report() Report¶
Collects the environment facts a Deepwave bug report should carry.
Print the result:
>>> import deepwave >>> print(deepwave.diagnostics.report()) Deepwave 0.1.0 PyTorch 2.5.1 ...
- Returns:
A
Report. It is a dataclass with named fields rather than a plaindictso that the shape of the answer is documented and so that nothing is tempted to parse the printed text.
Stability: provisional (see Stability policy).
- class deepwave.diagnostics.Report(deepwave_version: str, torch_version: str, python_version: str, machine: str, required_baseline: str | None, simd_features: dict[str, bool | None], cuda_available: bool, cuda_devices: tuple[str, ...])¶
A snapshot of the environment Deepwave is running in.
Returned by
report(). Print it (orstr()it) to get the text to paste into a bug report; read the individual fields if you need one fact programmatically.- required_baseline¶
The instruction-set baseline the compiled backend was built to require. An empty string reports no fixed ISA requirement;
Nonemeans the backend did not report it.- Type:
str | None
- simd_features¶
Per-feature CPU SIMD support.
True/Falsewhere the feature is meaningful and could be probed,Nonewhere it is not applicable (any non-x86 machine).
- cuda_devices¶
The name of each visible CUDA device, in device order. Empty when CUDA is unavailable.
Stability: provisional (see Stability policy).
IGNORE_LOCATION¶
- deepwave.IGNORE_LOCATION¶
Sentinel value to place in a source or receiver location tensor to mark that point as unused, allowing a single rectangular
[shot, point, ndim]locations tensor to describe shots that have differing numbers of active sources or receivers.A point whose coordinates are all
IGNORE_LOCATIONis ignored: a source injects nothing, a receiver records zeros. A point with only some coordinates set to the sentinel raises, because that is far more likely to be a mistake than an intention.Its numeric value is unspecified and may change. Always compare against, and assign from,
deepwave.IGNORE_LOCATIONitself; never hard-code the number it happens to hold.The
*_maskarguments are the modern spelling of the same mechanism – a[shot, point]boolean tensor,True= active – and produce bit-identical results. Supplying both for one component raises.
Type aliases¶
- deepwave.SourceSpec¶
alias of
tuple[torch.Tensor, LocationsLike]|tuple[torch.Tensor, LocationsLike, Optional[torch.Tensor]]
- deepwave.ReceiverSpec¶
alias of
LocationsLike|tuple[LocationsLike, Optional[torch.Tensor]]
Introspection¶
The machine-readable description of what a propagator offers, for code that must drive any of the nine without hard-coding one of them. Each function accepts the functional (deepwave.elastic), the Module class (deepwave.Elastic) or the name ("elastic").
- deepwave.spec(propagator: Callable[[...], Any] | type | str) PropagatorSpec¶
What a propagator offers, as data.
The machine-readable description a generic wrapper needs in order to drive any of the nine propagators without hard-coding one of them: the channels it has, the options it implements, and the names of everything it returns. See
deepwave.PropagatorSpec.- Parameters:
propagator – The functional (
deepwave.elastic), the Module class (deepwave.Elastic) or the name ("elastic").- Returns:
The propagator’s frozen specification. Cached, so the same propagator always yields the same object.
- Raises:
deepwave.DeepwaveTypeError – If the argument does not name a propagator.
deepwave.DeepwaveValueError – If a string is not a propagator’s name.
Examples
>>> import deepwave >>> deepwave.spec(deepwave.elastic).source_channels(2) ('pressure_rate', 'y', 'x') >>> deepwave.spec("elastic").source_amplitudes_kwarg("pressure_rate") 'source_amplitudes_pressure_rate'
- class deepwave.PropagatorSpec(name: str, status: str, ndims: tuple[int, ...], supported_options: frozenset[str], synthetic_source_channels: frozenset[str], synthetic_receiver_channels: frozenset[str], has_bg_receivers: bool, has_forward_wavefield: bool)¶
What one propagator offers, as data.
Returned by
deepwave.spec(); not constructed directly. Every field is plain data and every method is a pure function of the number of spatial dimensions, so instances compare equal,reprusefully, and pickle.Stability: provisional (see Stability policy). Fields and methods may be added as more of a propagator’s interface becomes worth publishing, and what a field reports changes when the thing it describes changes. The fields and methods it has are not renamed or withdrawn without a release’s notice under
deepwave.DeepwaveFutureWarning.- supported_options¶
The shared options it implements (
"mpml_fraction","surface_method","q_freq","q_n_mechanisms","q_band"). Every propagator accepts every shared option at its default value; a non-default value of one that is not listed here raises.
- synthetic_source_channels¶
Source channels the propagator offers that have no backend slot of their own because they are expanded into other channels.
{"pressure_rate"}onacoustic_vti, empty everywhere else.
- synthetic_receiver_channels¶
Receiver channels the propagator offers that have no backend slot of their own because it computes them from other channels’ recordings.
{"p"}onacoustic_vti, whose pressure is-(sigma_v + (ndim - 1) sigma_h) / ndim, empty everywhere else. Such a channel still occupies its own output tuple slot.
- has_bg_receivers¶
Whether the propagator takes a
bg_receivers=mapping (equivalently, whether it is a Born propagator).- Type:
- has_forward_wavefield¶
Whether the propagator implements
backward_callback_needs_forward_wavefield=True, and so can servedeepwave.CallbackState.get_wavefield()’srole="forward"inside abackward_callback.Trueonscalaralone today. All nine accept the argument at its default ofFalse; passingTruewhere this isFalseraisesdeepwave.DeepwaveNotImplementedError.- Type:
- bg_receiver_channels(ndim: int) tuple[str, ...]¶
The background-receiver channels; empty on a non-Born propagator.
- bg_receiver_locations_kwarg(channel: str) str¶
The flat keyword carrying this channel’s background locations.
- channel_aliases() Mapping[str, str]¶
The permanent alternative spellings of this propagator’s channels.
Maps each accepted alias to the channel it names, on both the source and the receiver side. Empty on all nine propagators: every channel has exactly one spelling. Read-only; any entry would be permanent. A spelling that is going away is in
deprecated_source_channels()instead.
- deprecated_source_channels() Mapping[str, str]¶
The deprecated source channel spellings, and what replaces each.
{"p": "volume_rate"}on the acoustic family and{"p": "pressure_rate"}on the elastic family andacoustic_vti; empty on the two scalar propagators. The old spelling keeps working, warningdeepwave.DeepwaveFutureWarning, until it is removed. It is a source-side deprecation only:"p"is the canonical receiver channel name on every propagator that records a pressure.
- model_names(ndim: int) tuple[str, ...]¶
The propagator’s model tensors, in the order it takes them.
The user-facing models – the arguments the propagator’s public function takes, in signature order – not the internal prepared models the kernel consumes:
acousticreports("v", "rho"), the two tensors a caller passes, rather than the bulk modulus and per-component buoyancies built from them. A Born propagator’s perturbation models are included (("v", "rho", "dv", "drho")foracoustic_born), as is any optional model tensor the propagator accepts at thisndim(elastic_vti’sc66, which is a model in 3-D only).These are the names
deepwave.estimate_memory()’smodels_requiring_gradtakes, and the names a generic wrapper needs in order to assemble a call.- Parameters:
ndim – The number of spatial dimensions.
- Returns:
The model argument names.
- Raises:
deepwave.DeepwaveValueError – On an unsupported
ndim.
- output_names(ndim: int, **options: Any) tuple[str, ...]¶
Every field of the returned tuple, in order.
The state fields of
wavefield_names()followed by the receiver fields ofreceiver_names(); the same tuple asdeepwave.PropagatorOutput.names.
- receiver_channels(ndim: int) tuple[str, ...]¶
The receiver channels, in the order of the returned tuple.
- receiver_locations_kwarg(channel: str) str¶
The flat keyword carrying this channel’s receiver locations.
On a Born propagator this is the scattered receivers’ keyword, spelled
sc_receiver_locations*; the background ones arebg_receiver_locations_kwarg().
- receiver_names(ndim: int) tuple[str, ...]¶
The receiver-amplitude output fields, in return order.
On a Born propagator the background block comes first, then the scattered block, matching the returned tuple.
- receiver_output_name(channel: str) str¶
The output field holding this channel’s recorded data.
The inverse of
receiver_channels(): the name under whichchannel’s data appears in the returned tuple and indeepwave.PropagatorOutput.receivers.
- source_amplitudes_kwarg(channel: str) str¶
The flat keyword carrying this channel’s source amplitudes.
- wavefield_names(ndim: int, **options: Any) tuple[str, ...]¶
The state fields the propagator returns, in return order.
Exactly the keys of
deepwave.PropagatorOutput.final_state: the public wavefields plus"auxiliary_state", and no receiver fields. These are the namesinitial_state=consumes, so they are what a wrapper needs in order to continue a simulation.- Parameters:
ndim – The number of spatial dimensions.
**options – Options that change the list.
q_n_mechanismson viscoacoustic is the only one today; any other key raises rather than being ignored.
- Returns:
The state field names.
- Raises:
deepwave.DeepwaveValueError – On an unsupported
ndimor an option key this propagator’s names do not depend on.
- deepwave.wavefield_names(propagator: Callable[[...], Any] | type | str, ndim: int, **options: Any) tuple[str, ...]¶
The state fields a propagator returns, in return order.
Exactly the keys of
deepwave.PropagatorOutput.final_state– the public wavefields plus"auxiliary_state", and no receiver fields – so this is the list aninitial_state=consumer needs.- Parameters:
propagator – The functional, the Module class or the name.
ndim – The number of spatial dimensions.
**options – Options that change the list;
q_n_mechanismson the viscoacoustic propagator is the only one today, and any other key raises rather than being silently ignored.
- Returns:
The state field names.
- Raises:
deepwave.DeepwaveValueError – On an unsupported
ndim, or an option key this propagator’s names do not depend on.
Examples
>>> import deepwave >>> deepwave.wavefield_names(deepwave.scalar, 2) ('wavefield_0', 'wavefield_m1', 'auxiliary_state')
- deepwave.receiver_names(propagator: Callable[[...], Any] | type | str, ndim: int) tuple[str, ...]¶
The receiver-amplitude fields a propagator returns, in return order.
On a Born propagator the background block comes first and the scattered block second, matching the returned tuple.
- Parameters:
propagator – The functional, the Module class or the name.
ndim – The number of spatial dimensions.
- Returns:
The receiver output field names.
- Raises:
deepwave.DeepwaveValueError – If
ndimis one the propagator does not support.
Examples
>>> import deepwave >>> deepwave.receiver_names(deepwave.acoustic, 2) ('receiver_amplitudes_p', 'receiver_amplitudes_y', 'receiver_amplitudes_x')
vpvsrho_to_lambmubuoyancy Function¶
- deepwave.vpvsrho_to_lambmubuoyancy(vp: Tensor, vs: Tensor, rho: Tensor, eps: float = 1e-15) tuple[Tensor, Tensor, Tensor]¶
Converts vp, vs, rho to lambda, mu, buoyancy.
All input Tensors must have the same shape.
- Parameters:
vp – A torch.Tensor containing the p wavespeed.
vs – A torch.Tensor containing the s wavespeed.
rho – A torch.Tensor containing the density.
eps – An optional float to avoid division by zero. Default 1e-15.
- Returns:
lambda: A torch.Tensor containing the first Lamé parameter.
mu: A torch.Tensor containing the second Lamé parameter.
buoyancy: The buoyancy (1/density) model. A cell with
buoyancy = 0is a vacuum, not an infinitely dense cell; see the vacuum sentinel in Boundary conditions, which also explains how the sentinel is averaged onto the staggered nodes.rho = 0is mapped tobuoyancy = 0rather than to infinity, so a vacuum built this way carries the sentinel.
- Return type:
Tuple
lambmubuoyancy_to_vpvsrho Function¶
- deepwave.lambmubuoyancy_to_vpvsrho(lamb: Tensor, mu: Tensor, buoyancy: Tensor, eps: float = 1e-15) tuple[Tensor, Tensor, Tensor]¶
Converts lambda, mu, buoyancy to vp, vs, rho.
All input Tensors must have the same shape.
- Parameters:
lamb – A torch.Tensor containing the first Lamé parameter.
mu – A torch.Tensor containing the second Lamé parameter.
buoyancy – The buoyancy (1/density) model. A cell with
buoyancy = 0is a vacuum, not an infinitely dense cell; see the vacuum sentinel in Boundary conditions, which also explains how the sentinel is averaged onto the staggered nodes. Such a cell is mapped back torho = 0rather than to an infinite density.eps – An optional float to avoid division by zero. Default 1e-15.
- Returns:
vp: A torch.Tensor containing the p wavespeed.
vs: A torch.Tensor containing the s wavespeed.
rho: A torch.Tensor containing the density.
- Return type:
Tuple
to_cells Function¶
- deepwave.to_cells(coordinates: Tensor, grid_spacing: float | Sequence[float], grid_origin: float | Sequence[float] | None = None, *, snap: bool | float = False) Tensor¶
Converts physical-unit coordinates to float grid-cell coordinates.
Computes
(coordinates - grid_origin) / grid_spacingelementwise, withgrid_spacing/grid_originbroadcasting over the last (ndim) dimension ofcoordinates. The result is suitable as floatsource_locations/receiver_locationsfor the propagators, which auto-detect off-grid-node coordinates and Hicks-interpolate them (seedeepwave.Hicks); on-node results are used directly by the exact (integer) path.With the default
snap=Falsethis function does not round: an off-node result is returned as-is, and even a physically on-node coordinate may not come back as an exact integer due to floating-point error. Snapping to the nearest integer grid cell is the propagators’ documented behaviour, not this function’s; call.round().long()yourself first if you specifically want integer cell indices, or passsnap=Trueto clean up the floating-point error only (below).- Parameters:
coordinates – A
[..., ndim]float Tensor of physical-unit coordinates.grid_spacing – The grid cell spacing: a single float (applied to every dimension) or a sequence of
ndimfloats.grid_origin – The physical-unit coordinate of grid cell 0 in each dimension: a single float, a sequence of
ndimfloats, orNone(default), meaning zero in every dimension. Named for the grid, not for the propagators’origin, which is the cell index at which a model region begins.snap –
Whether to remove floating-point error from results that should have landed exactly on a node.
False(the default) returns the division’s result untouched, bit-for-bit.Truereplaces any result withindeepwave.ON_NODE_TOLERANCEcells of an integer by that integer. A float instead ofTrueuses that tolerance, in cells, in place ofON_NODE_TOLERANCE.This is a cleanup of floating-point error, not a rounding of genuinely off-node coordinates: a coordinate that is really 0.4 cells off a node is returned unchanged, whatever
snapis, and will still be Hicks-interpolated. Its purpose is the case where(coordinates - grid_origin) / grid_spacingis arithmetically an integer but comes back as6.999999999999999– which, without snapping, routes an exactly-on-node survey through the interpolated path and changes the numbers. Keyword-only, because the other three parameters are positional in released code.
- Returns:
A
[..., ndim]float Tensor of cell coordinates, the same shape, dtype and device as the unsnapped result. The dtype iscoordinates’ own if that is a floating type, and the default float dtype otherwise (an integercoordinatestensor is accepted: the conversion is done in floating point, so a fractionalgrid_spacing/originis not truncated).- Raises:
DeepwaveValueError – If
snapis a negative number.
Hicks Class¶
- class deepwave.Hicks(locations: Tensor, halfwidth: int = 4, *, free_surfaces: Sequence[bool] | None = None, free_surface_locs: Sequence[float] | None = None, model_shape: Sequence[int] | None = None, monopole: Tensor | bool = True, dipole_dim: Tensor | int = 0, dtype: dtype | None = None, eps: float = 1e-05, offset: Sequence[float] | None = None, rigid_surfaces: Sequence[bool] | None = None, differentiable: bool = False)¶
Location interpolation onto grid using method of Hicks.
Hicks (2002, https://doi.org/10.1190/1.1451454) proposed using a Kaiser windowed sinc function to interpolate source and receiver locations onto a grid when they are not centred on a grid point. This class implements this approach. It can also be used to create dipole sources and receivers.
The interpolator supports 1, 2, and 3 spatial dimensions (matching the Deepwave propagator being used) and is staggered-grid aware via the
offsetargument, so a field component that does not live on the regular-grid (pressure) node can still be interpolated about its true location.- Parameters:
locations – A three dimensional torch.Tensor [shot, per_shot, ndim] specifying the locations to interpolate, provided as float values in units of cells relative to the origin of the grid, so [[[0.5, 0.5]]] corresponds to the point half a cell from the origin in both dimensions.
ndimmay be 1, 2, or 3.halfwidth – An integer specifying the halfwidth of the window that will be used to interpolate each point onto the grid. A halfwidth of 4 (the default) means that an 8x8 window will be used. Possible values are in [1, 10].
free_surfaces –
A sequence of
2 * ndimbooleans specifying whether the corresponding edge of the grid is a free surface, in the order: [beginning of first dimension, end of first dimension, beginning of second dimension, end of second dimension, …]. For example, in 2D [True, False, False, True] means that the beginning of the first dimension and end of the second dimension are free surfaces, while the other two edges are not. Defaults to no edges being free surfaces.A free surface is a point at which the field is zero (an odd/antisymmetric mirror, also called a Dirichlet or pressure-release boundary). Part of the interpolation window that extends past the surface is folded back into the model with a sign flip (the method of images).
Note
Matching a Deepwave boundary. Which condition a face with
pml_width = 0produces, and where its mirror plane sits, depends on the propagator and the field component; the measured table is at Reflecting edges, and is the single source for it. WhatHicksneeds from that table is only the surface location, which is whatfree_surface_locsdefaults to: one cell outside the model edge for a free surface, half a cell outside for a rigid one. Choose the surface type and location to match the propagator and field component you are placing.scalarandscalar_bornneed noHicksof their own for this: their source and receiver points are on the regular grid and theirboundary="free"/"rigid"faces mirror about exactly these default planes, so a plain float location next to such a face is folded automatically, giving bit for bit what building this class with those surfaces gives. The other seven propagators carry their components on staggered nodes, where the image is component dependent, and refuse a float location whose window would cross such a face.free_surface_locs – An optional sequence of
2 * ndimfloats giving the location (in cells) of each face named infree_surfaces/rigid_surfaces, in the same order. Defaults to the location Deepwave’s own boundaries use: one cell outside the model edge (-1/model_shape) for a free surface, half a cell outside (-0.5/model_shape - 0.5) for a rigid surface.model_shape – A sequence of
ndimintegers specifying the size of the grid. This is only used when the model contains free or rigid surfaces.monopole – A boolean or torch.Tensor of booleans (of shape [shot, per_shot]) specifying whether the source/receiver is a monopole. If False, the point will be a dipole. Default True.
dipole_dim – An integer or torch.Tensor of integers (of shape [shot, per_shot]) in
[0, ndim)specifying the dimension in which the dipole is oriented. This is only used for points that are dipoles (not monopoles). Default 0.dtype – The datatype to use for the interpolation weights.
None(the default) meanstorch.get_default_dtype(), matching the otherdtypeparameters in the package. Under the usual float32 default this is the dtype the weights were always built in, so a propagator’s results are bit-identical. Under a float64 default the weights are built in float64 and then promoted (or demoted) to the propagator’s own dtype when they are used, which is a different rounding path from building them in float32: measured differences of order 1e-6 relative in the weights themselves and in receiver amplitudes from a float32 model, and of order 5e-8 relative from a float64 model. Passdtype=torch.floatto build the weights in float32 regardless of the default.eps – A small value to prevent division by zero. Points closer to a grid cell centre than this will be rounded to the grid cell. Default 1e-5.
offset –
An optional sequence of
ndimfloats giving the half-cell offset of the field component being interpolated relative to the regular-grid (pressure) node, used to make interpolation staggered-grid aware. Defaults to zero offset in every dimension (regular grid).Note
Staggered surfaces. The default surface locations and the
offsetshift applied to them are validated for the regular grid (zerooffset), which covers thescalarfree surface and theacousticpressure (rigid) surface. For a staggered component (non-zerooffset) the image symmetry is component dependent (Robertsson, 1996): different components reflect with different signs and about different planes. Out-of-model cells are always dropped so the result is safe to use, but to be physically exact for a staggered component you should pass the correctfree_surfaces/rigid_surfacesand explicitfree_surface_locsfor that component.rigid_surfaces –
A sequence of
2 * ndimbooleans specifying whether the corresponding edge of the grid is a rigid surface, in the same order asfree_surfaces. Defaults to no edges being rigid surfaces. A face must not be both a free and a rigid surface.A rigid surface is a point at which the field has zero normal gradient (an even/symmetric mirror, also called a Neumann or zero-normal-velocity boundary). The part of the interpolation window past the surface is folded back into the model with the same sign (no flip); a point exactly on the surface has its weight doubled.
differentiable –
If True, the interpolation weights are built as differentiable torch expressions of
locations(with no float-keyed weight cache), so gradients flow fromsource()/receiver()back to the source/receiver positions. This enables gradient-based survey design and source localisation. Default False (the fast, cached, bit-for-bit-stable path).Stability: provisional (see Stability policy).
Weights are differentiable within a cell. At integer crossings the support changes: monopole values remain continuous, but their position gradients can jump; dipole values can jump too. The support indices are detached from autograd.
float64is recommended for gradient work.IGNORE_LOCATIONsentinel points are handled as usual (their gradient is zero). Free and rigid surfaces are not supported in this mode (aNotImplementedErroris raised); dipole sources/receivers are supported. See the “Differentiable positions” section of the usage documentation for the recommended recipe.Note
A
differentiable=Trueinstance holds its interpolation weights in the autograd graph (they are torch expressions oflocations). Callingbackward()frees that graph, so a second backward through the same instance raises the usual “backward through the graph a second time” error. In an optimisation loop either construct a freshHicksinstance each iteration (the normal pattern: build it from the current positions inside the loop), or passretain_graph=Truetobackward()if you deliberately reuse one instance across steps.
Initialises the Hicks interpolation class.
See the class docstring for a description of the arguments.
- get_locations(shot_idxs: Tensor | None = None) Tensor¶
Get the interpolated locations.
The interpolated locations can be provided to a Deepwave propagator as the source or receiver locations.
- Parameters:
shot_idxs – A 1D torch.Tensor containing the indices of the shots that amplitudes are being provided for, within the locations provided at initialisation. This is useful if shots are being used in batches, so only some of the shots are needed now. Default (None) means all shots.
- Returns:
A torch.Tensor containing the interpolated locations.
- receiver(amplitudes: Tensor, shot_idxs: Tensor | None = None) Tensor¶
Convert receiver amplitudes from interpolated to original locations.
- Parameters:
amplitudes – A torch.Tensor containing the amplitudes recorded at interpolated receiver locations.
shot_idxs – A 1D torch.Tensor containing the indices of the shots that amplitudes are being provided for, within the locations provided at initialisation. This is useful if shots are being used in batches, so only some of the shots are needed now. Default (None) means all shots.
- Returns:
The amplitudes of receivers at the original locations.
- source(amplitudes: Tensor, shot_idxs: Tensor | None = None) Tensor¶
Calculate the amplitudes of the interpolated sources.
- Parameters:
amplitudes – A torch.Tensor containing the amplitudes of the sources at the original locations.
shot_idxs – A 1D torch.Tensor containing the indices of the shots that amplitudes are being provided for, within the locations provided at initialisation. This is useful if shots are being used in batches, so only some of the shots are needed now. Default (None) means all shots.
- Returns:
The amplitudes of the interpolated sources, ready to be provided to a Deepwave propagator.
ricker Function¶
- deepwave.wavelets.ricker(freq: float, nt: int = <required>, dt: float = <required>, peak_time: float | None = None, dtype: dtype | None = None, *, length: int | None = None, shape: int | Sequence[int] | None = None) Tensor¶
Return a Ricker wavelet with the specified central frequency.
- Parameters:
freq – The central frequency (Hz, if
dtis in seconds). Must be positive.nt – The number of time samples. Required: omitting it (and its deprecated
lengthalias), or passingNone, raisesTypeError.dt – The time sample spacing (seconds). Required: omitting it, or passing
None, raisesTypeError.peak_time – The time (in secs) of the peak amplitude. Optional; if not provided the wavelet is auto-centred in the record (
peak_time = nt * dt / 2), the same record-centring convention astone_burst(),gaussian_pulse()andormsby()(which also default topeak_time = nt * dt / 2), so the (symmetric) wavelet is not truncated. Passing an explicit value is unchanged.dtype – The PyTorch datatype to use. Optional, defaults to PyTorch’s default (float32).
length – Deprecated alias for
nt, retained for backward compatibility. Removed in Deepwave 0.4.0. Providing both raisesValueError. Keyword-only, so a positional call such asricker(freq, 100, dt)is unaffected by this deprecation.shape – Keyword-only. The leading dimensions of the returned tensor, with
ntalways last.None(the default) returns a one-dimensional[nt]tensor.(n_shots, n_sources)returns[n_shots, n_sources, nt], ready to pass straight to a propagator’ssource_amplitudes. Anintnis shorthand for(n,)and returns[n, nt]; an empty sequence()returns[nt], asNonedoes. Every entry must be a positive integer. The leading dimensions are added withtorch.Tensor.expand(), so no data is copied and the result is a view: every shot and source shares one copy of the wavelet, and each added dimension has stride zero. Call.contiguous()or.clone()on it before modifying it in place. Deepwave’s propagators do not modify their inputs, so passing the view to one directly is safe.
- Returns:
A PyTorch tensor representing the Ricker wavelet, of shape
[nt], or[*shape, nt]ifshapewas given.- Raises:
TypeError – If
nt(or its deprecatedlengthalias) ordtis not provided, or is provided asNone, or ifshapeis notNone, an int, or a sequence of ints.ValueError – If
freqis not positive, ifdtis zero, if bothntandlengthare provided, or if any entry ofshapeis not positive.
tone_burst Function¶
- deepwave.wavelets.tone_burst(freq: float, n_cycles: float, *, nt: int, dt: float, peak_time: float | None = None, dtype: dtype | None = None, window: str = 'hann', shape: int | Sequence[int] | None = None) Tensor¶
Return a windowed sinusoid (“tone burst”).
A tone burst is a sinusoid of frequency
freqmultiplied by a smooth envelope of finite duration. It is the standard excitation in ultrasound and non-destructive testing, where the number of cyclesn_cyclessets the trade-off between temporal resolution (few cycles, broad band) and spectral purity (many cycles, narrow band). The envelope duration isn_cycles / freqseconds, centred onpeak_time.Concretely, with \(T = n_{cycles} / f\) the burst duration and \(\tau = t - t_{peak}\), the wavelet is
\[s(t) = w(\tau)\,\sin(2\pi f\,\tau), \qquad |\tau| \le T/2,\]and zero outside that window, where \(w\) is the chosen taper (a Hann window \(w(\tau) = \tfrac12\,[1 + \cos(2\pi\tau/T)]\) by default).
The record must be long enough to hold the burst. With the default, record-centred
peak_timethat meansnt * dtof at leastn_cycles / freq + 2 * dt. A shorter record cuts the burst off part-way through its oscillation, so the wavelet steps from zero to a non-zero value in a single sample; that step is broadband, which is exactly what choosing afreqand ann_cycleswas meant to avoid, and it would otherwise pass unnoticed because nothing about it is an error. Deepwave therefore emits aUserWarningnaming the end that was cut, the burst duration, the record length and anntthat would fit. Lengthen the record or shorten the burst; if the truncation is deliberate, filter the warning.- Parameters:
freq – The centre frequency of the sinusoid (Hz, if
dtis in seconds).n_cycles – The number of cycles of the sinusoid spanned by the window (may be fractional). The window duration is
n_cycles / freq, in the same time units asdt.nt – The number of time samples in the returned wavelet.
dt – The time sample spacing (seconds).
peak_time – The time (seconds) of the centre of the window. Optional; if not provided the burst is centred in the record (
peak_time = nt * dt / 2), the same record-centring convention asricker(),gaussian_pulse()andormsby(), so that swapping one wavelet for another does not move the source in time. Deepwave previously defaulted instead topeak_time = n_cycles / freq / 2, which centred the burst window rather than the record, placing the start of the burst att = 0; passpeak_time=n_cycles / freq / 2explicitly to reproduce that waveform.dtype – The PyTorch datatype to use. Optional, defaults to PyTorch’s default (float32).
window – Keyword-only. The envelope taper; see
deepwave.transducers.window_weights()for the recognised names and their aliases (default"hann").shape – Keyword-only. The leading dimensions of the returned tensor, with
ntalways last.None(the default) returns a one-dimensional[nt]tensor.(n_shots, n_sources)returns[n_shots, n_sources, nt], ready to pass straight to a propagator’ssource_amplitudes. Anintnis shorthand for(n,)and returns[n, nt]; an empty sequence()returns[nt], asNonedoes. Every entry must be a positive integer. The leading dimensions are added withtorch.Tensor.expand(), so no data is copied and the result is a view: every shot and source shares one copy of the wavelet, and each added dimension has stride zero. Call.contiguous()or.clone()on it before modifying it in place. Deepwave’s propagators do not modify their inputs, so passing the view to one directly is safe.
- Returns:
A PyTorch tensor of shape
[nt]containing the tone burst, or of shape[*shape, nt]ifshapewas given.- Raises:
TypeError – If
shapeis notNone, an int, or a sequence of ints.ValueError – If
dtis zero, iffreqorn_cyclesis not positive, ifwindowis not a recognised name, or if any entry ofshapeis not positive.
gaussian_pulse Function¶
- deepwave.wavelets.gaussian_pulse(freq: float, bandwidth_frac: float, *, nt: int, dt: float, peak_time: float | None = None, dtype: dtype | None = None, shape: int | Sequence[int] | None = None) Tensor¶
Return a Gaussian-modulated sinusoid with a prescribed bandwidth.
This is a cosine of frequency
freqmultiplied by a Gaussian envelope, the excitation used to model broadband ultrasound/NDT transducers. The envelope width is set by the fractional -6 dB bandwidthbandwidth_fracfollowing the convention ofscipy.signal.gausspulse(F. Harris, “On the use of windows…”, Proc. IEEE 66(1), 1978, for the windowing background): the magnitude spectrum is Gaussian and falls to \(-6\,\text{dB}\) (a factor \(10^{-6/20} = 1/2\)) of its peak at the frequencies \(f_c(1 \pm b/2)\), where \(f_c\) isfreqand \(b\) isbandwidth_frac. Equivalently the wavelet is\[s(t) = e^{-a\,\tau^2}\,\cos(2\pi f_c\,\tau), \qquad a = \frac{(\pi f_c b)^2}{4\ln 2}, \qquad \tau = t - t_{peak},\]since a Gaussian time envelope \(e^{-a\tau^2}\) has the Gaussian spectrum \(\propto e^{-\pi^2 f^2 / a}\), whose half-amplitude (\(-6\,\text{dB}\)) half-width is \(\Delta f = f_c b / 2\).
- Parameters:
freq – The centre frequency of the sinusoid (Hz, if
dtis in seconds).bandwidth_frac – The fractional -6 dB bandwidth \(b = \Delta f_{-6dB} / f_c\) (e.g.
0.5for a 50%-bandwidth transducer). Must be positive.nt – The number of time samples in the returned wavelet.
dt – The time sample spacing (seconds).
peak_time – The time (seconds) of the envelope peak. Optional; if not provided the pulse is centred in the record (
peak_time = nt*dt/2), the same record-centring convention asricker(),tone_burst()andormsby().dtype – The PyTorch datatype to use. Optional, defaults to PyTorch’s default (float32).
shape – Keyword-only. The leading dimensions of the returned tensor, with
ntalways last.None(the default) returns a one-dimensional[nt]tensor.(n_shots, n_sources)returns[n_shots, n_sources, nt], ready to pass straight to a propagator’ssource_amplitudes. Anintnis shorthand for(n,)and returns[n, nt]; an empty sequence()returns[nt], asNonedoes. Every entry must be a positive integer. The leading dimensions are added withtorch.Tensor.expand(), so no data is copied and the result is a view: every shot and source shares one copy of the wavelet, and each added dimension has stride zero. Call.contiguous()or.clone()on it before modifying it in place. Deepwave’s propagators do not modify their inputs, so passing the view to one directly is safe.
- Returns:
A PyTorch tensor of shape
[nt]containing the Gaussian pulse, or of shape[*shape, nt]ifshapewas given.- Raises:
TypeError – If
shapeis notNone, an int, or a sequence of ints.ValueError – If
dtis zero, iffreqorbandwidth_fracis not positive, or if any entry ofshapeis not positive.
ormsby Function¶
- deepwave.wavelets.ormsby(f1: float, f2: float, f3: float, f4: float, *, nt: int, dt: float, peak_time: float | None = None, dtype: dtype | None = None, shape: int | Sequence[int] | None = None) Tensor¶
Return an Ormsby wavelet with the specified four corner frequencies.
The Ormsby wavelet is the standard seismic wavelet whose amplitude spectrum is an exact trapezoid: zero below
f1, ramping linearly up to full amplitude atf2, flat (unity) betweenf2andf3, then ramping linearly back down to zero atf4and beyond. This gives more direct control over the excited passband thanricker()(a single central frequency) orgaussian_pulse()(a fractional bandwidth around one centre frequency).With \(\tau = t - t_{peak}\), the wavelet is
\[w(\tau) \propto \frac{\pi f_4^2\,\mathrm{sinc}^2(\pi f_4 \tau) - \pi f_3^2\,\mathrm{sinc}^2(\pi f_3 \tau)}{f_4 - f_3} - \frac{\pi f_2^2\,\mathrm{sinc}^2(\pi f_2 \tau) - \pi f_1^2\,\mathrm{sinc}^2(\pi f_1 \tau)}{f_2 - f_1},\]where \(\mathrm{sinc}(x) = \sin(x)/x\) (the mathematical, not normalised, sinc), normalised so the returned wavelet’s peak absolute amplitude is exactly
1.0. (Note:torch.sinc(x)computes the normalised sinc, \(\sin(\pi x)/(\pi x)\); since \(\mathrm{sinc}(\pi f \tau) = \sin(\pi f \tau) / (\pi f \tau) = \mathrm{torch.sinc}(f\tau)\), the mathematical sinc above is computed astorch.sinc(f * tau)directly, with no extra factor of \(\pi\).)- Parameters:
f1 – The low-cut corner frequency (amplitude zero below this). All four corner frequencies are in Hz, if
dtis in seconds, and must satisfy0 < f1 < f2 < f3 < f4.f2 – The low-pass corner frequency (amplitude reaches full scale here).
f3 – The high-pass corner frequency (amplitude starts rolling off here).
f4 – The high-cut corner frequency (amplitude zero above this).
nt – The number of time samples.
dt – The time sample spacing (seconds).
peak_time – The time (in secs) of the peak amplitude. Optional; if not provided the wavelet is auto-centred in the record (
peak_time = nt * dt / 2), the same record-centring convention asricker(),tone_burst()andgaussian_pulse(), so the (symmetric) wavelet is not truncated. Passing an explicit value is unchanged.dtype – The PyTorch datatype to use. Optional, defaults to PyTorch’s default (float32).
shape – Keyword-only. The leading dimensions of the returned tensor, with
ntalways last.None(the default) returns a one-dimensional[nt]tensor.(n_shots, n_sources)returns[n_shots, n_sources, nt], ready to pass straight to a propagator’ssource_amplitudes. Anintnis shorthand for(n,)and returns[n, nt]; an empty sequence()returns[nt], asNonedoes. Every entry must be a positive integer. The leading dimensions are added withtorch.Tensor.expand(), so no data is copied and the result is a view: every shot and source shares one copy of the wavelet, and each added dimension has stride zero. Call.contiguous()or.clone()on it before modifying it in place. Deepwave’s propagators do not modify their inputs, so passing the view to one directly is safe.
- Returns:
A PyTorch tensor of shape
[nt]containing the Ormsby wavelet, or of shape[*shape, nt]ifshapewas given.- Raises:
TypeError – If
shapeis notNone, an int, or a sequence of ints.ValueError – If
dtis zero, if the corner frequencies do not satisfy0 < f1 < f2 < f3 < f4, or if any entry ofshapeis not positive.
Transducer array helpers¶
Stability: provisional (see Stability policy) – this module is expected to gain element types, functions and constructor keywords. The unit convention has settled: positions in grid cells, speeds in cells per unit time, delays and dt in one time unit of your choosing. What it has today is not respelled or withdrawn without a release’s notice under deepwave.DeepwaveFutureWarning.
The deepwave.transducers module builds extended transducer elements (lines, arcs, discs, focused bowls) as tessellated point sets with apodisation weights and focusing delay laws, for ultrasound and NDT modelling. See Ultrasound and NDT (coming from k-Wave) for an overview and worked usage.
- deepwave.transducers.line_element(centre: Sequence[float] | Tensor, direction: Sequence[float] | Tensor, length: float, spacing: float = 1.0, window: str = 'rect', dtype: dtype | None = None) tuple[Tensor, Tensor]¶
Tessellate a straight line-segment aperture (2-D or 3-D).
The element is a line segment of the given
length(in grid cells), centred oncentreand oriented alongdirection. It is sampled bynequally spaced points whose spacing is<= spacingcells; the endpoints are symmetric aboutcentre.- Parameters:
centre – The centre of the segment, shape
[ndim](grid cells).direction – The orientation vector, shape
[ndim](need not be normalised).length – The total length of the segment (grid cells); must be non-negative (a length of 0 gives a single point at
centre).spacing – The maximum spacing between adjacent points (grid cells). Optional, default 1 cell.
window – Window (apodisation) type passed to
window_weights(). Optional, default"rect"(uniform) – note that this differs fromwindow_weights()’s own"hann"default, because an element should not be apodised unless you ask for it.dtype – The PyTorch datatype. Optional, defaults to PyTorch’s default.
- Returns:
(locations, weights)wherelocationshas shape[n, ndim](grid cells) andweightshas shape[n]. Prefer passing both toelement_source(), which applies the weights (and any delay law) and returns the(source_amplitudes, source_locations)pair a propagator takes; the raw pair is the low-level path, and forgetting to applyweightssilently changes the beam pattern.
- deepwave.transducers.arc_element(centre_of_curvature: Sequence[float] | Tensor, radius: float, angles: Sequence[float], spacing: float = 1.0, window: str = 'rect', dtype: dtype | None = None) tuple[Tensor, Tensor]¶
Tessellate a circular-arc aperture (2-D only).
Models a curved (focused) 2-D element: points lie on a circle of the given
radiusaboutcentre_of_curvature, spanning the angular rangeangles = (theta_start, theta_end)in radians. The arc’s centre of curvature is not a point on the arc. The point at angle \(\theta\) is atcentre_of_curvature + radius * [cos(theta), sin(theta)], i.e.thetais measured from the first (slow) grid dimension towards the second. Adjacent points are spaced<= spacingcells along the arc.A curved array focused at a point is obtained by placing an arc of the desired radius about that focal point.
For a full circle (an angular span that is an integer multiple of \(2\pi\)) the final sample would coincide with the first; the duplicated seam point is dropped so every returned cell is distinct.
- Parameters:
centre_of_curvature – The centre of the circle the arc lies on, shape
[2](grid cells). This is the arc’s geometric focus, not a point on the arc itself.radius – The arc radius (grid cells).
angles –
(theta_start, theta_end)in radians.spacing – Maximum arc spacing between adjacent points (grid cells).
window – Window (apodisation) type passed to
window_weights(). Optional, default"rect"(uniform) – note that this differs fromwindow_weights()’s own"hann"default, because an element should not be apodised unless you ask for it.dtype – The PyTorch datatype. Optional.
- Returns:
(locations, weights)withlocationsof shape[n, 2]. Prefer passing both toelement_source(), which applies the weights (and any delay law) and returns the(source_amplitudes, source_locations)pair a propagator takes; the raw pair is the low-level path, and forgetting to applyweightssilently changes the beam pattern.
- deepwave.transducers.disc_element(centre: Sequence[float] | Tensor, radius: float, normal: Sequence[float] | Tensor, spacing: float = 1.0, window: str = 'rect', dtype: dtype | None = None) tuple[Tensor, Tensor]¶
Tessellate a flat circular-disc aperture (3-D).
Points fill a flat disc of the given
radiuscentred oncentreand lying in the plane whose normal isnormal(an unfocused, planar circular element). The disc is tessellated by concentric rings spaced<= spacingcells, with points spaced<= spacingalong each ring, giving an approximately uniform areal density.- Parameters:
centre – The disc centre, shape
[3](grid cells).radius – The disc radius (grid cells).
normal – The disc-plane normal, shape
[3](need not be normalised).spacing – Target point spacing (grid cells).
window – Window (apodisation) applied as a function of the radial fraction (0 at centre, 1 at the rim); see
window_weights()for the available types. Optional, default"rect"(uniform) – note that this differs fromwindow_weights()’s own"hann"default, because an element should not be apodised unless you ask for it.dtype – The PyTorch datatype. Optional.
- Returns:
(locations, weights)withlocationsof shape[n, 3]. Prefer passing both toelement_source(), which applies the weights (and any delay law) and returns the(source_amplitudes, source_locations)pair a propagator takes; the raw pair is the low-level path, and forgetting to applyweightssilently changes the beam pattern.
- deepwave.transducers.bowl_element(apex: Sequence[float] | Tensor, radius: float, diameter: float, focus: Sequence[float] | Tensor, spacing: float = 1.0, window: str = 'rect', dtype: dtype | None = None) tuple[Tensor, Tensor]¶
Tessellate an axisymmetric focused-bowl aperture (3-D).
Models a spherically focused single-element transducer (a spherical cap), the workhorse geometry of therapeutic and photoacoustic ultrasound. The bowl is the cap of a sphere of radius-of-curvature
radiuswhose centre of curvature is atfocus;apexis the pole of the cap on the sphere, and the axis points fromapextowardfocus. The cap extends out to an aperture of the givendiameter. The surface is tessellated by concentric rings of constant polar angle, spaced<= spacingcells along the surface.This mirrors k-Wave’s
makeBowlparameterisation (apex position, radius of curvature, aperture diameter, focus direction).- Parameters:
apex – The bowl apex/pole, shape
[3](grid cells). This is a point on the bowl surface; the bowl’s centre of curvature is the separatefocusargument.radius – The radius of curvature (grid cells).
diameter – The aperture diameter (grid cells); must be
<= 2*radius.focus – The centre of curvature / geometric focus, shape
[3](grid cells). Its distance fromapexshould beradius; only its direction fromapexis used to define the axis.spacing – Target surface point spacing (grid cells).
window – Window (apodisation) as a function of polar-angle fraction (0 at apex, 1 at rim); see
window_weights()for the available types. Optional, default"rect"(uniform) – note that this differs fromwindow_weights()’s own"hann"default, because an element should not be apodised unless you ask for it.dtype – The PyTorch datatype. Optional.
- Returns:
(locations, weights)withlocationsof shape[n, 3]. Prefer passing both toelement_source(), which applies the weights (and any delay law) and returns the(source_amplitudes, source_locations)pair a propagator takes; the raw pair is the low-level path, and forgetting to applyweightssilently changes the beam pattern.
- deepwave.transducers.window_weights(n: int, kind: str = 'hann', dtype: dtype | None = None, device: device | None = None) Tensor¶
Return per-point window (apodisation) weights for an
n-point element.A window/taper shapes the amplitude across an aperture, most commonly to reduce beam side lobes. The returned window is not normalised (its peak is 1 for the tapered windows, and every value is 1 for
"rect"); normalise it yourself if you need the element’s total amplitude to be independent ofkind.kinddefaults to"hann"here, while the element builders’windowparameter defaults to"rect". The difference is deliberate: an element should not be apodised unless you ask for it, whereas calling this function at all is asking for a window.- Parameters:
n – The number of points in the element.
kind – The window type:
"rect"(a.k.a."uniform","boxcar"or"none"; all ones),"hann"(a.k.a."hanning", default),"hamming", or"blackman".dtype – The PyTorch datatype. Optional, defaults to PyTorch’s default.
device – The PyTorch device. Optional, defaults to the CPU.
- Returns:
A tensor of shape
[n]containing the weights.
- deepwave.transducers.delays_to_focus(locations: Tensor, focus: Sequence[float] | Tensor, c: float) Tensor¶
Per-point transmit delays to focus/steer a beam at
focus.For each element point, returns the time delay to apply on transmit so that all points’ contributions arrive in phase at the focal point. Points farther from the focus fire earlier; the delays are non-negative and the point nearest the focus has the largest delay. Placing the focus far away in a chosen direction approximates plane-wave steering.
This helper assumes isotropic physical cell spacing. Convert physical speed to cells per unit time with the single common grid spacing; for an anisotropic grid, compute delays from physical coordinates separately.
All quantities are in your units. Positions are in grid cells,
cis a speed in cells per unit time, and the returned delays are in that same unit of time – the one to use fordt. If your model is in metres and seconds,c_cells = c_physical / grid_spacing, and the delays come back in seconds.- Parameters:
locations – Element point locations, shape
[n, ndim](grid cells).focus – The focal point, shape
[ndim](grid cells).c – The wave speed between the element and the focus, in grid cells per unit time.
- Returns:
A tensor of shape
[n]of non-negative delays, in the time unit implied byc.
- deepwave.transducers.apply_delays(wavelet: Tensor, delays: Tensor, dt: float, nt: int | None = None) Tensor¶
Build per-point amplitudes by fractional-delay-shifting a base wavelet.
Each element point emits the same base
waveletshifted in time by its delay. Shifting is done by a spectral (FFT) phase ramp, which applies an arbitrary fractional (sub-sample) delay exactly for a band-limited signal – the same spectral philosophy Deepwave uses for source/receiver time resampling. A positive delay shifts the wavelet later in time.The operation is differentiable with respect to both
waveletanddelays(the phase ramp is a smooth function of the delays), so delay laws can be optimised end-to-end.delaysanddtare in the same unit of time, whichever unit that is – the onedelays_to_focus()returns for yourc. Nothing here needs to know what a second is.Note
The FFT shift is circular: energy shifted past the end of the record wraps to the beginning. Make
ntlong enough (and pad the base wavelet) that the delayed wavelet stays within the record.- Parameters:
wavelet – The base wavelet, shape
[nt0].delays – Per-point delays, shape
[n], in the same time unit asdt. May be negative.dt – The time sample spacing.
nt – The output number of time samples. Optional; if given and different from
len(wavelet)the wavelet is zero-padded or truncated tontbefore shifting.
- Returns:
A tensor of shape
[n, nt]of per-point amplitudes.
- deepwave.transducers.element_source(wavelet: Tensor, locations: Tensor, weights: Tensor, *, delays: Tensor | None = None, dt: float | None = None, nt: int | None = None, n_shots: int | None = None) tuple[Tensor, Tensor]¶
Assemble an element into the source pair a propagator takes.
Takes the
(locations, weights)an element builder returns, a basewavelet, and an optional delay law, and returns(source_amplitudes, source_locations)of shapes[n_shots, n, nt]and[n_shots, n, ndim], ready to pass straight to a propagator. This is the recommended way to drive an element: it applies the apodisationweights, which are easy to forget and whose omission silently changes the beam pattern.Every step is differentiable, with respect to
wavelet,weights,locationsanddelaysalike, so a delay law or an apodisation can itself be optimised.delaysanddtare in the same unit of time, whichever unit that is – the onedelays_to_focus()returns for acin cells per unit time. Nothing here needs to know what a second is.The shot dimension is added with
torch.Tensor.expand(), so a single-shot wavelet costs no extra memory when broadcast to many shots.- Parameters:
wavelet – The base wavelet, shape
[nt0](the same wavelet in every shot) or[n_shots, nt0](one per shot).locations – Element point locations, shape
[n, ndim](grid cells), from an element builder.weights – Per-point apodisation weights, shape
[n], from the same builder.delays – Per-point transmit delays, shape
[n], e.g. fromdelays_to_focus(). Optional; if given,dtis required.dt – The time sample spacing, in the same unit as
delays. Required if and only ifdelaysis given.nt – The output number of time samples. Optional; if given and different from
wavelet.shape[-1]the wavelet is zero-padded or truncated tont, exactly as inapply_delays().n_shots – The number of shots. Optional; defaults to 1 for a 1-D wavelet and to
wavelet.shape[0]for a 2-D one. Giving a value that disagrees with a 2-D wavelet’s leading dimension raises rather than broadcasting.
- Returns:
(source_amplitudes, source_locations)of shapes[n_shots, n, nt]and[n_shots, n, ndim].- Raises:
DeepwaveValueError – If
delaysis given withoutdt; ifweightsandlocationsdescribe different numbers of points; ifwaveletis neither 1-D nor 2-D; ifdelaysis not 1-D of lengthn; or ifn_shotsdisagrees with a 2-D wavelet’s leading dimension.
- deepwave.transducers.element_receiver_trace(receiver_amplitudes: Tensor, weights: Tensor) Tensor¶
Collapse an element’s per-point receiver traces into one trace.
Returns the discrete (apodisation-)weighted sum of traces at the element’s tessellation points. Builder weights do not include area quadrature or normalisation, so the result and its units depend on point density.
- Parameters:
receiver_amplitudes – Recorded traces with the element’s points on the second-to-last axis, shape
[..., n, nt](e.g. the propagator’s[shot, receiver, time]output withreceiver == n).weights – Per-point weights, shape
[n](e.g. from an element builder orwindow_weights()).
- Returns:
The weighted-sum element trace, shape
[..., nt].
Misfit functions¶
The deepwave.misfits module provides three differentiable FWI data-misfit (loss) functions: deepwave.misfits.l2() (the conventional sum of squared differences), and two that are less prone to cycle-skipping than it, deepwave.misfits.envelope() (mean squared error between Hilbert envelopes) and deepwave.misfits.normalised_cross_correlation() (one minus the per-trace normalised cross-correlation, which is invariant to amplitude scaling). All three share one shape contract – the time axis is reduced away to one value per trace, and reduction ("mean", "sum" or "none") then says what to do with that per-trace tensor – so swapping one for another really is a one-line change. They do not share a scale, though: deepwave.misfits.l2() sums the squared differences over time while deepwave.misfits.envelope() averages them, and deepwave.misfits.normalised_cross_correlation() is bounded in [0, 2] per trace whatever the amplitudes, so expect to retune the step length after a swap. See Graph Space Optimal Transport for when and why to swap the misfit function during an inversion.
Two of these functions are commonly reached for as a replacement for torch.nn.functional.mse_loss(), and the correspondence is exact in one reduction only. l2(pred, target, reduction="sum") equals mse_loss(pred, target, reduction="sum"), but l2(pred, target, reduction="mean") is nt times mse_loss(pred, target, reduction="mean") – deepwave.misfits.l2() sums over time and averages over traces, whereas mse_loss averages over every element. Nothing raises, so a swap made on the default reduction scales the loss and its gradient by nt (often 1e3 to 1e4); divide the learning rate by nt, or use reduction="sum" on both sides.
- deepwave.misfits.l2(pred: Tensor, target: Tensor, *, reduction: Literal['mean', 'sum', 'none'] = 'mean') Tensor¶
Squared-error (least-squares) misfit between
predandtarget.The conventional FWI data misfit. Per trace, with \(p[n]\) =
pred, \(t[n]\) =target, and \(n = 0, \dots, n_t - 1\),\[L = \sum_{n} \bigl(p[n] - t[n]\bigr)^2,\]which is then reduced across the leading (non-time) dimensions according to
reduction.The misfit is usually written in the FWI literature with a leading factor of \(1/2\) (which cancels the 2 produced by differentiating the square); omitting it, as here, multiplies the misfit and its gradient by the same constant 2, moving no minimum and only rescaling the step length an optimiser must take.
This is the misfit that is most prone to cycle-skipping: it compares the oscillatory waveforms sample by sample, so a predicted arrival more than half a period from the observed one is pulled towards the wrong cycle.
envelope()andnormalised_cross_correlation()are drop-in alternatives with the same shape contract, though not with the same scale: this function sums over time whereenvelope()averages, so a swap usually wants the step length adjusted. The module docstring gives the exact relationship, and the one totorch.nn.functional.mse_loss(), whose"mean"reduction differs from this one by a factor ofnt.- Parameters:
pred – Predicted data, a real floating-point tensor of shape
[..., nt](e.g.[shot, receiver, time]).target – Observed/target data. Must have the same shape and dtype as
pred.reduction – One of
"mean"(default),"sum", or"none". With"none", the time axis is still reduced away (it is not meaningful to leave it, since the misfit is inherently a per-trace quantity), returning the per-trace sum of squared differences with shapepred.shape[:-1];"mean"/"sum"further reduce that per-trace tensor to a scalar.
- Returns:
A scalar tensor (
"mean"/"sum") or a tensor of shapepred.shape[:-1]("none").- Raises:
TypeError – If
predortargetis not a floating-point tensor, or their dtypes differ.ValueError – If
predandtargethave different shapes, either has zero dimensions, orreductionis not recognised.
- deepwave.misfits.envelope(pred: Tensor, target: Tensor, *, reduction: Literal['mean', 'sum', 'none'] = 'mean') Tensor¶
Mean squared error between the Hilbert envelopes of
predandtarget.The Hilbert envelope (analytic-signal magnitude) of a real trace \(x[n]\), \(n = 0, \dots, n_t - 1\), is computed from its discrete Fourier transform \(X = \mathrm{FFT}(x)\) by discarding the negative-frequency half of the spectrum and doubling the positive frequencies, following the standard FFT-based construction of the discrete analytic signal (Marple, 1999):
\[\begin{split}A[k] = \begin{cases} 1, & k = 0 \\ 2, & 1 \le k < \lceil n_t / 2 \rceil \\ 1, & n_t \text{ even and } k = n_t / 2 \\ 0, & \text{otherwise,} \end{cases} \qquad z = \mathrm{IFFT}(A \odot X), \qquad e[n] = |z[n]|,\end{split}\]where \(z\) is the discrete analytic signal and \(e\) its envelope (the \(k = n_t / 2\) “Nyquist” case only arises for even \(n_t\); it is self-conjugate and so kept at weight 1, not doubled). Writing \(e_{\text{pred}}\) and \(e_{\text{target}}\) for the envelopes of
predandtarget, the per-trace value is their mean squared difference over time,\[L = \frac{1}{n_t} \sum_{n} \bigl(e_{\text{pred}}[n] - e_{\text{target}}[n]\bigr)^2,\]which is then reduced across the leading (non-time) dimensions according to
reduction. Because it responds to envelope (amplitude-of-arrival) differences rather than the raw, oscillatory waveform, this misfit is markedly less sensitive to the phase/traveltime errors that cause cycle-skipping in the raw squared-error misfit, and is a standard FWI objective for that reason (Bozdağ, Trampert & Tromp, 2011).- Parameters:
pred – Predicted data, a real floating-point tensor of shape
[..., nt](e.g.[shot, receiver, time]).target – Observed/target data. Must have the same shape and dtype as
pred.reduction – One of
"mean"(default),"sum", or"none". With"none", the time axis is still reduced away (it is not meaningful to leave it, since the misfit is inherently a per-trace quantity), returning the per-trace mean squared envelope difference with shapepred.shape[:-1];"mean"/"sum"further reduce that per-trace tensor to a scalar.
- Returns:
A scalar tensor (
"mean"/"sum") or a tensor of shapepred.shape[:-1]("none").- Raises:
TypeError – If
predortargetis not a floating-point tensor, or their dtypes differ.ValueError – If
predandtargethave different shapes, either has zero dimensions, orreductionis not recognised.
References
S. L. Marple, “Computing the discrete-time analytic signal via FFT,” IEEE Transactions on Signal Processing, vol. 47, no. 9, 1999.
E. Bozdağ, J. Trampert, J. Tromp, “Misfit functions for full waveform inversion based on instantaneous phase and envelope measurements,” Geophysical Journal International, vol. 185, no. 2, 2011.
- deepwave.misfits.normalised_cross_correlation(pred: Tensor, target: Tensor, *, reduction: Literal['mean', 'sum', 'none'] = 'mean') Tensor¶
One minus the per-trace normalised cross-correlation.
Per trace, with \(p[n]\) =
pred, \(t[n]\) =target, \(n = 0, \dots, n_t - 1\), and \(\lVert \cdot \rVert_2\) the L2 norm over the time axis,\[\begin{split}\mathrm{NCC} = \begin{cases} \dfrac{\sum_n p[n]\, t[n]}{\lVert p \rVert_2 \, \lVert t \rVert_2}, & \lVert p \rVert_2 \, \lVert t \rVert_2 > 0, \\[6pt] 0, & \lVert p \rVert_2 \, \lVert t \rVert_2 = 0, \end{cases} \qquad L = 1 - \mathrm{NCC},\end{split}\]No mean is subtracted from either trace before normalising, so \(\mathrm{NCC}\) is the cosine similarity of the raw traces, not the statistical (mean centred) correlation coefficient: the misfit stays sensitive to any DC offset, which is physically meaningful for wave data, since a non-zero mean is itself a mismatch worth penalising.
The \(\lVert p \rVert_2 \, \lVert t \rVert_2 = 0\) branch applies only when at least one of
pred/targetis an exactly all-zero trace (its L2 norm is exactly0.0, not merely small): then \(\mathrm{NCC} = 0\) and \(L = 1\), a finite, defined result (rather than the0 / 0that an unguarded division would give) that reads as “no correlation” for a trace with no signal. It is a branch on the value 0.0, not a small-denominator threshold, so it does not fire at small-but-nonzero amplitudes: traces many orders of magnitude below unit scale are compared exactly like unit-scale data (see Normalised cross-correlation: numerical conditioning for the internal rescaling that makes that so).\(L\) is exactly 0 whenever
predequalstargetscaled by any positive constant on that trace (a pure amplitude error), and otherwise ranges over \([0, 2]\). That amplitude-scale invariance, with undiminished sensitivity to phase/traveltime shifts, is why normalised-correlation (“global correlation norm”) objectives are used as an FWI misfit robust to the amplitude mismatches that a squared-error misfit penalises but that need not indicate a poor model (Choi & Alkhalifah, 2012). The per-trace \(L\) is then reduced across the leading (non-time) dimensions according toreduction.- Parameters:
pred – Predicted data, a real floating-point tensor of shape
[..., nt](e.g.[shot, receiver, time]).target – Observed/target data. Must have the same shape and dtype as
pred.reduction – One of
"mean"(default),"sum", or"none". With"none", the time axis is still reduced away (it is not meaningful to leave it, since the misfit is inherently a per-trace quantity), returning the per-trace value with shapepred.shape[:-1];"mean"/"sum"further reduce that per-trace tensor to a scalar.
- Returns:
A scalar tensor (
"mean"/"sum") or a tensor of shapepred.shape[:-1]("none").- Raises:
TypeError – If
predortargetis not a floating-point tensor, or their dtypes differ.ValueError – If
predandtargethave different shapes, either has zero dimensions, orreductionis not recognised.
References
Y. Choi, T. Alkhalifah, “Application of multi-source waveform inversion to marine streamer data using the global correlation norm,” Geophysical Prospecting, vol. 60, no. 4, 2012.
Wavefield-space tools¶
The deepwave.wavefields module widens Deepwave from a differentiable shot-data operator (point sources in, receiver traces out) to a differentiable wavefield operator. Reach for it when the thing you want to force, reduce, or observe is the field itself rather than a trace: source_field_to_sources() and merge_point_sources() turn a dense forcing field into ordinary point sources, for wavefield-reconstruction inversion, source encoding and adjoint-source experiments; measure() with a RunningDFT accumulates a frequency-domain reduction as a run proceeds, so a frequency-domain objective costs O(n_freqs) storage instead of O(nt) and still carries gradients; and record_boundary() / reinject_boundary() record traces on a rectangular surface and replay them as source amplitudes at its cells. Everything here is pure Python built on the public propagator interface, and works on any propagator. Wavefield-space tools covers each in depth, including the limits of trace replay.
- deepwave.wavefields.source_field_to_sources(source_field: Tensor, *, origin: Sequence[int] | None = None, mask: Tensor | None = None) tuple[Tensor, Tensor]
Convert a volumetric source field into per-cell point sources.
Stability: provisional (see Stability policy).
A volumetric (extended) source is a forcing
f(shot, t, *cell)added to the wavefield at every cell of a region on every outer time step. Deepwave injects a point source at an integer grid cell as an exact single-cell forcing (the Hicks windowed-sinc interpolation collapses to a Kronecker delta at an on-node location), so a volumetric field is represented exactly by one integer-located point source per cell, with that cell’s time series as its amplitude. Because each cell appears once, the resulting source locations are automatically unique – the unique-cell constraint that governs hand-placed point sources never bites for field forcing.The mapping is a pure reshape/gather, so the returned amplitudes are differentiable w.r.t.
source_field: a gradient on the receiver data or on a later wavefield flows straight back to the field.- Parameters:
source_field – The forcing, shape
[shot, nt, *region_shape]whereregion_shapehasndimentries (e.g.[shot, nt, ny, nx]in 2-D).ntis the number of time steps and defines the propagation length.origin – The model cell index (length
ndim) of the region’s first cell, i.e.regioncell(0, ..., 0)sits at model cellorigin. Defaults to all zeros (the region starts at the model origin). This is added to the in-region cell indices to give model-relative source locations.mask – An optional boolean tensor of shape
region_shapeselecting which cells are active. If given, only theTruecells become sources (the field values atFalsecells are ignored); this is the region-masked form and keeps the source count (and cost) down for sparse regions.
- Returns:
A tuple
(source_amplitudes, source_locations)withsource_amplitudesof shape[shot, n_cell, nt]andsource_locationsof shape[shot, n_cell, ndim](integer, model-relative), ready to pass to any Deepwave propagator.
- deepwave.wavefields.merge_point_sources(field_amplitudes: Tensor, field_locations: Tensor, source_amplitudes: Tensor | None, source_locations: Tensor | None) tuple[Tensor, Tensor]
Concatenates field-derived sources with explicit point sources.
The second half of the volumetric-source recipe.
source_field_to_sources()turns an extended forcing into one exact single-cell source per cell; this merges that set with any point sources of your own, so that both can be passed through the propagator’s single source-amplitude/location pair:amp, loc = deepwave.wavefields.source_field_to_sources( field, origin=origin, mask=mask ) amp, loc = deepwave.wavefields.merge_point_sources( amp, loc, source_amplitudes, source_locations ) out = deepwave.scalar(v, dx, dt, sources={"u": (amp, loc)}, receivers={"u": receiver_locations})
The combined location set still has to be unique within each shot, so the explicit source cells must be disjoint from the field’s cells (fold any coincident point source into the field instead).
Stability: provisional (see Stability policy).
- Parameters:
field_amplitudes – The amplitudes returned by
source_field_to_sources().field_locations – The locations returned by
source_field_to_sources().source_amplitudes – The explicit point sources’ amplitudes, or
Nonefor “no point sources”, in which case the field’s own amplitudes and locations are returned unchanged.source_locations – The explicit point sources’ locations, as an integer torch.Tensor. Off-grid (floating-point or
deepwave.Hicks) point sources cannot share the field’s exact-node source slot and are rejected.
- Returns:
The concatenated source amplitudes.
The concatenated source locations.
- Return type:
Tuple
- Raises:
ValueError – If
source_amplitudesis given withoutsource_locations, if the two sets have different numbers of time samples, or ifsource_locationsis not an integer tensor.
- class deepwave.wavefields.RunningDFT(frequencies: Sequence[float], on: str | None = None)
Declarative running-DFT measurement, accumulated segment-wise.
Stability: provisional (see Stability policy).
Accumulates
X[f] = sum_t x[t] * exp(-2j*pi*f*t*dt)over the propagation, wherexis either a receiver-amplitude output or a wavefield of the propagator. The sum is built incrementally as segments complete. The DFT accumulators scale with the number of frequencies; each segment’s trace and propagation history also contribute to peak memory. The result matches an offline DFT of the assembled trace to round-off and is differentiable.- Parameters:
frequencies – The frequencies at which to evaluate the transform, in the reciprocal of
dt’s time unit (Hz whendtis in seconds).on – The name of the propagator output to transform. For receiver transforms this is a receiver-amplitude output name (e.g.
"receiver_amplitudes"for scalar,"receiver_amplitudes_p"for acoustic);None(default) selects the propagator’s first receiver-amplitude output. It may instead be a wavefield name (e.g."wavefield_0"), which requires per-step segmentation (seemeasure()), or one of the keys of the propagator’sauxiliary_state(e.g."psiy_m1"), which is treated the same way. Those keys are implementation state, not part of the public interface: their names, number and shapes may change in any release (see Stability policy).
- deepwave.wavefields.measure(propagator: Callable[[...], Any], model_args: Sequence[Tensor], grid_spacing: float | int | Sequence[float], dt: float, source_amplitudes: Mapping[str, Tensor], measurements: Sequence[RunningDFT], *, n_segments: int = 1, initial_state: PropagatorOutput | Mapping[str, Tensor | AuxiliaryState] | None = None, return_output: bool = False, **kwargs: Any) MeasureResult
Run a propagator while accumulating declarative reductions on the fly.
Stability: provisional (see Stability policy).
Runs
propagatorfrom step 0 tontinn_segmentscontiguous segments (threading state throughinitial_state), accumulating eachRunningDFTinmeasurementsas the segments complete. Nothing but the running accumulators (and, ifreturn_output, the final state) is kept across segments. The accumulators costO(n_freqs)per receiver; peak memory includes a segment’s trace and propagation history. The result is returned inside the autograd graph.Receiver-amplitude measurements work at any
n_segments. A measurement on a wavefield (onnames a wavefield, or one of the propagator’sauxiliary_statekeys) needs the field at every outer step, so it forces per-step segmentation (n_segments = nt); this makes the callntpropagator invocations. If a differentn_segmentswas explicitly requested, this override is reported via a warning (the per-step segmentation itself is exact but has Python-per-step overhead – prefer receiver measurements when you can).Requires ``step_ratio == 1`` when the run is segmented (whenever the resolved
n_segments > 1, including the per-step wavefield path). Otherwise aValueErroris raised, because splitting a propagation into segments is only bit-exact – and hence the accumulated reduction only equals an offline reduction of the whole trace – when the userdtalready satisfies the CFL condition, so Deepwave does no internal sub-stepping (the source amplitudes are otherwise resampled to the internal time step with a global FFT that does not decompose across a segment boundary; seedeepwave.checkpointing’s module docstring). A single-segment (n_segments == 1) receiver measurement runs at anystep_ratio.- Parameters:
propagator – A Deepwave propagator function (
deepwave.scalaretc.).model_args – Model tensors in positional order (
(v,)for scalar).grid_spacing – Grid spacing, as passed to the propagator.
dt – The user time step.
source_amplitudes – Mapping from source-amplitude argument name to tensor, defining
nt(e.g.{"source_amplitudes": src}).measurements – The reductions to accumulate.
n_segments – Number of contiguous segments (default 1). Ignored (forced to
nt) if any measurement is on a wavefield.initial_state – Optional starting state threaded into the first segment.
return_output – If True, populate the result’s
outputfield with the finalPropagatorOutput; it isNoneotherwise. The shape of the return value does not depend on this:MeasureResultis atuplesubclass, so a result that was sometimes a bare list and sometimes a pair would makefor m in measure(...)iterate the wrong thing.**kwargs – Remaining propagator keyword arguments forwarded to every segment.
storage_modeanddeepwave.checkpointing.run_segments()’s checkpointing parameters must not appear.
- Returns:
A
MeasureResult; unpack it asmeasurements, output = measure(...).measurementsholds one accumulated complex tensor per measurement, in the order ofmeasurements(result.measurements[i]is the reduction ofmeasurements[i]). Receiver transforms have shape[shot, receiver, n_freq]; wavefield transforms have shape[shot, *wavefield_spatial, n_freq].outputis the finalPropagatorOutputifreturn_outputis True, andNoneotherwise.
- class deepwave.wavefields.MeasureResult(measurements: tuple[Tensor, ...], output: PropagatorOutput | None)
The result of
deepwave.wavefields.measure().Returned for both values of
return_output; unpack it asmeasurements, output = measure(...), or readmeasure(...).measurements.Stability: provisional (see Stability policy).
- measurements
One accumulated complex tensor per
RunningDFT, in the order they were given.- Type:
tuple[torch.Tensor, …]
- output
The final
PropagatorOutput, orNoneunlessreturn_output=True.- Type:
deepwave.PropagatorOutput | None
Create new instance of MeasureResult(measurements, output)
- deepwave.wavefields.boundary_surface_locations(box: Sequence[tuple[int, int]]) Tensor
Integer cell coordinates of the surface (faces) of a rectangular box.
Stability: provisional (see Stability policy).
Enumerates every cell lying on the boundary faces of the axis-aligned box
box(each dimension given as a(start, stop)half-open range, in model cell indices). Corner and edge cells appear exactly once. The result is the dense receiver/source set forrecord_boundary()/reinject_boundary().- Parameters:
box – A sequence of
ndim(start, stop)tuples giving the box extent in model cell indices (half-open: cellsstart .. stop - 1).- Returns:
An integer tensor of shape
[n_surface, ndim]of the surface cell coordinates.
- deepwave.wavefields.record_boundary(propagator: Callable[[...], Any], model_args: Sequence[Tensor], grid_spacing: float | int | Sequence[float], dt: float, source_amplitudes: Tensor, source_locations: Tensor, box: Sequence[tuple[int, int]], *, channel: str | None = None, source_channel: str | None = None, receiver_kwarg: str | None = None, output_name: str | None = None, **kwargs: Any) BoundaryRecording
Record the wavefield on a rectangular surface with dense receivers.
Stability: provisional (see Stability policy).
Places a receiver at every cell on the faces of
box(viaboundary_surface_locations()) and runs the propagator, returning the recorded surface traces and the surface cell coordinates. The source traces supplied by the caller excite the selected source channel.Use the same explicit
pml_freqwhen comparing this run with a replay run, so their absorbing boundaries have the same configuration.- Parameters:
propagator – A Deepwave propagator function – any that
deepwave.spec()accepts.model_args – Model tensors in positional order.
grid_spacing – Grid spacing.
dt – Time step.
source_amplitudes – Source amplitudes
[shot, source, nt].source_locations – Source locations
[shot, source, ndim].box – The recording surface, as
(start, stop)per dimension.channel – The receiver channel to record, e.g.
"u"ondeepwave.scalar(),"p"ondeepwave.elastic(). Both the receiver keyword and the output field are resolved from it throughdeepwave.spec(), so callers name a channel – which is stable – rather than a keyword spelling, which is not. Defaults to the propagator’s first receiver channel, which is"u"onscalarand reproduces this function’s original behaviour exactly.source_channel – The source channel to excite, resolved independently of
channel. Defaults to the propagator’s first source channel for the dimension ofbox. For example, use"volume_rate"for the acoustic Born background source.receiver_kwarg – An explicit override of the receiver-locations keyword, for anything the spec cannot express. Passing it together with
channelraises.output_name – An explicit override of the output field holding the recorded traces. Passing it together with
channelraises.**kwargs – Remaining propagator keyword arguments.
- Returns:
A
BoundaryRecording, a named(recording, surface_locations)tuple whererecordinghas shape[shot, n_surface, nt]andsurface_locationsis[n_surface, ndim]– the two argumentsreinject_boundary()takes, in that order.
- class deepwave.wavefields.BoundaryRecording(recording: Tensor, surface_locations: Tensor)
The result of
deepwave.wavefields.record_boundary().A named two-tuple, so
recording, surface = record_boundary(...)still unpacks and still composes withreinject_boundary().Stability: provisional (see Stability policy).
- recording
The recorded surface traces,
[shot, surface_cell, time].- Type:
- surface_locations
The surface cell coordinates,
[surface_cell, ndim].- Type:
Create new instance of BoundaryRecording(recording, surface_locations)
- deepwave.wavefields.reinject_boundary(propagator: Callable[[...], Any], model_args: Sequence[Tensor], grid_spacing: float | int | Sequence[float], dt: float, recording: Tensor, surface_locations: Tensor, *, channel: str | None = None, amplitude_scale: float = 1.0, **kwargs: Any) PropagatorOutput
Replay boundary traces as sources at the supplied surface cells.
Stability: provisional (see Stability policy).
This helper multiplies the supplied traces by
amplitude_scaleand uses them as source amplitudes at the supplied surface cells. It applies no boundary-integral weights, normal derivatives, dipole terms, or automatic conversion from recorded-field units to source units. It does not in general reconstruct the original field or preserve its phase or amplitude.Use the same explicit
pml_freqwhen comparing the recording and replay runs, so their absorbing boundaries have the same configuration.- Parameters:
propagator – A Deepwave propagator function.
model_args – Model tensors for the replay run.
grid_spacing – Grid spacing.
dt – Time step.
recording – Boundary recording
[shot, n_surface, nt]fromrecord_boundary().surface_locations – Surface cell coordinates
[n_surface, ndim].channel – The physical channel to inject into, e.g.
"u"ondeepwave.scalar(),"p"ondeepwave.elastic(). The source amplitude and location keywords are resolved from it throughdeepwave.spec(), so this works on every propagator rather than only the ones that spell their source keywords the wayscalardoes. Defaults to the propagator’s first source channel, reproducing this function’s original behaviour onscalar.amplitude_scale – A scalar multiplying the supplied traces before injection. Default
1.0.**kwargs – Remaining propagator keyword arguments.
- Returns:
The propagator output of the replay run.