Seismic Receiver Function Inversion#

Receiver functions are a common observable in seismology. They represent the conversion of the form of energy of seismic waves from compressional to transverse (and vice versa) at geological interfaces. Details aside, the aim of a receiver function inversion is to determine the seismic structure at a point down to a depth of a few tens of kilometers. Typically the subsurface is parameterised as a few layers of constant velocity, where the thickness, velocity (and potentially even number) of layers are unknowns to be solved for.

This example uses the espresso library to provide data, a forward solver and other utilities related to a receiver function inversion.

from espresso import ReceiverFunctionInversion
import numpy as np
import matplotlib.pyplot as plt

from neighpy import NASearcher, NAAppraiser

Set up Espresso Receiver Function Inversion#

We start with a problem where we want to find the seismic velocity in fixed layers.

rfi = ReceiverFunctionInversion(example_number=2)
rfi.description
'Inverting velocities of 5 layers'

Lets have a look at the data

rfi.plot_data(rfi.data)
<Axes: xlabel='Time/s', ylabel='Amplitude'>
../_images/2c3447b82eb891e50fad25e2bb7c5d5af9f976f744e85ee91c4936639a786580.png

To first order, every peak in the receiver function is caused by a seismic discontinuity (change in seismic velocity).

And now let’s look at what a “good” model looks like, and a potential starting model

ax = rfi.plot_model(rfi.good_model, rfi.starting_model, "Good model", "Starting model")
ax.legend()
<matplotlib.legend.Legend at 0x28307fa50>
../_images/011033b2d8e37d582d9c06a1717e96f070e922d3499e2002e80348ea1d2152ac.png

Out of curiosity, let’s see how well these models fit the observed data

ax = rfi.plot_data(rfi.data, label="Data")
for name, (model, colour) in {"Starting model" : [rfi.starting_model, "r"], "Good model": [rfi.good_model, "b"]}.items():
    synth = rfi.forward(model)
    ax.plot(rfi._t, synth, label=name, c=colour)
ax.legend()
<matplotlib.legend.Legend at 0x283068cd0>
../_images/476a360bd0bbdc87888d8357a7a6ef8805174f18adb7c0da6c8347629cd2e4c0.png

Set up Neighbourhood Algorithm with Neighpy#

Espresso provides us with a log_likelihood function, which we can use as our objective function (although remember to multiply by -1 since we want to minimise the objective!). We can choose the bounds on the velocity in each layer as we wish. These should be generally increasing with depth.

searcher = NASearcher(
    objective=lambda m: -1 * rfi.log_likelihood(rfi.data, rfi.forward(m)),
    bounds=[(3.5, 7.0)] * rfi.model_size,
    ni=2000,  # size of initial population
    ns=50,  # number of new samples at each iteration
    nr=10,  # number of neighbourhoods to resample at each iteration
    n=100  # number of iterations
)

searcher.run(parallel=False)  # some serialisation issues with the EspressoProblem object preventing parallelisation

best_i = np.argmin(searcher.objectives)
best = searcher.samples[best_i]
NAI - Initial Random Search
NAI - Optimisation Loop: 100%|██████████| 100/100 [00:08<00:00, 12.38it/s]

A rather crude check on the convergence

plt.plot(searcher.objectives, marker=".", linestyle="", markersize=2)
plt.scatter(best_i, searcher.objectives[best_i], c="g", s=10, zorder=10)
plt.axvline(searcher.ni, c="k", ls="--")
plt.yscale("log")
plt.text(0.05, 0.95, "Initial Random Search", transform=plt.gca().transAxes, ha="left")
plt.text(0.95, 0.95, "Neighbourhood Search", transform=plt.gca().transAxes, ha="right")
Text(0.95, 0.95, 'Neighbourhood Search')
../_images/c7f31571b8a6b80dc147a4063001888b9a4eb029460598fa108a1dfd45b74e4d.png

Plot models

def _plot_model(model):
    model_setup = rfi._model_setup(model)
    px = np.zeros([2 * len(model_setup), 2])
    px[0::2, 0] = model_setup[:, 1]
    px[1::2, 0] = model_setup[:, 1]
    px[1::2, 1] = model_setup[:, 0]
    px[2::2, 1] = model_setup[:-1, 0]
    return px


ax = rfi.plot_model(
    rfi.good_model,
    label="Good model",
)
for model in searcher.samples[:searcher.ni]:
    ax.plot(*_plot_model(model).T, c="k", alpha=0.01)
ax.plot(
    *_plot_model(searcher.samples[np.argmin(searcher.objectives)]).T,
    "g--",
    label="Best model found"
)
ax.legend(loc="lower left")
<matplotlib.legend.Legend at 0x284457ed0>
../_images/c1267be2e0828f99408b637f7b49eef94a832c6fdb97f7157fa1bebd622ba107.png

Plot predicted data

ax = rfi.plot_data(rfi.data, label="Data")
_ylim = ax.get_ylim()
for model in searcher.samples[:searcher.ni]:
    ax.plot(rfi._t, rfi.forward(model), c="k", alpha=0.01)
ax.get_lines()[0].set_zorder(10)
ax.plot(rfi._t, rfi.forward(best), "g--", label="Best model found", zorder=11)
ax.set_ylim(_ylim)
ax.legend()
<matplotlib.legend.Legend at 0x289946250>
../_images/c0ce1b52df4876ba900fb286d3a569e96f222acec19c1c9a7c2eaed956fe52de.png

Pretty decent! Now to independently resample the posterior using the Appraisal phase of the NA.

appraiser = NAAppraiser(searcher=searcher, n_resample=20000, n_walkers=10)
appraiser.run()
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 46.13it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 46.11it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 46.04it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 46.01it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 45.95it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 45.94it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 45.88it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 45.85it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 45.61it/s]
NAII - Random Walk: 100%|██████████| 2000/2000 [00:43<00:00, 45.54it/s]
ax = rfi.plot_model(best, rfi.good_model, label="Best model found (NAI)", label2="Good model")
for model in appraiser.samples[::10]:
    ax.plot(*_plot_model(model).T, c="k", alpha=0.01)
ax.get_lines()[0].set_color("g")
ax.get_lines()[0].set_linestyle("--")
ax.get_lines()[0].set_zorder(10)
ax.get_lines()[1].set_zorder(10)
ax.get_lines()[1].set_color("b")
ax.plot(*_plot_model(appraiser.mean).T, "r--", label="Mean model (NAII)")
ax.legend(loc="lower left")
<matplotlib.legend.Legend at 0x296ed6250>
../_images/96b829531bb79b407e333ec3c7841f1725f838752b7da153d248bd72a4df6cb7.png
fig, ax = plt.subplots(rfi.model_size, rfi.model_size, figsize=(10, 10), tight_layout=True)
for i in range(rfi.model_size):
    for j in range(rfi.model_size):
        ax[i, j].set_xlim(searcher.bounds[j])
        if i == j:
            ax[i, j].hist(appraiser.samples[:, j], bins=100, histtype="step", color="k")
            ax[i, j].axvline(best[j], c="g", ls="--")
            ax[i, j].axvline(rfi.good_model[j], c="b", ls="--")
            ax[i, j].axvline(appraiser.mean[j], c="r", ls="--")
        elif j<i:
            ax[i, j].scatter(appraiser.samples[:, j], appraiser.samples[:, i], s=1, c="k", alpha=0.01)
            ax[i, j].scatter(best[j], best[i], c="g", s=10, zorder=10)
            ax[i, j].scatter(rfi.good_model[j], rfi.good_model[i], c="b", s=10, zorder=10)
            ax[i, j].scatter(appraiser.mean[j], appraiser.mean[i], c="r", s=10, zorder=10)
        else:
            ax[i, j].axis("off")
../_images/59dfb14bf5b73c5c54ed1a7639bfa4e7f614c7f051b79961144a2743c0415b0a.png

A more complicated inversion#

What if we also wanted to invert for the interface depths?

rfi = ReceiverFunctionInversion(example_number=3)
rfi.description
'Inverting depths and velocities of 5 layers'
searcher = NASearcher(
    objective=lambda m: -1 * rfi.log_likelihood(rfi.data, rfi.forward(m)),
    bounds=[(0.1, 2), (3.5, 7.0),
            (0.5, 5), (3.5, 7.0),
            (2, 15), (3.5, 7.0),
            (10, 25), (3.5, 7.0),
            (15, 50), (3.5, 7.0),],
    ni=5000,  # size of initial population
    ns=200,  # number of new samples at each iteration
    nr=50,  # number of neighbourhoods to resample at each iteration
    n=100  # number of iterations
)

searcher.run(parallel=False)  # some serialisation issues with the EspressoProblem object preventing parallelisation

best_i = np.argmin(searcher.objectives)
best = searcher.samples[best_i]
NAI - Initial Random Search
NAI - Optimisation Loop: 100%|██████████| 100/100 [01:03<00:00,  1.57it/s]
plt.plot(searcher.objectives, marker=".", linestyle="", markersize=2)
plt.scatter(best_i, searcher.objectives[best_i], c="g", s=10, zorder=10)
plt.axvline(searcher.ni, c="k", ls="--")
plt.yscale("log")
plt.text(0.05, 0.95, "Initial Random Search", transform=plt.gca().transAxes, ha="left")
plt.text(0.95, 0.95, "Neighbourhood Search", transform=plt.gca().transAxes, ha="right")
Text(0.95, 0.95, 'Neighbourhood Search')
../_images/14567a89abc06cd055a27e313981fb2c9cbba71ee42a5639d58944b74a5d8f8a.png
appraiser = NAAppraiser(searcher=searcher, n_resample=75000, n_walkers=15)
appraiser.run()
NAII - Random Walk: 100%|██████████| 5000/5000 [25:32<00:00,  3.26it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:33<00:00,  3.26it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:40<00:00,  3.25it/s]0:42,  3.82it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:42<00:00,  3.24it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:43<00:00,  3.24it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:44<00:00,  3.24it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:46<00:00,  3.23it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:48<00:00,  3.23it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:50<00:00,  3.22it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:51<00:00,  3.22it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:53<00:00,  3.22it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:53<00:00,  3.22it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:57<00:00,  3.21it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:58<00:00,  3.21it/s]
NAII - Random Walk: 100%|██████████| 5000/5000 [25:58<00:00,  3.21it/s]
ax = rfi.plot_model(best, rfi.good_model, label="Best model found (NAI)", label2="Good model")
for model in appraiser.samples[::10]:
    ax.plot(*_plot_model(model).T, c="k", alpha=0.01)
ax.get_lines()[0].set_color("g")
ax.get_lines()[0].set_linestyle("--")
ax.get_lines()[0].set_zorder(10)
ax.get_lines()[1].set_zorder(10)
ax.get_lines()[1].set_color("b")
ax.plot(*_plot_model(appraiser.mean).T, "r--", label="Mean model (NAII)")
ax.legend(loc="lower left")
<matplotlib.legend.Legend at 0x299af7ed0>
../_images/d5e88dea6f048bbe7c045b1d5351a6b96ee58e6be4fc73d668fa0c252b93cb05.png
fig, ax = plt.subplots(rfi.model_size, rfi.model_size, figsize=(10, 10), tight_layout=True, gridspec_kw={"hspace": 0, "wspace": 0})
for i in range(rfi.model_size):
    for j in range(rfi.model_size):
        ax[i, j].set_xlim(searcher.bounds[j])
        ax[i, j].set_xticks([], [])
        ax[i, j].set_yticks([], [])
        if i == j:
            ax[i, j].hist(appraiser.samples[::10, j], bins=100, histtype="step", color="k")
            ax[i, j].axvline(best[j], c="g", ls="--")
            ax[i, j].axvline(rfi.good_model[j], c="b", ls="--")
            ax[i, j].axvline(appraiser.mean[j], c="r", ls="--")
        elif j<i:
            ax[i, j].scatter(appraiser.samples[::10, j], appraiser.samples[::10, i], s=1, c="k", alpha=0.01)
            ax[i, j].scatter(best[j], best[i], c="g", s=10, zorder=10)
            ax[i, j].scatter(rfi.good_model[j], rfi.good_model[i], c="b", s=10, zorder=10)
            ax[i, j].scatter(appraiser.mean[j], appraiser.mean[i], c="r", s=10, zorder=10)
            ax[i, j].set_ylim(searcher.bounds[i])
        else:
            ax[i, j].axis("off")

for ax_, xlabel in zip(ax[-1, :], ["d_1", "v_1", "d_2", "v_2", "d_3", "v_3", "d_4", "v_4", "d_5", "v_5"]):
    ax_.set_xlabel(xlabel)
for ax_, ylabel in zip(ax.T[0, :], ["d_1", "v_1", "d_2", "v_2", "d_3", "v_3", "d_4", "v_4", "d_5", "v_5"]):
    ax_.set_ylabel(ylabel)
ax[0,0].set_ylabel("")
Text(0, 0.5, '')
../_images/7392fc69dede8d31db299a2a20a82a0e3d68c560a57a237f840e19dac00f10d3.png
ax = rfi.plot_data(rfi.data, label="Data")
_ylim = ax.get_ylim()
for model in appraiser.samples[::15]:
    ax.plot(rfi._t, rfi.forward(model), c="k", alpha=0.01)
ax.get_lines()[0].set_zorder(10)
ax.plot(rfi._t, rfi.forward(best), "g--", label="Best model (I)", zorder=11)
ax.plot(rfi._t, rfi.forward(appraiser.mean), "r--", label="Mean model (II)", zorder=11)
ax.set_ylim(_ylim)
ax.legend()
<matplotlib.legend.Legend at 0x2c84efed0>
../_images/ed57c38f4941505ececfdf45b779abed1e68a7afebe187dee3b712f67f754da9.png