#NBVAL_IGNORE_OUTPUT
# Necessary imports
import devito as dv
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
from examples.seismic import TimeAxis, RickerSource
ADER-FD
This notebook demonstrates the implementation of a finite-difference scheme for solving the first-order formulation of the acoustic wave equation using ADER (Arbitrary-order-accuracy via DERivatives) time integration. This enables a temporal discretisation up the order of the spatial discretisation, whilst preventing the grid-grid decoupling (often referred to as checkerboarding) associated with solving first-order systems of equations on a single finite-difference grid.
The method is detailed in “Fast High Order ADER Schemes for Linear Hyperbolic Equations” by Schwartzkopf et al. (https://doi.org/10.1016/j.jcp.2003.12.007).
The state vector is defined as
\(\mathbf{U} = \begin{bmatrix} p \\ \mathbf{v} \end{bmatrix}\),
where \(p\) is pressure, and \(\mathbf{v}\) is particle velocity. Taking a Taylor-series expansion of \(\mathbf{U}(t+\Delta t)\) at time \(t\), one obtains
\(\mathbf{U}(t+\Delta t) = \mathbf{U}(t) + \Delta t\frac{\partial \mathbf{U}}{\partial t}(t) + \frac{\Delta t^2}{2}\frac{\partial^2 \mathbf{U}}{\partial t^2}(t) + \frac{\Delta t^3}{6}\frac{\partial^3 \mathbf{U}}{\partial t^3}(t) + \dots\).
Using the governing equations
\(\frac{\partial \mathbf{U}}{\partial t} = \begin{bmatrix}\rho c^2 \boldsymbol{\nabla}\cdot\mathbf{v} \\ \frac{1}{\rho}\boldsymbol{\nabla}p \end{bmatrix}\),
expressions for higher-order time derivatives of the state vector are derived in terms of spatial derivatives. For example, taking the second derivative of the state vector with respect to time
\(\frac{\partial^2 \mathbf{U}}{\partial t^2} = \begin{bmatrix}\rho c^2 \boldsymbol{\nabla}\cdot\frac{\partial \mathbf{v}}{\partial t} \\ \frac{1}{\rho}\boldsymbol{\nabla}\frac{\partial p}{\partial t} \end{bmatrix}\).
Substituting the temporal derivatives on the right hand side for the expressions given in the governing equations
\(\frac{\partial^2 \mathbf{U}}{\partial t^2} = \begin{bmatrix}\rho c^2 \boldsymbol{\nabla}\cdot\left(\frac{1}{\rho}\boldsymbol{\nabla}p\right) \\ \frac{1}{\rho}\boldsymbol{\nabla}\left(\rho c^2 \boldsymbol{\nabla}\cdot\mathbf{v}\right) \end{bmatrix}\).
Assuming constant \(c\) and \(\rho\), this simplifies to
\(\frac{\partial^2 \mathbf{U}}{\partial t^2} = \begin{bmatrix}c^2 \nabla^2 p \\ c^2\boldsymbol{\nabla}\left(\boldsymbol{\nabla}\cdot\mathbf{v}\right) \end{bmatrix}\).
This process is iterated to obtain the required temporal derivatives.
High-order explicit timestepping is achieved by substituting these expressions into the Taylor expansion, truncated at the desired temporal discretisation order. As such, the order of the temporal discretisation can be increased to that of the spatial discretisation.
To begin, we set up the Grid
. Note that no staggering is specified for the Function
s as it is not needed in this case.
# 1km x 1km grid
= dv.Grid(shape=(201, 201), extent=(1000., 1000.))
grid = dv.TimeFunction(name='p', grid=grid, space_order=16)
p = dv.VectorTimeFunction(name='v', grid=grid, space_order=16, staggered=(None, None)) v
Material parameters are specified as in the staggered case. Note that if one assumes non-constant material parameters when deriving higher-order time derivatives in terms of spatial derivatives, the resultant expressions will contain derivatives of material parameters. As such, it will be necessary to set the space_order
of the Function
s containing material parameters accordingly, and values may need extending into the halo using the data_with_halo
view.
# Material parameters
= dv.Function(name='c', grid=grid)
c = dv.Function(name='rho', grid=grid)
rho
= 1.5
c.data[:] 150] = 1.25
c.data[:, :100] = 1.
c.data[:, :50] = 0.75
c.data[:, :
= c.data[:]
rho.data[:]
# Define bouyancy for shorthand
= 1/rho
b # Define celerity shorthands
= c**2
c2 = c**4 c4
Now we will specify each of the temporal derivatives required for a 4th-order ADER timestepping scheme. Note that for conciseness, the derivations assume constant material parameters.
# dv.grad(dv.div(v)) is not the same as expanding the continuous operator and then discretising
# This is because dv.grad(dv.div(v)) applies a gradient stencil to a divergence stencil
def graddiv(f):
return sp.Matrix([[f[0].dx2 + f[1].dxdy],
0].dxdy + f[1].dy2]])
[f[
def lapdiv(f):
return f[0].dx3 + f[0].dxdy2 + f[1].dx2dy + f[1].dy3
def gradlap(f):
return sp.Matrix([[f.dx3 + f.dxdy2],
+ f.dy3]])
[f.dx2dy
def gradlapdiv(f):
return sp.Matrix([[f[0].dx4 + f[0].dx2dy2 + f[1].dx3dy + f[1].dxdy3],
0].dx3dy + f[0].dxdy3 + f[1].dx2dy2 + f[1].dy4]])\
[f[
def biharmonic(f):
return f.dx4 + 2*f.dx2dy2 + f.dy4
# First time derivatives
= rho*c2*dv.div(v)
pdt = b*dv.grad(p)
vdt
# Second time derivatives
= c2*p.laplace
pdt2 = c2*graddiv(v)
vdt2
# Third time derivatives
= rho*c4*lapdiv(v)
pdt3 = c2*b*gradlap(p)
vdt3
# Fourth time derivatives
= c4*biharmonic(p)
pdt4 = c4*gradlapdiv(v) vdt4
Define the model timestep.
# Model timestep
= 0.85*np.amin(grid.spacing)/np.amax(c.data) # Courant number of 0.85 op_dt
Now define the update equations for 4th-order ADER timestepping.
= grid.stepping_dim.spacing
dt
# Update equations (2nd-order ADER timestepping)
# eq_p = dv.Eq(p.forward, p + dt*pdt + (dt**2/2)*pdt2)
# eq_v = dv.Eq(v.forward, v + dt*vdt + (dt**2/2)*vdt2)
# Update equations (3rd-order ADER timestepping)
# eq_p = dv.Eq(p.forward, p + dt*pdt + (dt**2/2)*pdt2 + (dt**3/6)*pdt3)
# eq_v = dv.Eq(v.forward, v + dt*vdt + (dt**2/2)*vdt2 + (dt**3/6)*vdt3)
# Update equations (4th-order ADER timestepping)
= dv.Eq(p.forward, p + dt*pdt + (dt**2/2)*pdt2 + (dt**3/6)*pdt3 + (dt**4/24)*pdt4)
eq_p = dv.Eq(v.forward, v + dt*vdt + (dt**2/2)*vdt2 + (dt**3/6)*vdt3 + (dt**4/24)*vdt4) eq_v
Add a source.
#NBVAL_IGNORE_OUTPUT
= 0. # Simulation starts a t=0
t0 = 450. # Simulation last 0.45 seconds (450 ms)
tn
= TimeAxis(start=t0, stop=tn, step=op_dt)
time_range
= 0.020 # Source peak frequency is 20Hz (0.020 kHz)
f0 = RickerSource(name='src', grid=grid, f0=f0,
src =1, time_range=time_range)
npoint
# Position source centrally in all dimensions
0, :] = np.array(grid.extent) * .5
src.coordinates.data[
# We can plot the time signature to see the wavelet
src.show()
Finally we can run our propagator.
#NBVAL_IGNORE_OUTPUT
= src.inject(field=p.forward, expr=src)
src_term
= dv.Operator([eq_p, eq_v] + src_term)
op apply(dt=op_dt) op.
Operator `Kernel` ran in 0.05 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.031563999999999974, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
(PerfKey(name='section1', rank=None),
PerfEntry(time=0.009770000000000003, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
#NBVAL_IGNORE_OUTPUT
= [0, 1000, 1000, 0]
extent = np.abs(np.amax(p.data[-1]))
vmax ='Greys', extent=extent)
plt.imshow(c.data.T, cmap-1].T, cmap='seismic', alpha=0.75, extent=extent, vmin=-vmax, vmax=vmax)
plt.imshow(p.data["x (m)")
plt.xlabel("y (m)")
plt.ylabel("ADER-FD scheme")
plt.title( plt.show()
Using a staggered discretisation to solve the first-order acoustic wave equation with the same parameterisation:
#NBVAL_IGNORE_OUTPUT
= dv.TimeFunction(name='ps', grid=grid, space_order=16, staggered=dv.NODE)
ps = dv.VectorTimeFunction(name='vs', grid=grid, space_order=16)
vs
# First time derivatives
= rho*c2*dv.div(vs.forward)
psdt = b*dv.grad(ps)
vsdt
= dv.Eq(ps.forward, ps + dt*psdt)
eq_ps = dv.Eq(vs.forward, vs + dt*vsdt)
eq_vs
= src.inject(field=ps.forward, expr=src)
src_term_s
= dv.Operator([eq_vs, eq_ps] + src_term_s)
ops apply(dt=op_dt) ops.
Operator `Kernel` ran in 0.04 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.023771000000000007, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
(PerfKey(name='section1', rank=None),
PerfEntry(time=0.009549000000000004, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
#NBVAL_IGNORE_OUTPUT
= np.abs(np.amax(ps.data[-1]))
vmax ='Greys', extent=extent)
plt.imshow(c.data.T, cmap-1].T, cmap='seismic', alpha=0.75, extent=extent, vmin=-vmax, vmax=vmax)
plt.imshow(ps.data["x (m)")
plt.xlabel("y (m)")
plt.ylabel("Staggered FD scheme")
plt.title( plt.show()
It is apparent that the staggered scheme with leapfrog timestepping is unstable with the timestep used in the unstaggered scheme with ADER timestepping.
-1]) # Printing the maximum shows that the wavefield has gone to NaN np.amax(ps.data[
Data(nan, dtype=float32)
Reducing the timestep for comparison:
#NBVAL_IGNORE_OUTPUT
# Reset the fields
= 0
p.data[:] = 0
ps.data[:]
0].data[:] = 0
v[1].data[:] = 0
v[0].data[:] = 0
vs[1].data[:] = 0
vs[
= 0.5*np.amin(grid.spacing)/np.amax(c.data) # Courant number of 0.5
new_dt
apply(dt=new_dt, src=src.resample(dt=new_dt))
op.apply(dt=new_dt, src=src.resample(dt=new_dt)) ops.
Operator `Kernel` ran in 0.10 s
Operator `Kernel` ran in 0.06 s
PerformanceSummary([(PerfKey(name='section0', rank=None),
PerfEntry(time=0.041284000000000015, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
(PerfKey(name='section1', rank=None),
PerfEntry(time=0.015035000000000008, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])
#NBVAL_IGNORE_OUTPUT
= np.amax(np.abs(p.data[-1]))
vmax
= plt.subplots(1, 3, figsize=(15, 10), tight_layout=True, sharey=True)
fig, ax
# Note that due to the leapfrog timestepping, fields need to be accessed at different timesteps
0].imshow(c.data.T, cmap='Greys', extent=extent)
ax[= ax[0].imshow(p.data[1].T, cmap='seismic', alpha=0.75, extent=extent, vmin=-vmax, vmax=vmax)
im_p 0].set_xlabel("x (m)")
ax[0].set_ylabel("y (m)")
ax[0].title.set_text("ADER-FD scheme")
ax[
1].imshow(c.data.T, cmap='Greys', extent=extent)
ax[1].imshow(ps.data[0].T, cmap='seismic', alpha=0.75, extent=extent, vmin=-vmax, vmax=vmax)
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel("y (m)")
ax[1].title.set_text("Staggered FD scheme")
ax[
2].imshow(c.data.T, cmap='Greys', extent=extent)
ax[2].imshow(ps.data[0].T - p.data[1].T, cmap='seismic', alpha=0.75, extent=extent, vmin=-vmax, vmax=vmax)
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel("y (m)")
ax[2].title.set_text("Diff")
ax[
plt.show()
Note the damping of the field at the boundaries when using the ADER scheme. This is in the absence of any damping boundary conditions, hence the presence of reflections in the staggered case.
assert(np.isclose(np.linalg.norm(p.data), 1.6494513))
assert(np.isclose(np.linalg.norm(ps.data), 1.8125491))