Wave ([Wave equations](../02_wave) chapter) and diffusion ([Diffusion equations](../03_diffu) chapter)
equations are solved reliably by finite difference methods. As soon as
we add a first-order derivative in space, representing *advective*
transport (also known as *convective* transport), the numerics gets
more complicated and intuitively attractive methods no longer work
well. We shall show how and why such methods fail and provide
remedies. The present chapter builds on basic knowledge about finite
difference methods for diffusion and wave equations, including the
analysis by Fourier components, ([truncation error analysis](../B_trunc/trunc.ipynb)), and compact difference notation.

**Remark on terminology.**

It is common to refer to movement of a fluid as convection, while advection
is the transport of some material dissolved or suspended in the fluid.
We shall mostly choose the word advection here, but both terms are
in heavy use, and for mass transport of a substance the PDE has an
advection term, while the similar term for the heat equation is a
convection term.

Much more comprehensive discussion of dispersion analysis for
advection problems can be found in the book by Duran.
This is a an excellent resource for further studies on the topic of
advection PDEs, with emphasis on gener
alizations to real geophysical
problems. The book by Fletcher also has a good
overview of methods for advection and convection problems.

# One-dimensional time-dependent advection equations

We consider the pure advection model

$$
\begin{equation}
\frac{\partial u}{\partial t} + v\frac{\partial u}{\partial x} = 0,\quad
 x\in (0,L),\ t\in (0,T],
\label{advec:1D:pde1:u} \tag{1}
\end{equation}
$$

$$
\begin{equation}
u(x,0) = I(x), x\in (0,L),
\label{advec:1D:pde1:U0} \tag{2}
\end{equation}
$$

$$
\begin{equation}
u(0,t) = U_0, t\in (0,T].
\label{advec:1D:pde1:I} \tag{3}
\end{equation}
$$

In ([1](#advec:1D:pde1:u)), $v$ is a given parameter, typically reflecting
the transport velocity of a quantity $u$ with a flow.
There is only one boundary condition ([3](#advec:1D:pde1:I)) since
the spatial derivative is only first order in the PDE ([1](#advec:1D:pde1:u)).
The information at $x=0$ and the initial condition get
transported in the positive $x$ direction
if $v>0$ through the domain.

It is easiest to find the solution of ([1](#advec:1D:pde1:u)) if we remove the
boundary condition and consider a process on the
infinite domain $(-\infty, \infty)$. The solution is simply

$$
\begin{equation}
u(x,t) = I(x-vt)\thinspace .
\label{advec:1D:pde1:sol} \tag{4}
\end{equation}
$$

This is also the solution we expect locally in a finite domain before boundary
conditions have reflected or modified the wave.

A particular feature of the solution ([4](#advec:1D:pde1:sol)) is that

$$
\begin{equation}
u(x_i, t_{n+1}) = u(x_{i-1}, t_n),
\label{advec:1D:pde1:uprop1} \tag{5}
\end{equation}
$$

if $x_i=i\Delta x$ and $t_n=n\Delta t$ are points in a uniform mesh.
We see this relation from

$$
u(i\Delta x, (n+1)\Delta t) = I(i\Delta x - v(n+1)\Delta t) \nonumber
$$

$$
= I((i-1)\Delta x - vn\Delta t - v\Delta t + \Delta x) \nonumber
$$

$$
= I((i-1)\Delta x - vn\Delta t) \nonumber
$$

$$
= u((i-1)\Delta x, n\Delta t), \nonumber
$$

provided $v = \Delta x/\Delta t$. So, whenever we see a scheme that
collapses to

$$
\begin{equation}
u^{n+1}_i = u_{i-1}^n,
\label{advec:1D:pde1:uprop2} \tag{6}
\end{equation}
$$

for the PDE in question, we have in fact a scheme that reproduces the
analytical solution, and many of the schemes to be presented possess
this nice property!

Finally, we add that a discussion of appropriate boundary conditions
for the advection PDE in multiple dimensions is a challenging topic beyond
the scope of this text.

## Simplest scheme: forward in time, centered in space

### Method

A first attempt to solve a PDE like ([1](#advec:1D:pde1:u)) will normally
be to look for a time-discretization scheme that is explicit so we avoid
solving systems of linear equations. In space, we anticipate that
centered differences are most accurate and therefore best. These
two arguments lead us to a Forward Euler scheme in time and
centered differences in space:

$$
\begin{equation}
[D_t^+ u + vD_{2x} u = 0]^n_i
\label{_auto1} \tag{7}
\end{equation}
$$

Written out, we see that this expression implies that

$$
u^{n+1} = u^n - \frac{1}{2} C (u^n_{i+1}-u_{i-1}^n),
$$

with $C$ as the Courant number

$$
C = \frac{v\Delta t}{\Delta x}\thinspace .
$$

### Implementation

A solver function for our scheme goes as follows.

```python
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
from devito import Grid, Eq, solve, TimeFunction, Operator
```

```python
# %load -s solver_FECS src-advec/advec1D.py
def solver_FECS(I, U0, v, L, dt, C, T, user_action=None):
    Nt = int(round(T/float(dt)))
    t = np.linspace(0, Nt*dt, Nt+1)  # Mesh points in time
    dx = v*dt/C
    Nx = int(round(L/dx))
    x = np.linspace(0, L, Nx+1)      # Mesh points in space

    # Make sure dx and dt are compatible with x and t
    dx = float(x[1] - x[0])
    dt = float(t[1] - t[0])
    C = v*dt/dx

    grid = Grid(shape=(Nx+1,), extent=(L,))
    t_s=grid.time_dim

    u = TimeFunction(name='u', grid=grid, space_order=2, save=Nt+1)

    pde = u.dtr + v*u.dxc

    stencil = solve(pde, u.forward)
    eq = Eq(u.forward, stencil)

    # Set initial condition u(x,0) = I(x)
    u.data[1, :] = [I(xi) for xi in x]

    # Insert boundary condition
    bc = [Eq(u[t_s+1, 0], U0)]

    op = Operator([eq] + bc)
    op.apply(dt=dt, x_m=1, x_M=Nx-1)

    if user_action is not None:
        for n in range(0, Nt + 1):
            user_action(u.data[n], x, t, n)

```

### Test cases

The typical solution $u$ has the shape of $I$ and is transported at
velocity $v$ to the right (if $v>0$). Let us consider two different
initial conditions, one smooth (Gaussian pulse) and one non-smooth
(half-truncated cosine pulse):

$$
\begin{equation}
u(x,0) = Ae^{-\frac{1}{2}\left(\frac{x-L/10}{\sigma}\right)^2},
\label{advec:1D:case_gaussian} \tag{8}
\end{equation}
$$

$$
\begin{equation}
u(x,0) = A\cos\left(\frac{5\pi}{L}\left( x - \frac{L}{10}\right)\right),\quad
x < \frac{L}{5} \hbox{ else } 0\thinspace .
\label{advec:1D:case_cos} \tag{9}
\end{equation}
$$

The parameter $A$ is the maximum value of the initial condition.

Before doing numerical simulations, we scale the PDE
problem and introduce $\bar x = x/L$ and $\bar t= vt/L$,
which gives

$$
\frac{\partial\bar u}{\partial \bar t} +
\frac{\partial\bar u}{\partial\bar x} = 0\thinspace .
$$

The unknown $u$ is scaled by the maximum value of the initial condition:
$\bar u = u/\max |I(x)|$ such that $|\bar u(\bar x, 0)|\in [0,1]$.
The scaled problem is solved by setting $v=1$, $L=1$, and $A=1$.
From now on we drop the bars.

To run our test cases and plot the solution, we make the function

```python
def run_FECS(case):
    """Special function for the FECS case."""
    if case == 'gaussian':
        def I(x):
            return np.exp(-0.5*((x-L/10)/sigma)**2)
    elif case == 'cosinehat':
        def I(x):
            return np.cos(np.pi*5/L*(x - L/10)) if x < L/5 else 0

    L = 1.0
    sigma = 0.02
    legends = []

    def plot(u, x, t, n):
        """Animate and plot every m steps in the same figure."""
        plt.figure(1)
        if n == 0:
            lines = plot(x, u)
        else:
            lines[0].set_ydata(u)
            plt.draw()
            #plt.savefig()
        plt.figure(2)
        m = 40
        if n % m != 0:
            return
        print('t=%g, n=%d, u in [%g, %g] w/%d points' % \
              (t[n], n, u.min(), u.max(), x.size))
        if np.abs(u).max() > 3:  # Instability?
            return
        plt.plot(x, u)
        legends.append('t=%g' % t[n])

    plt.ion()
    U0 = 0
    dt = 0.001
    C = 1
    T = 1
    solver(I=I, U0=U0, v=1.0, L=L, dt=dt, C=C, T=T,
           user_action=plot)
    plt.legend(legends, loc='lower left')
    plt.savefig('tmp.png'); plt.savefig('tmp.pdf')
    plt.axis([0, L, -0.75, 1.1])
    plt.show()

```

### Bug?

Running either of the test cases, the plot becomes a mess, and
the printout of $u$ values in the `plot` function reveals that
$u$ grows very quickly. We may reduce $\Delta t$ and make it
very small, yet the solution just grows.
Such behavior points to a bug in the code.
However, choosing a coarse mesh and performing one time step by
hand calculations produces the same numbers as the code, so
the implementation seems to be correct.
The hypothesis is therefore that the solution is unstable.

## Analysis of the scheme

It is easy to show that a typical Fourier component

$$
u(x,t)= B\sin (k(x-ct))
$$

is a solution of our PDE for any spatial wave length $\lambda = 2\pi /k$
and any amplitude $B$. (Since the PDE to be investigated by this method
is homogeneous and linear, $B$ will always cancel out, so we tend to skip
this amplitude, but keep it here in the beginning for completeness.)

A general solution may be viewed as a collection of long and
short waves with different amplitudes. Algebraically, the work
simplifies if we introduce the complex Fourier component

$$
u(x,t)=A_\text{e} e^{ikx},
$$

with

$$
A_\text{e}=Be^{-ikv\Delta t} = Be^{-iCk\Delta x}\thinspace .
$$

Note that $|A_\text{e}| \leq 1$.

It turns out that many schemes also allow a Fourier wave component as
solution, and we can use the numerically computed values of $A_\text{e}$
(denoted $A$) to learn about the
quality of the scheme. Hence, to analyze the difference scheme we have just
implemented, we look at how it treats the Fourier component

$$
u_q^n = A^n e^{ikq\Delta x}\thinspace .
$$

Inserting the numerical component in the scheme,

$$
[D_t^+ A e^{ikq\Delta x} + v D_{2x}A e^{ikq\Delta x} = 0]^n_q,
$$

and making use of [this equation](../A_formulas/formulas.ipynb#form:exp:fd1c:center)
results in

$$
[e^{ikq\Delta x} (\frac{A-1}{\Delta t} + v\frac{1}{\Delta x}i\sin (k\Delta x)) = 0]^n_q,
$$

which implies

$$
A = 1 - iC\sin(k\Delta x)\thinspace .
$$

The numerical solution features the formula $A^n$. To find out whether
$A^n$ means growth in time, we rewrite $A$ in polar form: $A=A_re^{i\phi}$,
for real numbers $A_r$ and $\phi$,
since we then have $A^n = A_r^ne^{i\phi n}$. The magnitude of $A^n$ is
$A_r^n$. In our case, $A_r = (1 + C^2\sin^2(kx))^{1/2} > 1$, so
$A_r^n$ will increase in time, whereas the
exact solution will not. Regardless of $\Delta t$, we get unstable
numerical solutions.

## Leapfrog in time, centered differences in space

### Method

Another explicit scheme is to do a "leapfrog" jump over $2\Delta t$ in
time and combine it with central differences in space:

$$
[D_{2t} u + vD_{2x} u = 0]_i^n,
$$

which results in the updating formula

$$
u^{n+1}_i = u^{n-1}_i - C(u_{i+1}^n-u_{i-1}^n)\thinspace .
$$

A special scheme is needed to compute $u^1$, but we leave that problem for
now. Anyway, this special scheme can be found in
[`advec1D.py`](https://github.com/devitocodes/devito_book/blob/master/fdm-devito-notebooks/04_advec/src-advec/advec1D.py).

### Implementation

We now need to work with three time levels and must modify our solver a bit:

```python
Nt = int(round(T/float(dt)))
t = np.linspace(0, Nt*dt, Nt+1)   # Mesh points in time
...
if scheme == 'FE':
    u   = TimeFunction(name='u', grid=grid, space_order=2, save=Nt+1)
    pde = u.dtr + v*u.dxc
    ...
elif scheme == 'LF':
    u = ...

    # Use some scheme for the first step
    pde0 = ...
    ...

    # Move to the LF scheme after the first step
    pde = u.dtc + v*u.dxc
    ...
```

### Running a test case

Let us try a coarse mesh such that the smooth Gaussian initial condition
is represented by 1 at mesh node 1 and 0 at all other nodes. This
triangular initial condition should then be advected to the right.
Choosing scaled variables as $\Delta t=0.1$, $T=1$, and $C=1$ gives
the plot in [Figure](#advec:1D:case_gaussian:fig:LFCS), which
is in fact identical to the exact solution (!).

Exact solution obtained by Leapfrog scheme with $\Delta t = 0.1$ and $C=1$.

### Running more test cases

We can run two types of initial conditions for $C=0.8$: one very
smooth with a Gaussian function ([Figure](#advec:1D:LF:fig1:C08)) and
one with a discontinuity in the first derivative ([Figure](#advec:1D:LF:fig2:C08)).  Unless we have a very fine mesh, as in
the left plots in the figures, we get small ripples behind the main
wave, and this main wave has the amplitude reduced.

Advection of a Gaussian function with a leapfrog scheme and $C=0.8$, $\Delta t = 0.001$ (left) and $\Delta t=0.01$ (right).

Advection of the Gaussian function with a leapfrog scheme, using $C=0.8$ and $\Delta t = 0.01$ can be seen in a [movie file](mov-advec/gaussian/LF/C08_dt01.ogg). Alternatively, with $\Delta t = 0.001$, we get this [movie file](mov-advec/gaussian/LF/C08_dt001.ogg).

Advection of half a cosine function with a leapfrog scheme and $C=0.8$, $\Delta t = 0.001$ (left) and $\Delta t=0.01$ (right).

Advection of the cosine hat function with a leapfrog scheme, using $C=0.8$ and $\Delta t = 0.01$ can be seen in a [movie file](mov-advec/cosinehat/LF/C08_dt01.ogg). Alternatively, with $\Delta t = 0.001$, we get this [movie file](mov-advec/cosinehat/LF/C08_dt001.ogg).

### Analysis

We can perform a Fourier analysis again. Inserting the numerical
Fourier component in the Leapfrog scheme, we get

$$
A^2 - i2C\sin(k\Delta x) A - 1 = 0,
$$

and

$$
A = -iC\sin(k\Delta x) \pm \sqrt{1-C^2\sin^2(k\Delta x)}\thinspace .
$$

Rewriting to polar form, $A=A_re^{i\phi}$, we see that $A_r=1$, so the
numerical component is neither increasing nor decreasing in time, which is
exactly what we want. However, for $C>1$, the square root can become
complex valued, so stability is obtained only as long as $C\leq 1$.

**Stability.**

For all the working schemes to be presented in this chapter, we
get the stability condition $C\leq 1$:

$$
\Delta t \leq \frac{\Delta x}{v}\thinspace .
$$

This is called the CFL condition and applies almost always to successful
schemes for advection problems. Of course, one can use Crank-Nicolson or
Backward Euler schemes for increased and even unconditional
stability (no $\Delta t$ restrictions), but these have other
less desired damping problems.

We introduce $p=k\Delta x$. The amplification factor now reads

$$
A = -iC\sin p \pm \sqrt{1-C^2\sin^2 p},
$$

and is to be compared to the exact amplification factor

$$
A_\text{e} = e^{-ikv\Delta t} = e^{-ikC\Delta x} = e^{-iCp}\thinspace .
$$

the section [Analysis of dispersion relations](#advec:1D:disprel) compares numerical amplification factors
of many schemes with the exact expression.

## Upwind differences in space

Since the PDE reflects transport of information along with a flow in
positive $x$ direction, when $v>0$, it could be natural to go (what is called)
upstream and not
downstream in the spatial derivative to collect information about the
change of the function. That is, we approximate

$$
\frac{\partial u}{\partial x}(x_i,t_n)\approx [D^-_x u]^n_i = \frac{u^n_{i} - u^n_{i-1}}{\Delta x}\thinspace .
$$

This is called an *upwind difference* (the corresponding difference in the
time direction would be called a backward difference, and we could use that
name in space too, but *upwind* is the common name for a difference against
the flow in advection problems). This spatial approximation does magic
compared to
the scheme we had with Forward Euler in time and centered difference in space.
With an upwind difference,

$$
\begin{equation} [D^+_t u + vD^-_x u = 0]^n_i,
\label{advec:1D:upwind} \tag{10}
\end{equation}
$$

written out as

$$
u^{n+1}_i = u^n_i - C(u^{n}_{i}-u^{n}_{i-1}),
$$

gives a generally popular and robust scheme that is stable if $C\leq 1$.
As with the Leapfrog scheme, it becomes exact if $C=1$, exactly as shown in
[Figure](#advec:1D:case_gaussian:fig:LFCS). This is easy to see since
$C=1$ gives the property ([6](#advec:1D:pde1:uprop2)).
However, any $C<1$ gives a significant reduction in the amplitude of the
solution, which is a purely numerical effect, see
[this Figure](#advec:1D:UP:fig1:C08) and [this Figure](#advec:1D:UP:fig2:C08).
Experiments show, however, that
reducing $\Delta t$ or $\Delta x$, while keeping $C$ reduces the
error.

Advection of a Gaussian function with a forward in time, upwind in space scheme and $C=0.8$, $\Delta t = 0.01$ (left) and $\Delta t=0.001$ (right).

Advection of the Gaussian function with a forward in time, upwind in space scheme, using $C=0.8$ and $\Delta t = 0.01$ can be seen in a [movie file](mov-advec/gaussian/UP/C08_dt001/movie.ogg). Alternatively, with $\Delta t = 0.005$, we get this [movie file](mov-advec/gaussian/UP/C08_dt0005/movie.ogg).

Advection of half a cosine function with a forward in time, upwind in space scheme and $C=0.8$, $\Delta t = 0.001$ (left) and $\Delta t=0.01$ (right).

Advection of the cosine hat function with a forward in time, upwind in space scheme, using $C=0.8$ and $\Delta t = 0.01$ can be seen in a [movie file](mov-advec/cosinehat/UP/C08_dt01.ogg). Alternatively, with $\Delta t = 0.001$, we get this [movie file](mov-advec/cosinehat/UP/C08_dt001.ogg).

The amplification factor can be computed using [this formula](../A_formulas/formulas.ipynb#form:exp:fd1:bw).

$$
\frac{A - 1}{\Delta t} + \frac{v}{\Delta x}(1 - e^{-ik\Delta x}) = 0,
$$

which means

$$
A = 1 - C(1 - \cos(p) - i\sin(p))\thinspace .
$$

For $C<1$ there is, unfortunately,
non-physical damping of discrete Fourier components, giving rise to reduced
amplitude of $u^n_i$ as in [this Figure](#advec:1D:UP:fig1:C08)
and [this Figure](#advec:1D:UP:fig2:C08). The damping seen
in these figures is quite severe. Stability requires $C\leq 1$.

**Interpretation of upwind difference as artificial diffusion.**

One can interpret the upwind difference as extra, artificial diffusion
in the equation. Solving

$$
\frac{\partial u}{\partial t} + v\frac{\partial u}{\partial x}
= \nu\frac{\partial^2 u}{\partial x^2},
$$

by a forward difference in time and centered differences in space,

$$
D^+_t u + vD_{2x} u = \nu D_xD_x u]^n_i,
$$

actually gives the upwind scheme ([10](#advec:1D:upwind)) if
$\nu = v\Delta x/2$. That is, solving the PDE $u_t + vu_x=0$
by centered differences in space and forward difference in time is
unsuccessful, but by adding some artificial diffusion $\nu u_{xx}$,
the method becomes stable:

$$
\frac{\partial u}{\partial t} + v
\frac{\partial u}{\partial x} = \left(\alpha + \frac{v\Delta x}{2}\right)
\frac{\partial^2 u}{\partial x^2}\thinspace .
$$

## Periodic boundary conditions

So far, we have given the value on the left boundary, $u_0^n$, and used
the scheme to propagate the solution signal through the domain.
Often, we want to follow such signals for long time series, and periodic
boundary conditions are then relevant since they enable a signal that
leaves the right boundary to immediately enter the left boundary and propagate
through the domain again.

The periodic boundary condition is

$$
u(0,t) = u(L,t),\quad u_0^n = u_{N_x}^n\thinspace .
$$

It means that we in the first equation, involving $u_0^n$, insert $u_{N_x}^n$,
and that we in the last equation, involving $u^{n+1}_{N_x}$ insert $u^{n+1}_0$.
Normally, we can do this in the simple way that `u[t_s, 0]` is updated as
`u[t_s, Nx]` at the beginning of a new time level.

In some schemes we may need $u^{n}_{N_x+1}$ and $u^{n}_{-1}$. Periodicity
then means that these values are equal to $u^n_1$ and $u^n_{N_x-1}$,
respectively. For the upwind scheme, it is sufficient to set
`u[t_s, 0]=u[t_s, Nx]` at a new time level before computing `u[t_s+1, 1]`. This ensures
that `u[t_s+1, 1]` becomes right and at the next time level `u[t_s+1, 0]` at the current
time level is correctly updated.
For the Leapfrog scheme we must update `u[t_s+1, 0]` and `u[t_s+1, Nx]` using the scheme:

```python
pbc = [Eq(u[t_s+1, 0], u[t_s-1, 0] - C*(u[t_s, 1] - u[t_s, Nx-1]))]
pbc += [Eq(u[t_s+1, Nx], u[t_s+1, 0])]
...
```

## Implementation

### Test condition

Analytically, we can show that the integral in space under the $u(x,t)$ curve
is constant:

$$
\begin{align*}
\int_0^L \left(\frac{\partial u}{\partial t} + v\frac{\partial u}{\partial x}
\right) dx &= 0\\
\frac{\partial }{\partial t} \int_0^L udx &=
- \int_0^L v\frac{\partial u}{\partial x}dx\\
\frac{\partial u}{\partial t} \int_0^L udx &=
[v u]_0^L =0
\end{align*}
$$

as long as $u(0)=u(L)=0$. We can therefore use the property

$$
\int_0^L u(x,t)dx = \hbox{const}
$$

as a partial verification during the simulation. Now, any numerical method
with $C\neq 1$ will deviate from the constant, expected value, so
the integral is a measure of the error in the scheme. The integral can
be computed by the Trapezoidal integration rule

```python
dx*(0.5*u.data[n][0] + 0.5*u.data[n][Nx] + np.sum(u.data[n][1:Nx]))
```

if `u` is a `TimeFunction` with the `save` parameter set to $Nx+1$ and `n` indicates a current timestep.

### The code

An appropriate `solver` function for multiple schemes may go as shown
below.

```python
# %load -s solver src-advec/advec1D.py
def solver(I, U0, v, L, dt, C, T, user_action=None,
           scheme='FE', periodic_bc=True):
    Nt = int(round(T/np.float64(dt)))
    t = np.linspace(0, Nt*dt, Nt+1)   # Mesh points in time
    dx = v*dt/C
    Nx = int(round(L/dx))
    x = np.linspace(0, L, Nx+1)       # Mesh points in space

    # Make sure dx and dt are compatible with x and t
    dx = x[1] - x[0]
    dt = t[1] - t[0]
    C = v*dt/dx
    print('dt=%g, dx=%g, Nx=%d, C=%g' % (dt, dx, Nx, C))

    integral = np.zeros(Nt+1)

    grid = Grid(shape=(Nx+1,), extent=(L,), dtype=np.float64)

    t_s=grid.time_dim

    def u(to=1, so=1):
        u = TimeFunction(name='u', grid=grid, time_order=to, space_order=so, save=Nt+1)
        return u

    if scheme == 'FE':
        u   = u(so=2)
        pde = u.dtr + v*u.dxc

        pbc = [Eq(u[t_s+1, 0], u[t_s, 0] - 0.5*C*(u[t_s, 1] - u[t_s, Nx]))]
        pbc += [Eq(u[t_s+1, Nx], u[t_s+1, 0])]

    elif scheme == 'LF':
        # Use UP scheme for the first timestep
        u = u(to=2, so=2)
        pde0 = u.dtr(fd_order=1) + v*u.dxl(fd_order=1)

        stencil0 = solve(pde0, u.forward)
        eq0      = Eq(u.forward, stencil0).subs(t_s, 0)

        pbc0 = [Eq(u[t_s, 0], u[t_s, Nx]).subs(t_s, 0)]

        # Now continue with LF scheme
        pde = u.dtc + v*u.dxc

        pbc = [Eq(u[t_s+1, 0], u[t_s-1, 0] - C*(u[t_s, 1] - u[t_s, Nx-1]))]
        pbc += [Eq(u[t_s+1, Nx], u[t_s+1, 0])]

    elif scheme == 'UP':
        u   = u()
        pde = u.dtr + v*u.dxl

        pbc = [Eq(u[t_s, 0], u[t_s, Nx])]

    elif scheme == 'LW':
        u = u(so=2)
        pde = u.dtr + v*u.dxc - 0.5*dt*v**2*u.dx2

        pbc = [Eq(u[t_s+1, 0], u[t_s, 0] - 0.5*C*(u[t_s, 1] - u[t_s, Nx-1]) + \
                  0.5*C**2*(u[t_s, 1] - 2*u[t_s, 0] + u[t_s, Nx-1]))]
        pbc += [Eq(u[t_s+1, Nx], u[t_s+1, 0])]

    else:
        raise ValueError('scheme="%s" not implemented' % scheme)

    stencil = solve(pde, u.forward)
    eq = Eq(u.forward, stencil)

    bc_init = [Eq(u[t_s+1, 0], U0).subs(t_s, 0)]

    # Set initial condition u(x,0) = I(x)
    u.data[0, :] = [I(xi) for xi in x]

    # Compute the integral under the curve
    integral[0] = dx*(0.5*u.data[0][0] + 0.5*u.data[0][Nx] + np.sum(u.data[0][1:Nx]))

    if user_action is not None:
        user_action(u.data[0], x, t, 0)

    bc  = [Eq(u[t_s+1, 0], U0)]

    if scheme == 'LF':
        op = Operator((pbc0 if periodic_bc else []) + [eq0] + (bc_init if not periodic_bc else []) \
                      + (pbc if periodic_bc else []) + [eq] + (bc if not periodic_bc else []))
    else:
        op = Operator(bc_init + (pbc if periodic_bc else []) + [eq] + (bc if not periodic_bc else []))

    op.apply(dt=dt, x_m=1, x_M=Nx if scheme == 'UP' else Nx-1)

    for n in range(1, Nt+1):
        # Compute the integral under the curve
        integral[n] = dx*(0.5*u.data[n][0] + 0.5*u.data[n][Nx] + np.sum(u.data[n][1:Nx]))

        if user_action is not None:
            user_action(u.data[n], x, t, n)

        print('I:', integral[n])
    return integral

```

### Solving a specific problem

We need to call up the `solver` function in some kind of administering
problem solving function that can solve specific problems and make
appropriate visualization. The function below makes both static plots,
screen animation, and hard copy videos in various formats.

```python
def run(scheme='UP', case='gaussian', C=1, dt=0.01):
    """General admin routine for explicit and implicit solvers."""

    if case == 'gaussian':
        def I(x):
            return np.exp(-0.5*((x-L/10)/sigma)**2)
    elif case == 'cosinehat':
        def I(x):
            return np.cos(np.pi*5/L*(x - L/10)) \
                   if 0 < x < L/5 else 0

    L = 1.0
    sigma = 0.02
    global lines  # needs to be saved between calls to plot

    def plot(u, x, t, n):
        """Plot t=0 and t=0.6 in the same figure."""
        plt.figure(1)
        global lines
        if n == 0:
            lines = plt.plot(x, u)
            plt.axis([x[0], x[-1], -0.5, 1.5])
            plt.xlabel('x'); plt.ylabel('u')
            plt.savefig('tmp_%04d.png' % n)
            plt.savefig('tmp_%04d.pdf' % n)
        else:
            lines[0].set_ydata(u)
            plt.axis([x[0], x[-1], -0.5, 1.5])
            plt.title('C=%g, dt=%g, dx=%g' %
                      (C, t[1]-t[0], x[1]-x[0]))
            plt.legend(['t=%.3f' % t[n]])
            plt.xlabel('x'); plt.ylabel('u')
            plt.draw()
            plt.savefig('tmp_%04d.png' % n)
        plt.figure(2)
        eps = 1E-14
        if abs(t[n] - 0.6) > eps and abs(t[n] - 0) > eps:
            return
        print('t=%g, n=%d, u in [%g, %g] w/%d points' % \
              (t[n], n, u.min(), u.max(), x.size))
        if np.abs(u).max() > 3:  # Instability?
            return
        plt.plot(x, u)
        plt.draw()
        if n > 0:
            y = [I(x_-v*t[n]) for x_ in x]
            plt.plot(x, y, 'k--')
            if abs(t[n] - 0.6) < eps:
                filename = ('tmp_%s_dt%s_C%s' % \
                            (scheme, t[1]-t[0], C)).replace('.', '')
                np.savez(filename, x=x, u=u, u_e=y)

    plt.ion()
    U0 = 0
    T = 0.7
    v = 1
    # Define video formats and libraries
    codecs = dict(flv='flv', mp4='libx264', webm='libvpx',
                  ogg='libtheora')
    # Remove video files
    import glob, os
    for name in glob.glob('tmp_*.png'):
        os.remove(name)
    for ext in codecs:
        name = 'movie.%s' % ext
        if os.path.isfile(name):
            os.remove(name)

    if scheme == 'CN':
        integral = solver_theta(
            I, v, L, dt, C, T, user_action=plot, FE=False)
    elif scheme == 'BE':
        integral = solver_theta(
            I, v, L, dt, C, T, theta=1, user_action=plot)
    else:
        integral = solver(
            I=I, U0=U0, v=v, L=L, dt=dt, C=C, T=T,
            scheme=scheme, user_action=plot)
    # Finish figure(2)
    plt.figure(2)
    plt.axis([0, L, -0.5, 1.1])
    plt.xlabel('$x$');  plt.ylabel('$u$')
    plt.savefig('tmp1.png'); plt.savefig('tmp1.pdf')
    plt.show()
    # Make videos from figure(1) animation files
    for codec in codecs:
        cmd = 'ffmpeg -i tmp_%%04d.png -r 25 -vcodec %s movie.%s' % \
              (codecs[codec], codec)
        os.system(cmd)
    print('Integral of u:', integral.max(), integral.min())

```

The complete code is found in the file
[`advec1D.py`](https://github.com/devitocodes/devito_book/blob/master/fdm-devito-notebooks/04_advec/src-advec/advec1D.py).

## A Crank-Nicolson discretization in time and centered differences in space

Another obvious candidate for time discretization is the Crank-Nicolson
method combined with centered differences in space:

$$
[D_t u]^n_i + v\frac{1}{2}([D_{2x} u]^{n+1}_i + [D_{2x} u]^{n}_i) = 0\thinspace .
$$

It can be nice to include the Backward Euler scheme too, via the
$\theta$-rule,

$$
[D_t u]^n_i + v\theta [D_{2x} u]^{n+1}_i + v(1-\theta)[D_{2x} u]^{n}_i = 0\thinspace .
$$

When $\theta$ is different from zero, this gives rise to an *implicit* scheme,

$$
u^{n+1}_i + \frac{\theta}{2} C (u^{n+1}_{i+1} - u^{n+1}_{i-1})
= u^n_i - \frac{1-\theta}{2} C (u^{n}_{i+1} - u^{n}_{i-1})
$$

for $i=1,\ldots,N_x-1$. At the boundaries we set $u=0$ and simulate just to
the point of time when the signal hits the boundary (and gets reflected).

$$
u^{n+1}_0 = u^{n+1}_{N_x} = 0\thinspace .
$$

The elements on the diagonal in the matrix become:

$$
A_{i,i} = 1,\quad i=0,\ldots,N_x\thinspace .
$$

On the subdiagonal and superdiagonal we have

$$
A_{i-1,i} = -\frac{\theta}{2} C,\quad A_{i+1,i} = \frac{\theta}{2} C,\quad i=1,\ldots,N_x-1,
$$

with $A_{0,1}=0$ and $A_{N_x-1,N_x}=0$ due to the known boundary conditions.
And finally, the right-hand side becomes

$$
\begin{align*}
b_0 &= u^n_{N_x}\\
b_i &= u^n_i - \frac{1-\theta}{2} C (u^{n}_{i+1} - u^{n}_{i-1}),\quad i=1,\ldots,N_x-1\\
b_{N_x} &= u^n_0
\end{align*}
$$

The dispersion relation follows from inserting $u^n_q = A^ne^{ikx}$
and using [this formula](../A_formulas/formulas.ipynb#form:exp:fd1c:center) for the spatial
differences:

$$
A = \frac{1 - (1-\theta) i C\sin p}{1 + \theta i C\sin p}\thinspace .
$$

Crank-Nicolson in time, centered in space, Gaussian profile, $C=0.8$, $\Delta t = 0.01$ (left) and $\Delta t=0.005$ (right).

Backward-Euler in time, centered in space, half a cosine profile, $C=0.8$, $\Delta t = 0.01$ (left) and $\Delta t=0.005$ (right).

```python
from IPython.display import HTML
_s = """
<div>

</div>
Crank-Nicolson in time, centered in space, $C=0.8$, $\Delta t = 0.005$. <!-- \label{advec:1D:CN:mov:C08:dt2} -->

<!-- Issue warning if in a Safari browser -->

"""
HTML(_s)
```

Crank-Nicolson in time, centered in space, $C=0.8$, $\Delta t = 0.005$.

```python
_s = """
<div>

</div>
Backward-Euler in time, centered in space, $C=0.8$, $\Delta t = 0.005$. <!-- \label{advec:1D:BE:mov:C08:dt2} -->

<!-- Issue warning if in a Safari browser -->

"""
HTML(_s)
```

Backward-Euler in time, centered in space, $C=0.8$, $\Delta t = 0.005$.

[This figure](#advec:1D:CN:fig:C08) depicts a numerical solution for $C=0.8$
and the Crank-Nicolson
with severe oscillations behind the main wave. These oscillations are
damped as the mesh is refined. Switching to the Backward Euler scheme
removes the oscillations, but the amplitude is
significantly reduced. One could expect that the discontinuous derivative
in the initial condition of the half a cosine wave would make even
stronger demands on producing a smooth profile, but [this figure](#advec:1D:BE:fig:C08) shows that also here, Backward-Euler is capable of producing a
smooth profile. All in all, there are no major differences between the
Gaussian initial condition and the half a cosine condition for any of
the schemes.

## The Lax-Wendroff method

The Lax-Wendroff method is based on three ideas:

1. Express the new unknown $u^{n+1}_i$ in terms of known
   quantities at $t=t_n$ by means of a Taylor polynomial of second degree.

2. Replace time-derivatives at $t=t_n$ by spatial derivatives,
   using the PDE.

3. Discretize the spatial derivatives by second-order differences so we
   achieve a scheme of accuracy $\mathcal{O}{\Delta t^2} + \mathcal{O}{\Delta x^2}$.

Let us follow the recipe. First we have the three-term Taylor polynomial,

$$
u^{n+1}_i = u^n_i + \Delta t\left(\frac{\partial u}{\partial t}\right)^n_i
+ \frac{1}{2}\Delta t^2\left(\frac{\partial^2 u}{\partial t^2}\right)^n_i\thinspace .
$$

From the PDE we have that temporal derivatives can be substituted by
spatial derivatives:

$$
\frac{\partial u}{\partial t} = -v\frac{\partial u}{\partial x},
$$

and furthermore,

$$
\frac{\partial ^2 u}{\partial t^2} = v^2\frac{\partial^2 u}{\partial x^2}\thinspace .
$$

Inserted in the Taylor polynomial formula, we get

$$
u^{n+1}_i = u^n_i -v \Delta t\left(\frac{\partial u}{\partial x}\right)^n_i
+ \frac{1}{2}\Delta t^2 v^2
\left(\frac{\partial^2 u}{\partial x^2}\right)^n_i\thinspace .
$$

To obtain second-order accuracy in space we now use central differences:

$$
u^{n+1}_i = u^n_i -v \Delta t [D_{2x} u]^n_i
+ \frac{1}{2}\Delta t^2 v^2 [D_xD_x u]^n_i,
$$

or written out,

$$
u^{n+1}_i = u^n_i - \frac{1}{2} C (u^{n}_{i+1} - u^{n}_{i-1})
+ \frac{1}{2} C^2 (u^{n}_{i+1}-2u^n_i+u^n_{i-1})\thinspace .
$$

This is the explicit Lax-Wendroff scheme.

**Lax-Wendroff works because of artificial viscosity.**

From the formulas above, we notice that the Lax-Wendroff method is nothing but
a Forward Euler, central difference in space scheme, which we have shown
to be useless because of chronic instability, plus an artificial
diffusion term of strength $\frac{1}{2}\Delta t v^2$. It means that we can take
an unstable scheme and add some diffusion to stabilize it. This is a common
trick to deal with advection problems. Sometimes, the real physical diffusion
is not sufficiently large to make schemes stable, so then we also add
artificial diffusion.

From an analysis similar to the ones carried out above, we get an
amplification factor for the Lax-Wendroff method that equals

$$
A = 1 - iC\sin p - 2C^2\sin^2 (p/2)\thinspace .
$$

This means that $|A|=1$ and also that we have an exact solution if $C=1$!

## Analysis of dispersion relations

We have developed expressions for $A(C,p)$ in the exact solution
$u_q^n=A^ne^{ikq\Delta x}$ of the discrete equations.
Note that the Fourier component that solves the original
PDE problem has no damping and moves with constant velocity $v$. There
are two basic errors in the numerical Fourier component: there may be
damping and the wave velocity may depend on $C$ and $p=k\Delta x$.

The shortest wavelength that can be represented is $\lambda = 2\Delta x$.
The corresponding $k$ is $k=2\pi/\lambda = \pi/\Delta x$, so $p=k\Delta x\in
(0,\pi]$.

Given a complex $A$ as a function of $C$ and $p$, how can we visualize
it? The two key ingredients in $A$ is the magnitude, reflecting damping or
growth of the wave, and the angle, closely related to the
velocity of the wave. The Fourier component

$$
D^n e^{ik(x-ct)}
$$

has damping $D$ and wave velocity $c$. Let us express our $A$ in
polar form, $A = A_re^{-i\phi}$, and insert this expression in
our discrete component $u_q^n = A^ne^{ikq\Delta x} = A^ne^{ikx}$:

$$
u^n_q = A_r^n e^{-i\phi n} e^{ikx} = A_r^n e^{i(kx - n\phi)} =
A_r^ne^{i(k(x - ct))},
$$

for

$$
c = \frac{\phi}{k\Delta t}\thinspace .
$$

Now,

$$
k\Delta t = \frac{Ck\Delta x}{v}=\frac{Cp}{v},
$$

so

$$
c = \frac{\phi v}{Cp}\thinspace .
$$

An appropriate dimensionless quantity to plot is the scaled wave velocity $c/v$:

$$
\frac{c}{v} = \frac{\phi}{Cp}\thinspace .
$$

Figures from [this](#advec:1D:disprel:C1:1) up to [this](#advec:1D:disprel:C05:2) contain
dispersion curves, velocity and damping, for various values of $C$.
The horizontal axis shows the dimensionless frequency $p$ of the wave,
while the figures to the left illustrate the error in wave velocity $c/v$
(should ideally be 1 for all $p$), and the figures to the right display
the absolute value (magnitude) of the damping factor $A_r$.
The curves are labeled according to the table below.

Label	                        Method

   FE       	    Forward Euler in time, centered difference in space

   LF       	    Leapfrog in time, centered difference in space

   UP       	    Forward Euler in time, upwind difference in space

   CN       	    Crank-Nicolson in time, centered difference in space

   LW       	    Lax-Wendroff's method

   BE       	    Backward Euler in time, centered difference in space

Dispersion relations for $C=1$.

Dispersion relations for $C=1$.

Dispersion relations for $C=0.8$.

Dispersion relations for $C=0.8$.

Dispersion relations for $C=0.5$.

Dispersion relations for $C=0.5$.

The total damping after some time $T=n\Delta t$ is reflected by
$A_r(C,p)^n$. Since normally $A_r<1$, the damping goes like
$A_r^{1/\Delta t}$ and approaches zero as $\Delta t\rightarrow 0$.
The only way to reduce damping is to increase $C$ and/or the mesh resolution.

We can learn a lot from the dispersion relation plots. For example,
looking at the plots for $C=1$, the schemes LW, UP, and LF has no
amplitude reduction, but LF has wrong phase velocity for the
shortest wave in the mesh. This wave does not (normally) have enough
amplitude to be seen, so for all practical purposes, there is no
damping or wrong velocity of the individual waves, so the total shape
of the wave is also correct. For the CN scheme, see [this figure](#advec:1D:CN:fig:C08), each individual wave has its amplitude, but
they move with different velocities, so after a while, we see some of
these waves lagging behind.  For the BE scheme, see [this figure](#advec:1D:BE:fig:C08), all the shorter waves are so heavily
dampened that we cannot see them after a while. We see only the
longest waves, which have slightly wrong velocity, but visible
amplitudes are sufficiently equal to produce what looks like a smooth
profile.

Another feature was that the Leapfrog method produced oscillations,
while the upwind scheme did not. Since the Leapfrog method does not
dampen the shorter waves, which have wrong wave velocities of order 10
percent, we can see these waves as noise. The upwind scheme, however,
dampens these waves. The same effect is also present in the Lax-Wendroff
scheme, but the damping of the intermediate waves is hardly present, so
there is visible noise in the total signal.

We realize that, compared to pure truncation error analysis, dispersion
analysis sheds more light on the behavior of the computational schemes.
Truncation analysis just says that Lax-Wendroff is
better than upwind, because of the increased order in time, but
most people would say upwind is the better one when looking at the plots.

# One-dimensional stationary advection-diffusion equation

Now we pay attention to a physical process where advection (or convection)
is in balance with diffusion:

$$
\begin{equation}
v\frac{du}{dx} = \alpha\frac{d^2 u}{dx^2}\thinspace .
\label{advec:1D:stat:pde1} \tag{11}
\end{equation}
$$

For simplicity, we assume $v$ and $\alpha$ to be constant, but the extension to
the variable-coefficient case is trivial.
This equation can be viewed as the stationary limit of the corresponding
time-dependent problem

$$
\begin{equation}
\frac{\partial u}{\partial t} + v\frac{\partial u}{\partial x} =
\alpha\frac{\partial^2 u}{\partial x^2}\thinspace .
\label{advec:1D:stat:pde2} \tag{12}
\end{equation}
$$

Equations of the form ([11](#advec:1D:stat:pde1)) or
([12](#advec:1D:stat:pde2)) arise from transport phenomena, either mass
or heat transport. One can also view the equations as a simple model
problem for the Navier-Stokes equations. With the chosen boundary
conditions, the differential equation problem models the phenomenon of
a *boundary layer*, where the solution changes rapidly very close to
the boundary. This is a characteristic of many fluid flow problems, which
makes strong demands to numerical methods. The fundamental numerical
difficulty is related to non-physical oscillations of the solution
(instability) if the first-derivative spatial term dominates over the
second-derivative term.

## A simple model problem

We consider ([11](#advec:1D:stat:pde1)) on $[0,L]$ equipped with the
boundary conditions $u(0)=U_0$, $u(L)=U_L$.  By scaling we can reduce the
number of parameters in the problem. We scale $x$ by $\bar x = x/L$,
and $u$ by

$$
\bar u = \frac{u - U_0}{U_L-U_0}\thinspace .
$$

Inserted in the governing equation we get

$$
\frac{v(U_L-U_0)}{L}\frac{d\bar u}{d\bar x} =
\frac{\alpha(U_L-U_0)}{L^2}\frac{d^2\bar u}{d\bar x^2},\quad
\bar u(0)=0,\ \bar u(1)=1\thinspace .
$$

Dropping the bars is common. We can then simplify to

$$
\begin{equation}
\frac{du}{dx} = \epsilon\frac{d^2 u}{d x^2},\quad u(0)=0,\ u(1)=1\thinspace .
\label{advec:1D:stat:pde1s} \tag{13}
\end{equation}
$$

There are two competing effects in this equation: the advection term
transports signals to the right, while the diffusion term transports
signals to the left and the right. The value $u(0)=0$ is transported
through the domain if $\epsilon$ is small, and $u\approx 0$ except in
the vicinity of $x=1$, where $u(1)=1$ and the diffusion transports
some information about $u(1)=1$ to the left. For large $\epsilon$,
diffusion dominates and the $u$ takes on the "average" value, i.e.,
$u$ gets a linear variation from 0 to 1 throughout the domain.

It turns out that we can find an exact solution to the differential
equation problem and also to many of its discretizations. This is one
reason why this model problem has been so successful in designing and
investigating numerical methods for mixed convection/advection and
diffusion.  The exact solution reads

$$
u_\text{e} (x) = \frac{e^{x/\epsilon} - 1}{e^{1/\epsilon} - 1}\thinspace .
$$

The forthcoming plots illustrate this function for various values of
$\epsilon$.

## A centered finite difference scheme

The most obvious idea to solve ([13](#advec:1D:stat:pde1s)) is to apply
centered differences:

$$
[D_{2x} u = \epsilon D_xD_x u]_i
$$

for $i=1,\ldots,N_x-1$, with $u_0=0$ and $u_{N_x}=1$.
Note that this is a coupled system of algebraic equations
involving $u_0,\ldots,u_{N_x}$.

Written out, the scheme becomes a tridiagonal system

$$
A_{i-1,i}u_{i-1} + A_{i,i}u_i + A_{i+1.i}u_{i+1} = 0,
$$

for $i=1,\ldots,N_x-1$

$$
\begin{align*}
A_{0,0} &= 1,\\
A_{i-1,i} &= -\frac{1}{\Delta x} -\epsilon\frac{1}{\Delta x^2},\\
A_{i,i} &= 2\epsilon\frac{1}{\Delta x^2},\\
A_{i,i+1} &=  \frac{1}{\Delta x} -\epsilon\frac{1}{\Delta x^2},\\
A_{N_x,N_x} &= 1\thinspace .
\end{align*}
$$

The right-hand side of the linear system is zero except $b_{N_x}=1$.

[This figure](#advec:1D:stationary:fdm:fig1) shows reasonably accurate
results with $N_x=20 $ and $N_x=40$ cells in $x$ direction and a value of
$\epsilon = 0.1$. Decreasing $\epsilon$ to $0.01$ leads to oscillatory
solutions as depicted in [this figure](#advec:1D:stationary:fdm:fig2).
This is, unfortunately, a typical phenomenon in this type of problem:
non-physical oscillations arise for small $\epsilon$ unless the resolution
$N_x$ is big enough. [Exercise 1: Analyze 1D stationary convection-diffusion problem](#advec:1D:stationary:exer:analysis1)
develops a precise criterion: $u$ is oscillation-free if

$$
\Delta x \leq \frac{2}{\epsilon}\thinspace .
$$

If we take the present model as a simplified model for a *viscous
boundary layer* in real, industrial fluid flow applications,
$\epsilon\sim 10^{-6}$
and millions of cells are required to resolve the boundary layer.
Fortunately, this is not strictly necessary as we have methods in
the next section to overcome the problem!

Comparison of exact and numerical solution for $\epsilon =0.1$ and $N_x=20,40$ with centered differences.

Comparison of exact and numerical solution for $\epsilon =0.01$ and $N_x=20,40$ with centered differences.

**Solver.**

A suitable solver for doing the experiments is presented below.

```python
# TODO: IMPLEMENT THIS IN DEVITO
import numpy as np

def solver(eps, Nx, method='centered'):
    """
    Solver for the two point boundary value problem u'=eps*u'',
    u(0)=0, u(1)=1.
    """
    x = np.linspace(0, 1, Nx+1)       # Mesh points in space
    # Make sure dx and dt are compatible with x and t
    dx = x[1] - x[0]
    u   = np.zeros(Nx+1)

    # Representation of sparse matrix and right-hand side
    diagonal = np.zeros(Nx+1)
    lower    = np.zeros(Nx)
    upper    = np.zeros(Nx)
    b        = np.zeros(Nx+1)

    # Precompute sparse matrix (scipy format)
    if method == 'centered':
        diagonal[:] = 2*eps/dx**2
        lower[:] = -1/dx - eps/dx**2
        upper[:] =  1/dx - eps/dx**2
    elif method == 'upwind':
        diagonal[:] = 1/dx + 2*eps/dx**2
        lower[:] =  1/dx - eps/dx**2
        upper[:] = - eps/dx**2

    # Insert boundary conditions
    upper[0] = 0
    lower[-1] = 0
    diagonal[0] = diagonal[-1] = 1
    b[-1] = 1.0

    # Set up sparse matrix and solve
    diags = [0, -1, 1]
    import scipy.sparse
    import scipy.sparse.linalg
    A = scipy.sparse.diags(
        diagonals=[diagonal, lower, upper],
        offsets=[0, -1, 1], shape=(Nx+1, Nx+1),
        format='csr')
    u[:] = scipy.sparse.linalg.spsolve(A, b)
    return u, x
```

## Remedy: upwind finite difference scheme

The scheme can be stabilized by letting the advective transport term, which
is the dominating term, collect its information in the flow direction, i.e.,
upstream or upwind of the point in question. So, instead of using a
centered difference

$$
\frac{du}{dx}_i\approx \frac{u_{i+1}-u_{i-1}}{2\Delta x},
$$

we use the one-sided *upwind* difference

$$
\frac{du}{dx}_i\approx \frac{u_{i}-u_{i-1}}{\Delta x},
$$

in case $v>0$. For $v<0$ we set

$$
\frac{du}{dx}_i\approx \frac{u_{i+1}-u_{i}}{\Delta x},
$$

On compact operator notation form, our upwind scheme can be expressed
as

$$
[D^-_x u = \epsilon D_xD_x u]_i
$$

provided $v>0$ (and $\epsilon > 0$).

We write out the equations and implement them as shown in the program
in the section [A centered finite difference scheme](#advec:1D:stationary:fdm). The results appear in [this figure](#advec:1D:stationary:upwind:fig1) and
[this figure](#advec:1D:stationary:upwind:fig2): no more oscillations!

Comparison of exact and numerical solution for $\epsilon =0.1$ and $N_x=20,40$ with upwind difference.

Comparison of exact and numerical solution for $\epsilon =0.01$ and $N_x=20,40$ with upwind difference.

We see that the upwind scheme is always stable, but it gives a thicker
boundary layer when the centered scheme is also stable.
Why the upwind scheme is always stable is easy to understand as
soon as we undertake the mathematical analysis in
[Exercise 1: Analyze 1D stationary convection-diffusion problem](#advec:1D:stationary:exer:analysis1).
Moreover, the thicker layer (seemingly larger diffusion) can be
understood by doing
[Exercise 2: Interpret upwind difference as artificial diffusion](#advec:1D:stationary:exer:analysis2).

**Exact solution for this model problem.**

It turns out that one can introduce a linear combination of the centered
and upwind differences for the first-derivative term in this model
problem. One can then adjust the weight in the linear combination so that
the numerical solution becomes identical to the analytical solution of
the differential equation problem at any mesh point.

# Time-dependent convection-diffusion equations

Now it is time to combine time-dependency, convection (advection) and
diffusion into one equation:

$$
\begin{equation}
\frac{\partial u}{\partial t} + v\frac{\partial u}{\partial x} =
\alpha\frac{\partial^2 u}{\partial x^2}\thinspace .
\label{advec:1D:stat:pde3} \tag{14}
\end{equation}
$$

### Analytical insight

The diffusion is now dominated by convection, a wave, and diffusion, a loss
of amplitude. One possible analytical solution is a traveling Gaussian
function

$$
u(x,t) = B\exp{\left(-\left(\frac{x - vt}{4at}\right)\right)}\thinspace .
$$

This function moves with velocity $v>0$ to the right
($v<0$ to the left) due to convection, but at the same time we have a damping
$e^{-16a^2t^2}$ from diffusion.

## Forward in time, centered in space scheme

The Forward Euler for the diffusion equation is a successful scheme, but it
has a very strict stability condition. The similar Forward in time, centered
in space strategy always gives unstable solutions for the advection PDE.
What happens when we have both diffusion and advection present at once?

$$
[D_t u + vD_{2x} u = \alpha D_xD_x u + f]_i^n\thinspace .
$$

We expect that diffusion will stabilize the scheme, but that advection will
destabilize it.

Another problem is non-physical oscillations, but not growing amplitudes,
due to centered differences in the advection term. There will hence be
two types of instabilities to consider.
Our analysis showed that pure advection with centered
differences in space needs some artificial diffusion to become stable
(and then it produces upwind differences for the advection term).
Adding more physical diffusion should further help the numerics to stabilize
the non-physical oscillations.

The scheme is quickly implemented, but suffers from the need for small
space and time steps, according to this reasoning. A better approach is
to get rid of the non-physical oscillations in space by simply applying
an upwind difference on the advection term.

## Forward in time, upwind in space scheme

A good approximation for the pure advection equation is to use upwind
discretization of the advection term. We also know that centered differences
are good for the diffusion term, so let us combine these two discretizations:

$$
\begin{equation}
[D_t u + vD^-_{x} u = \alpha D_xD_x u + f]_i^n,
\label{_auto2} \tag{15}
\end{equation}
$$

for $v>0$. Use $vD^+ u$ if $v<0$.
In this case the physical diffusion and the extra numerical diffusion
$v\Delta x/2$ will stabilize the solution, but give an overall too large
reduction in amplitude compared with the exact solution.

We may also interpret the upwind difference as artificial numerical diffusion
and centered differences in space everywhere, so the scheme can be expressed as

$$
\begin{equation}
[D_t u + vD^-_{2x} u = \alpha \frac{v\Delta x}{2} D_xD_x u + f]_i^n\thinspace .
\label{_auto3} \tag{16}
\end{equation}
$$

# Applications of advection equations

There are two major areas where advection and convection applications arise:
transport of a substance and heat transport *in a fluid*.
To derive the models, we may look at the similar derivations of
diffusion models in the [Applications](../03_diffu/diffu_app.ipynb) section of the Diffusion equations chapter,
but change the assumption from a solid to fluid medium.
This gives rise to the extra advection or convection term $\boldsymbol{v}\cdot\nabla u$.
We briefly show how this is done.

Normally, transport in a fluid is dominated by the fluid flow and not
diffusion, so we can neglect diffusion compared to advection or convection.
The end result is anyway an equation of the form

$$
\frac{\partial u}{\partial t} + \boldsymbol{v}\cdot\nabla u = 0\thinspace .
$$

## Transport of a substance

The diffusion of a substance in the section [Diffusion of a substance](../03_diffu/diffu_app.ipynb#diffu:app:substance) takes place
in a solid medium, but in a fluid we can have two transport mechanisms:
one by diffusion and one by advection.
The latter arises from the fact
that the substance particles are moved with the fluid velocity $\boldsymbol{v}$ such that
the effective flux now consists of two and not only one component as in
[this equation](../03_diffu/diffu_app.ipynb#diffu:app:substance:Fick):

$$
\boldsymbol{q} = -\alpha\nabla c + \boldsymbol{v}_\xi\thinspace .
$$

Inserted in the equation $\nabla\cdot\boldsymbol{q} = 0$ we get the extra advection
term $\nabla\cdot (\boldsymbol{v}_\xi)$. Very often we deal with incompressible flows,
$\nabla\cdot\boldsymbol{v} = 0$ such that the advective term becomes $\boldsymbol{v}\cdot\nabla c$.
The mass transport equation for a substance then reads

$$
\begin{equation}
\frac{\partial c}{\partial t} + \boldsymbol{v}\cdot\nabla c = \alpha\nabla^2 c\thinspace .
\label{_auto4} \tag{17}
\end{equation}
$$

## Transport of heat in fluids

The derivation of the heat equation in the section [Heat conduction](../03_diffu/diffu_app.ipynb#diffu:app:heat) is limited
to heat transport in solid bodies. If we turn the attention to heat transport
in fluids, we get a material derivative of the internal energy in
[this equation](../03_diffu/diffu_app.ipynb#diffu:app:heat:PDE1),

$$
\frac{De}{dt} = - \nabla\cdot\boldsymbol{q},
$$

and more terms if work by stresses is also included, where

$$
\frac{De}{dt} = \frac{\partial e}{\partial t} + \boldsymbol{v}\cdot\nabla e,
$$

$\boldsymbol{v}$ being the velocity of the fluid. The convective term
$\boldsymbol{v}\cdot\nabla e$ must therefore be added to the governing equation,
resulting typically in

$$
\begin{equation}
\varrho c\left(\frac{\partial T}{\partial t} + \boldsymbol{v}\cdot\nabla T\right)
= \nabla\cdot(k\nabla T) + f,
\label{advec:app:heat:PDE} \tag{18}
\end{equation}
$$

where $f$ is some external heating inside the medium.

# Exercises

## Exercise 1: Analyze 1D stationary convection-diffusion problem

Explain the observations in the numerical experiments from the sections [A centered finite difference scheme](#advec:1D:stationary:fdm) and [Remedy: upwind finite difference scheme](#advec:1D:stationary:upwind) by
finding exact numerical solutions.

**Hint.**
The difference equations allow solutions on the form $A^i$, where
$A$ is an unknown constant and $i$ is a mesh point counter.
There are two solutions for $A$, so the general solution is a linear
combination of the two, where the constants in the linear combination
are determined from the boundary conditions.

Filename: `twopt_BVP_analysis1`.

## Exercise 2: Interpret upwind difference as artificial diffusion

Consider an upwind, one-sided difference approximation to
a term $du/dx$ in a differential equation. Show that this
formula can be expressed as a centered difference plus an artificial
diffusion term of strength proportional to $\Delta x$.
This means that introducing an upwind difference also means introducing
extra diffusion of order $\mathcal{O}{\Delta x}$.
Filename: `twopt_BVP_analysis2`.

# Bibliography

```python

```


# Creating seismic synthetics with Devito and GemPy
In this tutorial, we aim to provide a start-to-finish guide for creating synthetic seismic models and datasets using the open-source packages Devito and GemPy. We will highlight the steps involved, from creating a 3D geological model, to forward modelling, to visualisation.

## Goals
* Introduce users to creating simple geological models with GemPy
* Illustrate how to include physical properties in the geological model and how to convert this into a format usable for seismic modelling with Devito
* Set up a Devito `operator` to solve the 2nd order acoustic wave equation, including sparse sources and receivers
* Forward propagate a shot through the model, and record a synthetic gather using receivers
* Visualise the wavefield in 3D using slicing in PyVista

## Structure
This tutorial will be presented in three sections. The first section will focus on creating the seismic synthetic, the second on model setup and the Devito API, and the latter will cover running the seismic wave model and visualising the wavefield.

Throughout the tutorial, several members of the team will be available in the `#t21-thurs-devito` channel of the [Software Underground Slack](https://softwareunderground.org/slack). For in-depth GemPy questions, the `#gempy` channel is another option. For Devito-centric questions, try the `#devito` channel or the [Devito Slack](https://devitocodes.slack.com/).

## Motivation
Synthetic seismic models are crucial for testing and benchmarking numerous seismic modelling and imaging applications. Whilst several seismic synthetics  are commonplace within the community for example the ubiquitous Marmousi model, or the SEG/EAGE Salt and Overthrust Models, the range of freely-available synthetics is relatively small, with other purpose-built models carrying a steep cost. Furthermore currently-available seismic synthetics may not bear sufficient resemblence to the desired target, or may be lacking certain material properties required for a particular wave equation. This has implications for both accessibility and reproducibility of studies, as benchmarks used may not be readily available for others to verify the work.

By using freely-available tools to create synthetic models and datasets, we can create open, sharable, customisable models for a wide range of applications.

## Side Note (Google Colab only)

If running on Google Colab, you will need to run the following cells to prevent 3D renders crashing your notebook.

```python
"""
!apt-get update
!apt-get -qq install xvfb
!pip install pyvirtualdisplay
"""
```

```python
"""
from pyvirtualdisplay import Display
display = Display(visible=0, size=(600, 400))
display.start()
"""
```

## GemPy installation

[Source](https://docs.gempy.org/examples/real/Greenstone.html#sphx-glr-download-examples-real-greenstone-py): An example of a detailed geological model, taken from the Greenstone GemPy example.

The synthetics which we will be building in this tutorial will be made with the use of GemPy, an open-source 3D geological modelling package for Python. If not already installed, we will need to install it. If issues are encountered whilst installing GemPy into a `conda` environment using `pip`, you can alternatively create a python `venv` and install into this environment using `pip` as per usual. Note that it will also be necesary to install an `ipykernel` in this environment to run this notebook. From here, we can install GemPy:

```python
try:
    # Import gempy
    import gempy as gp
except ModuleNotFoundError:
    # Install gempy
    ! pip install gempy
    # Import gempy
    import gempy as gp

# Check jinja2 is installed for colour-coding of geological units
try:
    import jinja2
except ModuleNotFoundError:
    ! pip install jinja2
    import jinja2
```

    No module named 'osgeo'

## Model overview

The simple geological model which we will be building is designed to evoke carbon-capture and storage (CCS) scenarios.

The model consists of a CO2 lens in a sandstone reservoir, with a shale layer in the overarching anticline providing the structural trap. This is then overlain by a layer of sediment, with water at the top of the model. Geological strata and their respective velocities are based on values detailed in [Queißer et al. 2013](https://doi.org/10.1190/geo2012-0216.1), a paper imaging the P-wave velocity anomaly generated by CO2 injection into the Utsira Sand at Sleipnir in the North Sea using FWI. The model we will create features a similar shale trap/permeable sandstone reservoir structure, albeit with a small number of thick layers rather than the thin interbedding, to limit model complexity for this tutorial. Further inspiration was taken from [Chadwick et al. 2004](https://doi.org/10.1016/j.energy.2004.03.071), a paper characterizing the Utsira Sand reservoir based on 2D seismic lines and well logs.

## Creating our geological model:

To begin, alongside GemPy, we need to import some  auxiliary modules:

```python
# Import auxiliary modules
import numpy as np

%matplotlib inline
```

### Initial setup

We will now set up a GemPy `Model` object. This encapsulates the grid onto which the scalar fields associated with various surfaces are interpolated. Note that the extent is slightly greater than it will be for our Devito model (an extra half a grid spacing is added to each side).

A comparison of the cell-centered vs node-centered conventions of GemPy and Devito respectively, along with the differences in how they measure extent. It is necessary to account for this to ensure that the two grids are co-located.

As we can see in the figure above, this is due to differences in the way in which grids are defined in each package and is necessary to ensure that the model is not stretched and distorted when transistioning between the two, and that they are correctly aligned.

```python
# Set overarching model parameters
extent = (-5., 1005., -5., 1005., -1005., 5.)
shape = (101, 101, 101)

geo_model = gp.create_model('transform-2021')
geo_model = gp.init_data(geo_model, extent=extent, resolution=shape)
```

    Active grids: ['regular']

We now need to set up Theano for our model (used by GemPy for interpolation of model properties). Bear in mind that that this may take some time to run.

```python
gp.set_interpolator(geo_model, output=['geology'], theano_optimizer='fast_compile')
```

    Setting kriging parameters to their default values.
    Compiling theano function...
    Level of Optimization:  fast_compile
    Device:  cpu
    Precision:  float64
    Number of faults:  0
    Compilation Done!
    Kriging values:
                            values
    range             1749.371316
    $C_o$            72864.285714
    drift equations           [3]

    <gempy.core.interpolator.InterpolatorModel at 0x7fc28874a110>

### Adding geological units
As the top CO2 surface is truncated by the upper shale layer, we will need to separate the geological strata into two GemPy `Series`. Each `Series` object, as the name suggests is intended to correspond with a geological unit, and they can be made to onlap, erode, etc one another. Whilst in practice, the top CO2 contact is not an erosive surface, treating it as such is the most straightforward way to create the desired truncation, as the shale trap is unconformable on the CO2 lens and reservoir sandstone.

A default series is included in the model. As such, rather than creating a new series, we will simply rename it to 'Lower'. As you can imagine, this is going to be used to contain the lower geological units, these being the lower shale, reservoir sandstone, and CO2 lens.

```python
geo_model.rename_series({'Default series': 'Lower'})
```

And now add our surfaces:

```python
geo_model.add_surfaces(['co2', 'sands', 'lowershale'])
```

        	        surface	        series	        order_surfaces	        color	        id

                        0
                        co2
                        Lower
                        1
                        #015482
                        1

                        1
                        sands
                        Lower
                        2
                        #9f0052
                        2

                        2
                        lowershale
                        Lower
                        3
                        #ffbe00
                        3

We will now set some points for the base of the sands and CO2. The lower shale is considered the basement, meaning that its base does not need to be defined and it will extend to the bottom of the model. Alongside these points, we wil need to define an orientation for the surface.

To minimise repetition, we will define a function to loop over a list of points and add each to the surface.

```python
def create_surface(model, points, surface):
    """Add a list of points to a surface in a model"""
    xyz = ('X', 'Y', 'Z')
    for point in points:
        kwargs = {**dict(zip(xyz, point)), 'surface': surface}
        model.add_surface_points(**kwargs)

# The points defining the base of the sand layer
sand_points = [(322, 135, -783), (635, 702, -791), (221, 668, -772), (732, 235, -801), (442, 454, -702)]

# Call our function
create_surface(geo_model, sand_points, 'sands')

# Add the surface orientation
geo_model.add_orientations(X=442., Y=495., Z=-752.,
                           surface='sands', pole_vector=(0.05, 0.05, 0.95))
```

      X
      Y
      Z
      G_x
      G_y
      G_z
      smooth
      surface

      0
      442.0
      495.0
      -752.0
      0.05
      0.05
      0.95
      0.01
      sands

We will now repeat this process for the CO2 lens.

```python
# Points defining the base of the CO2 layer
co2_points = [(322, 135, -650), (635, 702, -650), (221, 668, -650), (732, 235, -650), (442, 454, -650)]

create_surface(geo_model, co2_points, 'co2')

# Add the surface orientation
geo_model.add_orientations(X=495., Y=495., Z=-650.,
                           surface='co2', pole_vector=(0., 0., 1.))
```

      X
      Y
      Z
      G_x
      G_y
      G_z
      smooth
      surface

      1
      495.0
      495.0
      -650.0
      0.00
      0.00
      1.00
      0.01
      co2

      0
      442.0
      495.0
      -752.0
      0.05
      0.05
      0.95
      0.01
      sands

We will now add an upper series, containing statigraphy above the CO2 lens.

```python
geo_model.add_series('Upper')
```

      order_series
      BottomRelation
      isActive
      isFault
      isFinite

      Lower
      1
      Erosion
      True
      False
      False

      Upper
      2
      Erosion
      False
      False
      False

As we can see, the upper series has been added below the lower series. This is not ideal for obvious reasons, and hence we will reorder them:

```python
geo_model.reorder_series(['Upper', 'Lower'])
```

      order_series
      BottomRelation
      isActive
      isFault
      isFinite

      Upper
      1
      Erosion
      False
      False
      False

      Lower
      2
      Erosion
      True
      False
      False

And add our remaining surfaces:

```python
geo_model.add_surfaces(['water', 'sediments', 'uppershale'])
```

        	        surface	        series	        order_surfaces	        color	        id

                        0
                        co2
                        Lower
                        1
                        #015482
                        1

                        1
                        sands
                        Lower
                        2
                        #9f0052
                        2

                        2
                        lowershale
                        Lower
                        3
                        #ffbe00
                        3

                        3
                        water
                        Lower
                        4
                        #728f02
                        4

                        4
                        sediments
                        Lower
                        5
                        #443988
                        5

                        5
                        uppershale
                        Lower
                        6
                        #ff3f20
                        6

As these surfaces are not mapped to the upper series by default, we shall do so:

```python
gp.map_stack_to_surfaces(geo_model, {'Upper': ('water', 'sediments', 'uppershale')})
```

        	        surface	        series	        order_surfaces	        color	        id

                        3
                        water
                        Upper
                        1
                        #728f02
                        1

                        4
                        sediments
                        Upper
                        2
                        #443988
                        2

                        5
                        uppershale
                        Upper
                        3
                        #ff3f20
                        3

                        0
                        co2
                        Lower
                        1
                        #015482
                        4

                        1
                        sands
                        Lower
                        2
                        #9f0052
                        5

                        2
                        lowershale
                        Lower
                        3
                        #ffbe00
                        6

Now we will add the points for the upper series. Note that there is only a single orientation included. It is not necessary to define an orientation for each surface, so long as there is an orientation in the series.

```python
# Surface points
uppershale_points = [(322, 135, -633), (635, 702, -641), (221, 668, -622), (732, 235, -651), (442, 454, -552)]
sediments_points = [(322, 135, -433), (635, 702, -441), (221, 668, -422), (732, 235, -451), (442, 454, -352)]
water_points = [(232, 153, -221), (653, 234, -216), (112, 872, -198), (532, 572, -223),
                (722, 884, -189), (632, 429, -201), (732, 348, -222)]

# Add the points to our surfaces
create_surface(geo_model, uppershale_points, 'uppershale')
create_surface(geo_model, sediments_points, 'sediments')
create_surface(geo_model, water_points, 'water')

# Set an orientation
geo_model.add_orientations(X=442., Y=495., Z=-502.,
                           surface='uppershale', pole_vector=(0.05, 0.05, 0.95))
```

      X
      Y
      Z
      G_x
      G_y
      G_z
      smooth
      surface

      2
      442.0
      495.0
      -502.0
      0.05
      0.05
      0.95
      0.01
      uppershale

      1
      495.0
      495.0
      -650.0
      0.00
      0.00
      1.00
      0.01
      co2

      0
      442.0
      495.0
      -752.0
      0.05
      0.05
      0.95
      0.01
      sands

### Adding physical properties (Vp)

Finally, we can add the p-wave velocities associated with each of these layers. Note that any parameter can be set in this manner (density, elastic parameters, attenuation, etc) if desired for more complex synthetics. This is one of the benefits of using GemPy to create these synthetics: models can be expanded upon with a huge suite of material properties.

```python
geo_model.add_surface_values([1.5, 1.75, 2.5, 1.1, 2., 2.5], ['vp'])
geo_model.surfaces
```

        	        surface	        series	        order_surfaces	        color	        id	        vp

                        3
                        water
                        Upper
                        1
                        #728f02
                        1
                        1.500000

                        4
                        sediments
                        Upper
                        2
                        #443988
                        2
                        1.750000

                        5
                        uppershale
                        Upper
                        3
                        #ff3f20
                        3
                        2.500000

                        0
                        co2
                        Lower
                        1
                        #015482
                        4
                        1.100000

                        1
                        sands
                        Lower
                        2
                        #9f0052
                        5
                        2.000000

                        2
                        lowershale
                        Lower
                        3
                        #ffbe00
                        6
                        2.500000

### Visualising the GemPy model

Now we can visualise our model, plotting data points and orientations. Firstly, the model must be computed to interpolate the surfaces and any associated scalar fields. Then we can plot our surfaces and the associated units.

```python
# Set up plotter
p3d = gp.plot_3d(geo_model, notebook=True)
# Plot data points and orientations
p3d.plot_data()

# Compute the model. Note that a solution is returned. We will use this later
sol = gp.compute_model(geo_model)

# Plot the surfaces
p3d.plot_surfaces()
# Plot the lithological units
p3d.plot_structured_grid('lith')
```

## Bridging the gap from GemPy to Devito:

As you may have noticed, when we compute our GemPy model, a `Solution` object is returned. From this, we can extract the rasterized values attached to each of our geological units. With this in mind, we can print the solution values:

```python
sol.values_matrix
```

    array([[2.5       , 2.44720385, 2.        , ..., 1.5       , 1.5       ,
            1.5       ]])

You will notice that these values correspond with the p-wave velocities we specified. However, they are in the form of 1D vector. Consequently, will need to reshape this array to fit into the `vp` parameter of a Devito `Model`. This can be done with further parameters such as density or shear wave velocity for more complex models. In this case, you would want to set up a Devito `Function` to contain each parameter.

Note that in this case, we need to select c-like index order to get the axes in the correct order.

```python
# Reshaping our data to the shape required by Devito
reshaped = np.reshape(sol.values_matrix, shape, order='C')
reshaped.shape
```

    (101, 101, 101)

### Quality checking

Now let us plot a slice through this model for quality checking purposes. Note that we need to bear Devito's `[x, y, z]` indexing convention in mind.

```python
import matplotlib.pyplot as plt

# Take the center slice in the x direction
# Remember that in Devito, indexing convention is [x, y, z] (need to flip for correct imshow display)
plt.imshow(reshaped[50].T, cmap='viridis', origin='lower')
plt.colorbar()
plt.show()
```

The model looks good, and we can see the geometry of the CO2 lens clearly. We are now ready to begin our seismic modelling with Devito.

## Break Time!
Time to go and get a coffee.

## Seismic modelling with Devito

We can now start building our Devito model. The following draws heavily from the Devito `examples/sesimic/tutorials/01_modelling.ipynb` notebook (see [here](https://github.com/devitocodes/devito/blob/master/examples/seismic/tutorials/01_modelling.ipynb)). We will begin, as always with some imports. If Devito is not installed, we will need to install it.

We will also be drawing on some convenient helper functions for "toy" seismic models from `examples.seismic`. Whilst these are not part of core Devito and probably shouldn't be used in practical implementations, they are helpful for the purposes of learning the workflow.

```python
try:
    # Import devito
    import devito as dv
    from examples.seismic import Model
except ModuleNotFoundError:
    # Install newest SymPy version
    ! pip install -Iv sympy==1.8
    # Install devito
    ! pip install devito
    # Import devito
    import devito as dv
    from examples.seismic import Model
```

### Modelling workflow
The core process we are aiming to model is a seismic survey, which consists of two main components:
* **Source** - A source is positioned at a single or a few physical locations where artificial pressure is injected into the domain we want to model. In the case of land survey, it is usually dynamite, or a vibroseis (a truck fitted with a vibrating plate generating continuous sound waves). For a marine survey, the source is an air gun sending a bubble of compressed air into the water that will expand and generate a seismic wave.
* **Receivers** - A set of geophones or hydrophones are used to measure the resulting wave and create a set of measurements called a "Shot Record" or "Shot Gather". These measurements are recorded across an array, usually located near the surface, although they may be a depth in the case of ocean-bottom seismometers (OBS) or downhole receivers.

In order to create a numerical model of a seismic survey, we need to solve a wave equation and implement source and receiver interpolation to inject the source and record the seismic wave at sparse point locations in the grid.

[Source](https://upload.wikimedia.org/wikipedia/commons/0/01/Diagram_of_a_marine_seismic_survey.png) A diagram of a marine seismic survey.

### The acoustic wave equation
The acoustic wave equation for the square slowness $m$, defined as $m=\frac{1}{c^2}$, where $c$ is the speed of sound in the given physical media, and a source $q$ is given by:

\begin{cases}
 &m \frac{d^2 u(x,t)}{dt^2} - \nabla^2 u(x,t) = q \ \text{in } \Omega \\
 &u(.,t=0) = 0 \\
 &\frac{d u(x,t)}{dt}|_{t=0} = 0
\end{cases}

with the zero initial conditions to guarantee unicity of the solution.
The boundary conditions are Dirichlet conditions:
\begin{equation}
 u(x,t)|_\delta\Omega = 0
\end{equation}

where $\delta\Omega$ is the surface of the boundary of the model $\Omega$.

### Finite domains

The last piece of the puzzle is the computational limitation. In the field, the seismic wave propagates in every direction to an "infinite" distance. However, solving the wave equation in a mathematically/discrete infinite domain is not feasible. In order to compensate, Absorbing Boundary Conditions (ABC) or Perfectly Matched Layers (PML) are required to mimic an infinite domain. These two methods allow to approximate an infinite media by damping and absorbing the waves at the limit of the domain to avoid reflections.

The simplest of these methods is the absorbing boundary region. The core idea is to extend the physical domain and to add an absorbing region in this extension that will absorb the incident waves. The acoustic wave equation with this damping region can be rewritten as:

\begin{cases}
 &m \frac{\partial^2 u(\textbf{x},t)}{\partial t^2} - \nabla^2 u(\textbf{x},t) + \eta \frac{\partial u(\textbf{x},t)}{\partial t}=q  \ \text{in } \Omega \\
 &u(.,0) = 0 \\
 &\frac{\partial u(\textbf{x},t)}{\partial t}|_{t=0} = 0
\end{cases}

where $\eta$ is the damping coefficient equal to $0$ inside the physical domain and increasing inside the absorbing region. Multiple choices of profile can be chosen for $\eta$ from linear to exponential.

### Defining the physical problem

The first step is to define the physical model:

* What are the physical dimensions of interest?
* What is the velocity profile of this physical domain?

We will use the velocity velocity model which we have just created using GemPy. As such we need to create a physical domain of the same size. As mentioned earlier, Devito and GemPy have slightly different grid implementations, which we will need to bear in mind to ensure our Devito model maps correctly to the GemPy grid.

We can now construct a Devito `Model`. This is a convenience object encapsulating the necessary parameters and components of an acoustic wave model, including additional damping layers around the perimeter (specified by `bcs="damp"`). For implementing custom damping setups, see `examples/userapi/04_boundary_conditions.ipynb` in the Devito repository. Note that we are using a relatively large number of damping layers here. This is to avoid our gathers becoming too messy, and ensure that reflections from horizons can be straightforwardly identified in the gathers. These damping layers are added to the edge of the specified model, increasing its extent (this is important for later visualisation).

```python
seis_model = Model(vp=reshaped, origin=(0., 0., -1000.), spacing=(10., 10., 10.), shape=shape, nbl=30, space_order=4, bcs="damp")
```

    Operator `initdamp` run in 0.05 s
    Operator `pad_vp` run in 0.01 s

### Acquisition geometry
To fully define our problem setup we also need to define the source that injects the wave to model and the set of receiver locations at which to sample the wavefield. The source time signature will be modelled using a Ricker wavelet defined as

\begin{equation}
  q(t) = (1-2\pi^2 f_0^2 (t - \frac{1}{f_0})^2 )e^{- \pi^2 f_0^2 (t - \frac{1}{f_0})}
\end{equation}

To fully define the source signature we first need to define the time duration for our model and the timestep size, which is dictated by the CFL condition and our grid spacing. Luckily, our `Model` utility provides us with the critical timestep size, so we can fully discretize our model time axis as an array:

```python
from examples.seismic import TimeAxis

t0 = 0.  # Simulation starts at t=0
tn = 1000.  # Simulation last 1 second (1000 ms)
dt = seis_model.critical_dt  # Time step from model grid spacing

time_range = TimeAxis(start=t0, stop=tn, step=dt)
```

We will position our source at a depth of 20m, center it in all other axes, and set the peak wavelet frequency to 15Hz.

```python
from examples.seismic import RickerSource

f0 = 0.015  # Source peak frequency is 15Hz (0.015 kHz)
src = RickerSource(name='src', grid=seis_model.grid, f0=f0,
                   npoint=1, time_range=time_range)

# First, position source centrally in all dimensions, then set depth
src.coordinates.data[:] = np.array(seis_model.domain_size) * .5
src.coordinates.data[0, -1] = -20  # Depth is 20m

# We can plot the time signature to see the wavelet
src.show()
```

Similarly to our source object, we can now define our receiver geometry as a symbol of type `Receiver`. It is worth noting here that both utility classes, `RickerSource` and `Receiver` are thin wrappers around the Devito's `SparseTimeFunction` type, which encapsulates sparse point data and allows us to inject and interpolate values into and out of the computational grid. As we have already seen, both types provide a `.coordinates` property to define the position within the domain of all points encapsulated by that symbol.

In this example we will position receivers at the same depth as the source, every $10m$ along the x axis, on the midline of the y axis. The `rec.data` property will be initialized, but left empty, as we will compute the receiver readings during the simulation.

```python
from examples.seismic import Receiver

# Create symbol for 101 receivers
rec = Receiver(name='rec', grid=seis_model.grid, npoint=101, time_range=time_range)

# Prescribe even spacing for receivers along the x-axis
rec.coordinates.data[:, 0] = np.linspace(0, seis_model.domain_size[0], num=101)
rec.coordinates.data[:, 1] = 0.5*seis_model.domain_size[1]
rec.coordinates.data[:, -1] = -20.  # Depth is 20m
```

### Wave equation and specification

As mentioned previously, we will be using the 2nd order acoustic wave equation in this tutorial. One of the key features of Devito is the ability to specify an equation, or system of equation, in a symbolic manner, and have this automatically discretized using the finite difference method.

In Devito, variables which vary in space only are represented using `Function` objects. If we also want them to vary over time, we must use a `TimeFunction`. There are further `Function`s for vector and tensor symbols: useful for concisely specifying the elastic wave equation for example. Each `Function` has a `.data` attribute, which contains the discretized values of the field. The FD discretization is specified using the `time_order` and `space_order` keyword arguments. In this case we will use a discretization with 2nd order accuracy in time, and 4th order accuracy in space.

We will create a function $u(t, x, y, z)$ to contain our pressure field. To do this, we need to provide a name (used when printing the function, or symbolic expressions which contain it), and a Devito `Grid` to which it should be attached (in this case, the grid included in the `Model` convenience object). Additional information about the discretization including time order, space order, and staggering can also be supplied.

```python
# Define the wavefield with the size of the model
u = dv.TimeFunction(name="u", grid=seis_model.grid, time_order=2, space_order=4)
u
```

$\displaystyle u{\left(t,x,y,z \right)}$

We can also view the data attached to this `Function` as below. When the object is created, this is initialised to zero.

```python
u.data.shape
```

    (3, 161, 161, 161)

There are also various convenient aliases contained within the `Function` object.

```python
u.shape == u.data.shape
```

    True

Since we set `time_order=2` when creating the function, Devito has allocated three buffers to represent `u(t-1, x, y, z)`, `u(t, x, y, z)`, and `u(t+1, x, y, z)`, hence the size of the time dimension being 3. A larger buffer can be specified when creating the `TimeFunction` allowing all timesteps to be saved, but we have not done so in this case.

The `data` attribute is a Numpy array, and can be interacted with like any other.

```python
isinstance(u.data, np.ndarray)
```

    True

To take the first of a variable `u` with respect to a dimension `x`, you can simply write `u.dx`, or `u.dx2` for the second derivative. For other dimensions (including time), simply swap out the `x`.

```python
# Show the derivative stencil
u.dx2.evaluate
```

$\displaystyle - \frac{2.5 u{\left(t,x,y,z \right)}}{h_{x}^{2}} - \frac{0.0833333333 u{\left(t,x - 2 h_{x},y,z \right)}}{h_{x}^{2}} + \frac{1.33333333 u{\left(t,x - h_{x},y,z \right)}}{h_{x}^{2}} + \frac{1.33333333 u{\left(t,x + h_{x},y,z \right)}}{h_{x}^{2}} - \frac{0.0833333333 u{\left(t,x + 2 h_{x},y,z \right)}}{h_{x}^{2}}$

### A detour into Finite Differences

This next section is included for completeness, explaining the derivation of finite difference stencils for given derivatives, and explicit timestepping schemes. Whilst an overview of the standard Taylor-series-based stencils is given, Devito also supports the specification of custom stencil coefficients necessary for various dispersion-relation preserving schemes. A tutorial for this can be found [here](https://github.com/devitocodes/devito/blob/master/examples/seismic/tutorials/07_DRP_schemes.ipynb). Note that there is also support for spatially-variant coefficients, either based on subdomains or over the full domain.

To understand where this stencil comes from, lets have a quick overview of finite differences. Consider a function $f(x,t)$. Recall that the Taylor series of $f(x,t)$ in the spatial dimension takes the form

\begin{equation*}
 f(x+h,t)=f(x,t)+\frac{\partial f}{\partial x}h+\frac{1}{2}\frac{\partial^2 f}{\partial x^2}h^2+\frac{1}{3!}\frac{\partial^3 f}{\partial x^3}h^3+\frac{1}{4!}\frac{\partial^4 f}{\partial x^4}h^4+\ldots.
\end{equation*}

We can re-arrange the above expansion in the form

\begin{equation*}
 \frac{\partial f}{\partial x}=\frac{f(x+h,t)-f(x,t)}{h}-\frac{1}{h}\sum_{n=2}^{\infty}\frac{1}{n!}\frac{\partial^n f}{\partial x^n}h^n.
\end{equation*}

Thus, provided $h$ is small we can say

\begin{equation*}
 \frac{\partial f}{\partial x}\approx\frac{f(x+h,t)-f(x,t)}{h},
\end{equation*}

which will have an associated error

\begin{equation*}
-\frac{1}{h}\sum_{n=2}^{\infty}\frac{1}{n!}\frac{\partial^n f}{\partial x^n}h^n
\end{equation*}

In comparison to our result (again, provided $h$ is small) the associated error will therefore be proportional to $h^2$ (if $h$ is small, $h^2$ will be very small and $h^3$ will be tiny). It is common to write this as

\begin{equation*}
 \frac{\partial f}{\partial x}=\frac{f(x+h,t)-f(x,t)+\mathcal{O}(h^2)}{h}.
\end{equation*}

This is the well known *forward difference* approximation and is how spatial derivatives are approximated in 'space order' 1 finite difference schemes.

We can also write the following Taylor expansion

\begin{equation*}
 f(x-h,t)=f(x,t)-\frac{\partial f}{\partial x}h+\frac{1}{2}\frac{\partial^2 f}{\partial x^2}h^2-\frac{1}{3!}\frac{\partial^3 f}{\partial x^3}h^3+\frac{1}{4!}\frac{\partial^4 f}{\partial x^4}h^4+\ldots.
\end{equation*}

This leads to the backward difference approximation which is also 'first order accurate' (i.e. the error is proportional to $h^2$)

\begin{equation*}
 \frac{\partial f}{\partial x}\approx\frac{f(x,t)-f(x-h,t)}{h}.
\end{equation*}

Note now that if we look at $f(x+h)-f(x-h)$ and re-arrange we arrive at

\begin{equation*}
 \frac{\partial f}{\partial x}=\frac{f(x+h,t)-f(x-h,t)+\mathcal{O}(h^3)}{2h},
\end{equation*}

which is known as the centered difference approximation. Notice that now the $h^2$ terms cancel and the error is 'pushed' back to be of order $h^3$. That is, this approximation is (in theory) more accurate.

We can continue this process by considering more and more expansions and solving the resulting set of 'simultaneous equations' for $\partial f/\partial x$ to create higher and higher order approximations. For example, if we consider the expansions $f(x+2h,t)$ and $f(x-2h,t)$ we arrive at a 'fourth order' accurate scheme of the form

\begin{equation*}
 \frac{\partial f}{\partial x}=\frac{\frac{1}{12}f(x-2h,t)-\frac{2}{3}f(x-h,t)+\frac{2}{3}f(x+h,t)-\frac{1}{12}f(x+2h,t)+\mathcal{O}(h^5)}{h}.
\end{equation*}

Notice that as we increase the accuracy of our approximation the **stencil** becomes larger (and hence computations with higher order stencils become more demanding).

Through this process of manipulating Taylor series we can also derive approximations for other derivatives. For example, summing the series expansions $f(x+h,t)+f(x-h,t)$ and re-ordering we arrive at

\begin{equation*}
  \frac{\partial^2 f}{\partial x^2}=\frac{f(x-h,t)-2f(x,t)+f(x+h,t)+\mathcal{O}(h^3)}{h^2}.
\end{equation*}

Further, we can do the same thing for time derivatives (or other spatial dimensions in higher dimensional problems) and arrive at

\begin{equation*}
 \frac{\partial f}{\partial t}\approx\frac{f(x,t+\delta t)-f(x,t)}{\delta t},
\end{equation*}

where $\delta t$ is some small increment in time. This then allows us to create 'time-stepping schemes'. For example, consider the partial differential equation

\begin{equation*}
 \frac{\partial u}{\partial t}+c\frac{\partial u}{\partial x}=0,
\end{equation*}

discretizing only the temporal term for the time being we have

\begin{equation*}
 \frac{f(x,t+\delta t)-f(x,t)}{\delta t}+c\frac{\partial u}{\partial x}=0,
\end{equation*}

which we can re-arrange as

\begin{equation*}
 f(x,t+\delta t)=f(x,t)-\delta tc\frac{\partial u}{\partial x}.
\end{equation*}

which is one of the most basic *explicit* time stepping schemes. Now if we approximated our spatial derivative with a forward difference approximation our scheme will become

\begin{equation*}
 f(x,t+\delta t)=f(x,t)-\delta tc\frac{f(x+h,t)-f(x,t)}{h}.
\end{equation*}

Then, provided we know the status of our function $f$ at $t=0$ we can use the above scheme to compute the evolution of $f$ forward in time.

### Setting up the equation

With this out of the way, we can define our partial differential equation, essentially as we would write it on paper:

```python
# We can now write the PDE
pde = seis_model.m * u.dt2 - u.laplace + seis_model.damp * u.dt

# The PDE representation is as on paper
pde
```

$\displaystyle \operatorname{damp}{\left(x,y,z \right)} \frac{\partial}{\partial t} u{\left(t,x,y,z \right)} - \frac{\partial^{2}}{\partial x^{2}} u{\left(t,x,y,z \right)} - \frac{\partial^{2}}{\partial y^{2}} u{\left(t,x,y,z \right)} - \frac{\partial^{2}}{\partial z^{2}} u{\left(t,x,y,z \right)} + \frac{\frac{\partial^{2}}{\partial t^{2}} u{\left(t,x,y,z \right)}}{\operatorname{vp}^{2}{\left(x,y,z \right)}}$

Now we can create our update stencil to get the solution at the forward timestep. The value of `u` at the forward timestep can be accessed using `u.forward`.

```python
u.forward
```

$\displaystyle u{\left(t + dt,x,y,z \right)}$

Similarly, there is a `u.backward` for the backward timestep.

```python
u.backward
```

$\displaystyle u{\left(t - dt,x,y,z \right)}$

Additionally, derivatives can be taken at these positions using `u.forward.dx`.

```python
u.forward.dx
```

$\displaystyle \frac{\partial}{\partial x} u{\left(t + dt,x,y,z \right)}$

With this, we can solve our discretized PDE for u at the forward timestep, and set this equal to `u.forward`. This creates our update stencil.

```python
# This discrete PDE can be solved in a time-marching way updating u(t+dt) from the previous time step
# Devito as a shortcut for u(t+dt) which is u.forward. We can then rewrite the PDE as
# a time marching updating equation known as a stencil using customized SymPy functions

stencil = dv.Eq(u.forward, dv.solve(pde, u.forward))
stencil
```

$\displaystyle u{\left(t + dt,x,y,z \right)} = \frac{- \frac{- \frac{2.0 u{\left(t,x,y,z \right)}}{dt^{2}} + \frac{u{\left(t - dt,x,y,z \right)}}{dt^{2}}}{\operatorname{vp}^{2}{\left(x,y,z \right)}} - \frac{2.5 u{\left(t,x,y,z \right)}}{h_{z}^{2}} - \frac{0.0833333333 u{\left(t,x,y,z - 2 h_{z} \right)}}{h_{z}^{2}} + \frac{1.33333333 u{\left(t,x,y,z - h_{z} \right)}}{h_{z}^{2}} + \frac{1.33333333 u{\left(t,x,y,z + h_{z} \right)}}{h_{z}^{2}} - \frac{0.0833333333 u{\left(t,x,y,z + 2 h_{z} \right)}}{h_{z}^{2}} - \frac{2.5 u{\left(t,x,y,z \right)}}{h_{y}^{2}} - \frac{0.0833333333 u{\left(t,x,y - 2 h_{y},z \right)}}{h_{y}^{2}} + \frac{1.33333333 u{\left(t,x,y - h_{y},z \right)}}{h_{y}^{2}} + \frac{1.33333333 u{\left(t,x,y + h_{y},z \right)}}{h_{y}^{2}} - \frac{0.0833333333 u{\left(t,x,y + 2 h_{y},z \right)}}{h_{y}^{2}} - \frac{2.5 u{\left(t,x,y,z \right)}}{h_{x}^{2}} - \frac{0.0833333333 u{\left(t,x - 2 h_{x},y,z \right)}}{h_{x}^{2}} + \frac{1.33333333 u{\left(t,x - h_{x},y,z \right)}}{h_{x}^{2}} + \frac{1.33333333 u{\left(t,x + h_{x},y,z \right)}}{h_{x}^{2}} - \frac{0.0833333333 u{\left(t,x + 2 h_{x},y,z \right)}}{h_{x}^{2}} + \frac{\operatorname{damp}{\left(x,y,z \right)} u{\left(t,x,y,z \right)}}{dt}}{\frac{\operatorname{damp}{\left(x,y,z \right)}}{dt} + \frac{1}{dt^{2} \operatorname{vp}^{2}{\left(x,y,z \right)}}}$

## Break time! (Again)
Maybe have a biscuit this time?

### Source injection and receiver interpolation

With a numerical scheme to solve the homogenous wave equation, we need to add the source to introduce seismic waves and to implement the measurement operator, and interpolation operator. This operation is linked to the discrete scheme and needs to be done at the proper time step.

With this in mind, we can set up the source and reciever terms to include in our `Operator`.

```python
# Finally we define the source injection and receiver read function to generate the corresponding code
src_term = src.inject(field=u.forward, expr=src * dt**2 / seis_model.m)

# Create interpolation expression for receivers
rec_term = rec.interpolate(expr=u.forward)
```

### Devito operator and solve (where the magic happens)
After constructing all the necessary expressions for updating the wavefield, injecting the source term and interpolating onto the receiver points, we can now create the Devito operator that will generate the C code at runtime. When creating the operator, Devito performs multiple optimizations to reduce flop count and ensure memory locality. Devito can generate code to run on CPUs, GPUs, and clusters thereof, and makes use of several layers of parallelism.

**Note**: The argument `subs=model.spacing_map` causes the operator to substitute values for our current grid spacing into the expressions before code generation. This reduces the number of floating point operations executed by the kernel by pre-evaluating certain coefficients.

```python
op = dv.Operator([stencil] + src_term + rec_term, subs=seis_model.spacing_map)
```

Now we can execute the operator for a number of timesteps. We specify the number of timesteps to compute with the keyword `time` and the timestep size with `dt`.

```python
op(time=time_range.num-1, dt=seis_model.critical_dt)
```

    Operator `Kernel` run in 7.76 s

    PerformanceSummary([(PerfKey(name='section0', rank=None),
                         PerfEntry(time=7.7405060000000105, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section1', rank=None),
                         PerfEntry(time=0.0009480000000000107, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[])),
                        (PerfKey(name='section2', rank=None),
                         PerfEntry(time=0.01052299999999995, gflopss=0.0, gpointss=0.0, oi=0.0, ops=0, itershapes=[]))])

### Plotting the synthetic shot record
We can now plot our shot record using everyone's favourite colourmap. We can clearly see the reflected arrivals from the seabed, top shale, and top CO2 (look for the reverse polarity arrival). We can also distinguish the base of the CO2 lens and the interface between the reservoir sandstone and the underlying shale.

```python
plt.imshow(rec.data, cmap='viridis', aspect='auto', vmax=0.01, vmin=-0.01, extent=(0, 100, tn, t0))
plt.xlabel("Reciever number")
plt.ylabel("Time (ms)")
plt.colorbar()
plt.show()
```

## Visualisation with PyVista:

In the final section of this tutorial, we will cover visualisation of the wavefield using PyVista: a dependency of GemPy. We can use its plotting and manipulation capabilities to visualise various slices of our wavefield.

Firstly, we need to import PyVista

```python
import pyvista as pv
```

With this done, we will want to take the 'data' attribute of 'u', containing the values of the field. This is a numpy array, indexed `[t, x, y, z]`, corresponding to the dimensions of `u`. As we want to plot the current (final) timestep, we will select time index 1.

```python
# Trim down the data from u to remove damping field
trimmed_data = u.data[1, 30:-30, 30:-30, 30:-30]
```

Now we will create a `UniformGrid`. This creates a simple uniform grid from a 3D NumPy array of values.

```python
# Create the spatial reference
grid = pv.UniformGrid()

# Set the grid dimensions: shape + 1 because we want to inject our values on
#   the CELL data
grid.dimensions = np.array(trimmed_data.shape) + 1

# Edit the spatial reference
grid.origin = (0., 0., -1000.)  # The bottom left corner of the data set
grid.spacing = (10, 10, 10)  # These are the cell sizes along each axis
```

We can now fill the grid cells with our wavefield data:

```python
# Add the data values to the cell data
grid.cell_arrays["values"] = trimmed_data.flatten(order="F")  # Flatten the array!
```

And plot some orthogonal slices of this grid to visualise its internal structure:

```python
orth_slices = grid.slice_orthogonal(x=200, y=200, z=-500)

orth_slices.plot(cmap='seismic', clim=[-0.01, 0.01], notebook=True, window_size=(600,400))
```

An alternative visualisation is to take a series of slices along an axis. By setting each slice to be slightly transparent, it is possible to show a fairly complete image of the 3D structure without the need for in interactive plot.

```python
y_slices = grid.slice_along_axis(n=5, axis="y")
p = pv.Plotter(notebook=True, window_size=(600,400))
p.add_mesh(grid.outline(), color="k")
p.add_mesh(y_slices, cmap='seismic', clim=[-0.01, 0.01], opacity=0.8)
p.show()
```

## Further reading and exercise:

There is a further reading notebook `further_reading_fwi.ipynb` showing how to set up a toy full-waveform inversion problem using a slice of the velocity model we created in this tutorial for some inspiration on possible applications.

As an exercise, try adding an additional density parameter to the GemPy model, and use this to create a 1st-order (staggered) acoustic model with Devito.
Some tips:

* Look to `examples/seismic/tutorials/05_staggered_acoustic.ipynb` for a 1st-order acoustic example with constant physical parameters.
* Reshaping the data for use with Devito may be a little more involved. Make sure you separate the density and velocity data before applying the reshape.

## Thank you for listening!

The ecosystem of open-source geoscience software continues to grow, with high-quality packages like GemPy providing accessable, versatile tools to tackle all manner of scientific problems. In combination, we can use these to build new resources for the geoscience community, and ensure that they are adaptable and widely available. Devito is used commercially, and has been deployed at scale, allowing straightforward scaling between small examples and production-scale codes.

We look forward to seeing where you will take this: it would be great to see a 100% open-source, community-created Swung benchmark synthetic with Devito RTM/FWI examples by the time the next Transform rolls around. We envision generating synthetics for machine learning as one potential application, but are excited to see what applications the community will build from this tutorial.

I hope this tutorial has been useful. I will leave you with some helpful resources, and once again, feel free to ask on the Software Underground Slack or our own active Slack if you have any questions!

* [Devito website](https://www.devitoproject.org/)
* [Devito slack](https://devitocodes.slack.com/)
* [GemPy website](https://www.gempy.org/)