Joint inversion of surface-wave dispersion and receiver function

import bayesbay as bb
from bayesbay.discretization import Voronoi1D

from functools import partial
import string
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.ticker import MaxNLocator, AutoLocator, FixedLocator
from disba import PhaseDispersion # install disba for forward modelling of dispersion curves
import pyhk  # Install pyhk for forward modelling of receiver functions 
np.random.seed(30)
np.seterr(all="ignore");
THICKNESS = np.array([10, 10, 15, 20, 20, 20, 20, 20, 0])
VS = np.array([3.38, 3.44, 3.66, 4.25, 4.35, 4.32, 4.315, 4.38, 4.5])
VP_VS = 1.77
VP = VS * VP_VS
RHO = 0.32 * VP + 0.77
def initialize_vs(param, positions=None):
    vmin, vmax = param.get_vmin_vmax(positions)
    sorted_vals = np.sort(np.random.uniform(vmin, vmax, positions.size))
    return sorted_vals

vs = bb.prior.UniformPrior(name="vs", 
                           vmin=[2.2, 2.8, 3.3, 4], 
                           vmax=[3.9, 4.6, 4.8, 5], 
                           position=[0, 20, 60, 150],
                           perturb_std=0.15)
vs.set_custom_initialize(initialize_vs)

voronoi = Voronoi1D(
    name="voronoi", 
    vmin=0,
    vmax=150,
    perturb_std=10,
    n_dimensions=None, 
    n_dimensions_min=4,
    n_dimensions_max=15,
    parameters=[vs], 
    birth_from='neighbour'
)
parameterization = bb.parameterization.Parameterization(voronoi)

Surface wave

PERIODS = np.geomspace(3, 80, 20)
RAYLEIGH_STD = 0.02
LOVE_STD = 0.02
def forward_sw(state, wave='rayleigh', mode=0):
    voronoi = state["voronoi"]
    voronoi_sites = voronoi["discretization"]
    thickness = Voronoi1D.compute_cell_extents(voronoi_sites)
    vs = voronoi["vs"]
    vp = vs * VP_VS
    rho = 0.32 * vp + 0.77
    pd = PhaseDispersion(thickness, vp, vs, rho)
    d_pred = pd(PERIODS, mode=mode, wave=wave).velocity
    return d_pred

forward_rayleigh = partial(forward_sw, wave='rayleigh', mode=0)
forward_love = partial(forward_sw, wave='love', mode=0)
pd = PhaseDispersion(THICKNESS, VP, VS, RHO)
rayleigh = pd(PERIODS, mode=0, wave="rayleigh").velocity
love = pd(PERIODS, mode=0, wave="love").velocity
rayleigh_obs = rayleigh + np.random.normal(0, RAYLEIGH_STD, rayleigh.size)
love_obs = love + np.random.normal(0, LOVE_STD, love.size)
depth_prior = np.linspace(0, 200)
vmin_prior, vmax_prior = vs.get_vmin_vmax(depth_prior)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4), gridspec_kw={'width_ratios': [1, 2.5]})
ax1.fill_betweenx(depth_prior, vmin_prior, vmax_prior, alpha=0.2, label='Prior')
Voronoi1D.plot_tessellation(THICKNESS, 
                            VS, 
                            label='True Vs', 
                            ax=ax1, 
                            color='k', 
                            lw=2, 
                            input_type='extents')
ax1.set_xlabel('Vs [km/s]')
ax1.set_ylabel('Depth [km]')
ax1.set_ylim(np.cumsum(THICKNESS)[-1] + max(THICKNESS), 0)
ax1.grid()
ax1.legend()

ax2.plot(PERIODS, rayleigh, 'r--', label='Rayleigh (true Vs)', lw=0.5)
ax2.plot(PERIODS, love, 'b--', label='Love (true Vs)', lw=0.5)
ax2.plot(PERIODS, rayleigh_obs, 'ro', label='Noisy Rayleigh')
ax2.plot(PERIODS, love_obs, 'bo', label='Noisy Love')
ax2.set_xlabel('Period [s]')
ax2.set_ylabel('Phase velocity [km/s]')
ax2.grid()
ax2.legend()

plt.tight_layout(w_pad=2)
plt.show()
../_images/1f078136e0dfd5cd62ebddb68b3888a20c21ab10d71b25efc226cfb94086ab06.png

Receiver function

T_SHIFT = 5
T_DURATION = 25
T_SAMPLING_INTERVAL = 0.1
GAUSS = 1
RAY_PARAM_S_KM = 0.07
RF_STD = 0.015
def forward_rf(state):
    voronoi = state["voronoi"]
    voronoi_sites = voronoi["discretization"]
    thickness = Voronoi1D.compute_cell_extents(voronoi_sites)
    vs = voronoi["vs"]
    return pyhk.rfcalc(
        ps=0, 
        thik=thickness, 
        beta=vs, 
        kapa=np.ones((len(vs),))*VP_VS,
        p=RAY_PARAM_S_KM, 
        duration=T_DURATION, 
        dt=T_SAMPLING_INTERVAL, 
        shft=T_SHIFT, 
        gauss=GAUSS
    )
rf = pyhk.rfcalc(
    ps=0, 
    thik=THICKNESS, 
    beta=VS, 
    kapa=np.ones((len(VS),))*VP_VS,
    p=RAY_PARAM_S_KM, 
    duration=T_DURATION, 
    dt=T_SAMPLING_INTERVAL, 
    shft=T_SHIFT, 
    gauss=GAUSS
)
rf_obs = rf + np.random.normal(0, RF_STD, rf.size)

RF_TIMES = np.arange(len(rf_obs)) * T_SAMPLING_INTERVAL - T_SHIFT
_, ax = plt.subplots(figsize=(6, 4))

ax.plot(RF_TIMES, rf, 'g--', label="Receiver Function (True vs)", lw=0.5)
ax.scatter(RF_TIMES, rf_obs, label="Receiver Function Observations", s=7, c="g")
ax.set_xlabel("Time [s]")
ax.legend()
<matplotlib.legend.Legend at 0x7fc03a5c3dc0>
../_images/b19af8cea802026bf93e4a2b7042841232a09a718b4dfc04b9b1365fb1b09c6c.png

Sample with BayesBay

target_rayleigh = bb.likelihood.Target("rayleigh", 
                            rayleigh_obs, 
                            std_min=0.001, 
                            std_max=0.1, 
                            std_perturb_std=0.002)
target_love = bb.likelihood.Target("love", 
                        love_obs, 
                        std_min=0.001, 
                        std_max=0.1, 
                        std_perturb_std=0.002)
target_rf = bb.likelihood.Target("rf", 
                      rf_obs, 
                      std_min=0.001, 
                      std_max=0.1, 
                      std_perturb_std=0.002)

targets = [target_rayleigh, target_love, target_rf]
fwd_functions = [forward_rayleigh, forward_love, forward_rf]

log_likelihood = bb.likelihood.LogLikelihood(targets, fwd_functions)
inversion = bb.BayesianInversion(
    parameterization=parameterization, 
    log_likelihood=log_likelihood,
    n_chains=24
)
inversion.run(
    sampler=bb.samplers.SimulatedAnnealing(temperature_start=3), 
    n_iterations=600_000, 
    burnin_iterations=150_000, 
    save_every=150,
    verbose=False
)

Plot results

results = inversion.get_results(concatenate_chains=True)


sampled_voronoi_nuclei = results["voronoi.discretization"]
sampled_thickness = [Voronoi1D.compute_cell_extents(n) for n in sampled_voronoi_nuclei]
sampled_vs = results["voronoi.vs"]
interp_depths = np.linspace(0, 160, 160)
statistics_vs = Voronoi1D.get_tessellation_statistics(
    sampled_thickness, sampled_vs, interp_depths, input_type="extents"
)
fig = plt.figure(figsize=(10, 12), constrained_layout=True)

gs_main = gridspec.GridSpec(2, 1, figure=fig, height_ratios=[1.8, 1], hspace=0.3)
gs_top = gridspec.GridSpecFromSubplotSpec(
    1, 2, subplot_spec=gs_main[0], width_ratios=[1, 2], wspace=0.8
)


# 6 RANDOMLY CHOSEN CHAINS
# gs_left = gridspec.GridSpecFromSubplotSpec(3, 2, subplot_spec=gs_top[0], wspace=0.001, hspace=0.02)
gs_left = gridspec.GridSpecFromSubplotSpec(
    4, 2, subplot_spec=gs_top[0], wspace=0, hspace=0.04, height_ratios=[1, 1, 1, 0.075]
)

random_chains = np.sort(np.random.choice(np.arange(20), 6, replace=False))
for ipanel, ichain in zip(range(6), random_chains):
    istart = 3000 * ichain  # 600_000 iterations, 150_000 burnin, 24 chains
    iend = istart + 3000
    samples_thickness = sampled_thickness[istart:iend]
    samples_vs = sampled_vs[istart:iend]

    ax = fig.add_subplot(gs_left[ipanel])
    # Your plotting commands for these subplots here
    # Voronoi1D.plot_tessellations(samples_thickness,
    #                               samples_vs,
    #                               input_type='extents',
    #                               ax=ax,
    #                               linewidth=0.1,
    #                               color="k",
    #                               bounds=(0, 150))
    density, Y, X = Voronoi1D.get_tessellation_density(
        samples_thickness, samples_vs, input_type="extents"
    )
    img = ax.pcolormesh(X, Y, density, cmap="binary", vmin=0, vmax=0.2)
    ax.invert_yaxis()
    ax.text(
        x=0.97,
        y=0.98,
        s=f"Chain {ichain+1}",
        va="top",
        ha="right",
        transform=ax.transAxes,
        bbox=dict(facecolor="w", edgecolor="w", alpha=1, boxstyle="square,pad=0.05"),
    )
    ax.tick_params(direction="in")
    # ax.grid()
    row, col = divmod(ipanel, 2)
    if col == 1:
        ax.tick_params(labelleft=False)
    else:
        ax.set_ylabel("Depth [km]")
    if row < 2:
        ax.tick_params(labelbottom=False)
        ax.set_xlabel("")
    else:
        ax.set_xlabel("Vs [km/s]")
    ax.yaxis.set_major_locator(MaxNLocator(prune="both", nbins=7))
    ax.xaxis.set_major_locator(AutoLocator())
    ax.set_xlim(3.3, 4.6)
    ax.text(
        x=0.1,
        y=0.05,
        s=f"({string.ascii_lowercase[ipanel]})",
        va="bottom",
        ha="left",
        fontsize=16,
        bbox=dict(facecolor="w", edgecolor="w", alpha=1, boxstyle="round,pad=0.2"),
        transform=ax.transAxes,
    )

cbar_ax = fig.add_subplot(gs_left[-1, :])
# cbar = fig.colorbar(img, cax=cbar_ax, orientation='horizontal')
# cbar.ax.xaxis.set_label_position('top')  # Move the label to the top
# cbar.set_label('Posterior Density', labelpad=5)
cbar_ax.set_axis_off()
inset_cbar_ax = cbar_ax.inset_axes([0.1, 0.2, 0.8, 0.75])
cbar = fig.colorbar(img, cax=inset_cbar_ax, orientation="horizontal")
cbar.ax.xaxis.set_label_position("top")  # Move the label to the top
cbar.set_label("Posterior Density", labelpad=5)
# INFERRED MODEL AND INTERFACES
gs_right = gridspec.GridSpecFromSubplotSpec(
    1, 2, subplot_spec=gs_top[1], width_ratios=[1.5, 1], wspace=0
)

ax1 = fig.add_subplot(gs_right[0])
ax1.fill_betweenx(
    depth_prior, vmin_prior, vmax_prior, color="orange", alpha=0.3, label="Prior"
)
ax1.plot(
    statistics_vs["median"], interp_depths, "royalblue", lw=3, label="Posterior Median"
)
Voronoi1D.plot_tessellation(
    THICKNESS,
    VS,
    ax=ax1,
    color="r",
    ls="--",
    lw=2,
    label="True Vs",
    input_type="extents",
)

ax1.fill_betweenx(
    interp_depths,
    *statistics_vs["percentiles"],
    color="#4169E133",
    # alpha=0.2,
    label="Uncertainty\n(10-90th perc.)",
)
handles, labels = ax1.get_legend_handles_labels()
handles = [handles[0], handles[2], handles[1], handles[3]]
labels = [labels[0], labels[2], labels[1], labels[3]]

ax1.set_xlabel("Vs [km/s]")
ax1.set_ylabel("Depth [km]")
ax1.grid()
ax1.legend(handles, labels, loc="lower left")
ax1.set_xlim(3.32, 4.6)
ax1.set_ylim(150, 0)
ax1.text(
    x=0.95,
    y=0.97,
    s="(g)",
    va="top",
    ha="right",
    fontsize=16,
    bbox=dict(facecolor="w", edgecolor="w", alpha=0.5, boxstyle="round,pad=0.2"),
    transform=ax1.transAxes,
)

ax2 = fig.add_subplot(gs_right[1])
ax2.fill_between([0, 1 / (150)], y1=0, y2=150, alpha=0.3, color="orange", label="Prior")

ax2 = Voronoi1D.plot_interface_hist(
    sampled_voronoi_nuclei,
    ax=ax2,
    swap_xy_axes=True,
    bins=75,
    fc="royalblue",
    ec="w",
    label="Posterior",
)

for interface_depth in np.cumsum(THICKNESS)[:-2]:
    ax2.axhline(y=interface_depth, xmax=0.2, color="r", lw=3, alpha=1, zorder=5, ls="-")
ax2.axhline(
    y=np.cumsum(THICKNESS)[-1],
    xmax=0.2,
    color="r",
    lw=3,
    alpha=1,
    zorder=5,
    ls="-",
    label="True\nInterface\nPosition",
)

handles, labels = ax2.get_legend_handles_labels()
handles = [handles[0], handles[2], handles[1]]
labels = [labels[0], labels[2], labels[1]]

ax2.tick_params(labelleft=False)
ax2.set_ylabel("")
ax2.set_ylim(*ax1.get_ylim())
ax2.grid()
ax2.legend(handles, labels, loc="center right")
ax2.xaxis.set_major_locator(MaxNLocator(prune="both", nbins=3))
ax2.text(
    x=0.95,
    y=0.97,
    s="(h)",
    va="top",
    ha="right",
    fontsize=16,
    bbox=dict(facecolor="w", edgecolor="w", alpha=0.5, boxstyle="round,pad=0.2"),
    transform=ax2.transAxes,
)


# NOISE STD
gs_bottom = gridspec.GridSpecFromSubplotSpec(
    2, 2, subplot_spec=gs_main[1], wspace=0.05, hspace=0.05
)

std_min_sw, std_max_sw = 0.001, 0.1
std_min_rf, std_max_rf = 0.001, 0.1

# Rayleigh
ax3 = fig.add_subplot(gs_bottom[0])
ax3.fill_between(
    [std_min_sw, std_max_sw],
    1 / (std_max_sw - std_min_sw),
    alpha=0.3,
    color="orange",
    label="Prior",
)
pdf, bins, _ = ax3.hist(
    results["rayleigh.std"],
    density=True,
    bins=np.linspace(std_min_sw, std_max_sw, 35),
    fc="royalblue",
    ec="w",
    label="Posterior",
)
ax3.axvline(
    x=RAYLEIGH_STD, ymax=0.2, color="r", lw=3, alpha=1, label="True Noise\n(Rayleigh)"
)
ax3.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
ax3.legend(loc="upper right")


ax4 = fig.add_subplot(gs_bottom[1])
ax4.fill_between(
    [std_min_sw, std_max_sw],
    1 / (std_max_sw - std_min_sw),
    alpha=0.3,
    color="orange",
    label="Prior",
)
pdf, bins, _ = ax4.hist(
    results["love.std"],
    density=True,
    bins=np.linspace(std_min_sw, std_max_sw, 35),
    fc="royalblue",
    ec="w",
    label="Posterior",
)
ax4.axvline(x=LOVE_STD, ymax=0.2, color="r", lw=3, alpha=1, label="True Noise\n(Love)")
ax4.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
ax4.legend(loc="upper right")


ax5 = fig.add_subplot(gs_bottom[2])
ax5.fill_between(
    [std_min_rf, std_max_rf],
    1 / (std_max_rf - std_min_rf),
    alpha=0.3,
    color="orange",
    label="Prior",
)
pdf, bins, _ = ax5.hist(
    results["rf.std"],
    density=True,
    bins=np.linspace(std_min_rf, std_max_rf, 35),
    fc="royalblue",
    ec="w",
    label="Posterior",
)
ax5.axvline(x=RF_STD, ymax=0.2, color="r", lw=3, alpha=1, label="True Noise\n(RF)")
ax5.ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
ax5.legend(loc="upper right")

ax6 = fig.add_subplot(gs_bottom[3])
ndim_min, ndim_max = 4, 15
ax6.fill_between(
    [ndim_min, ndim_max],
    1 / (ndim_max - ndim_min),
    alpha=0.3,
    color="orange",
    label="Prior",
)

ax6.hist(
    results["voronoi.n_dimensions"],
    bins=np.arange(ndim_min - 0.5, ndim_max + 0.5),
    fc="royalblue",
    density=True,
    ec="w",
    label="Posterior",
)
ax6.axvline(
    x=9, ymax=0.2, color="r", lw=3, alpha=1, label="True\nNo. Layers", zorder=100
)
ax6.legend()

for ipanel, ax in enumerate([ax3, ax4, ax5, ax6]):
    if ipanel == 0 or ipanel == 2:
        ax.set_ylabel("Probability Density")
    ax.set_xlabel("Standard Deviation")
    ax.spines["right"].set_visible(False)
    ax.spines["top"].set_visible(False)
    ax.text(
        x=0.03,
        y=0.97,
        s=f"({string.ascii_lowercase[8 + ipanel]})",
        va="top",
        ha="left",
        fontsize=16,
        bbox=dict(facecolor="w", edgecolor="w", alpha=0.5, boxstyle="round,pad=0.2"),
        transform=ax.transAxes,
    )

ax6.set_xlabel("No. Layers")
# ax6.legend()

plt.show()
../_images/3cddc8eb866c8b2008a699f774cee9a2c890849201209b1858858f3871011f2e.png
results.keys()
dict_keys(['voronoi.n_dimensions', 'voronoi.discretization', 'voronoi.vs', 'rayleigh.std', 'love.std', 'rf.std', 'rayleigh.dpred', 'love.dpred', 'rf.dpred'])