Tilt correction#

This script implements a simple example for dynamic tilt correction. It reproduces the tilt table experiment from Bernauer et al (2020) based on the method by Crawford and Webb (2000).

The example demonstrates the different correction variants provided by twistpy.tilt.correction.remove_tilt.

The example data required by this script is included in the TwistPy example_data collection.

import os

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import rc
from matplotlib.transforms import Bbox, TransformedBbox, blended_transform_factory
from mpl_toolkits.axes_grid1.inset_locator import (
    BboxPatch,
    BboxConnector,
    BboxConnectorPatch,
)
from obspy import Stream, Trace

from twistpy.tilt.correction import remove_tilt
from twistpy.tilt.util import (
    get_data,
    trigger,
    calc_residual_disp,
    get_angle,
    theo_resid_disp,
    calc_height_of_mass,
)

This script implements a simple example for dynamic tilt correction. It reproduces the # tilt table experiment from Bernauer et al. 2020 (SRL).

data_dir = "../example_data/tilt_correction"

This method is needed for the matplotlib zoom effect

def connect_bbox(
    bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, prop_lines, prop_patches=None
):
    if prop_patches is None:
        prop_patches = {
            **prop_lines,
            "alpha": prop_lines.get("alpha", 1) * 0.0,
        }

    c1 = BboxConnector(bbox1, bbox2, loc1=loc1a, loc2=loc2a, **prop_lines)
    c1.set_clip_on(False)
    c2 = BboxConnector(bbox1, bbox2, loc1=loc1b, loc2=loc2b, **prop_lines)
    c2.set_clip_on(False)

    bbox_patch1 = BboxPatch(bbox1, **prop_patches)
    bbox_patch2 = BboxPatch(bbox2, **prop_patches)

    p = BboxConnectorPatch(
        bbox1, bbox2, loc1a=loc1a, loc2a=loc2a, loc1b=loc1b, loc2b=loc2b, **prop_patches
    )
    p.set_clip_on(False)

    return c1, c2, bbox_patch1, bbox_patch2, p

This method is needed for the matplotlib zoom effect

def zoom_effect01(ax1, ax2, xmin, xmax, **kwargs):
    """
    Connect *ax1* and *ax2*. The *xmin*-to-*xmax* range in both axes will
    be marked.
    Parameters
    ----------
    ax1
        The main axes.
    ax2
        The zoomed axes.
    xmin, xmax
        The limits of the colored area in both plot axes.
    **kwargs
        Arguments passed to the patch constructor.
    """

    trans1 = blended_transform_factory(ax1.transData, ax1.transAxes)
    trans2 = blended_transform_factory(ax2.transData, ax2.transAxes)

    bbox = Bbox.from_extents(xmin, 0, xmax, 1)

    mybbox1 = TransformedBbox(bbox, trans1)
    mybbox2 = TransformedBbox(bbox, trans2)

    prop_patches = {**kwargs, "ec": "none", "alpha": 0.0}

    c1, c2, bbox_patch1, bbox_patch2, p = connect_bbox(
        mybbox1,
        mybbox2,
        loc1a=3,
        loc2a=2,
        loc1b=4,
        loc2b=1,
        prop_lines=kwargs,
        prop_patches=prop_patches,
    )

    ax1.add_patch(bbox_patch1)
    ax2.add_patch(bbox_patch2)
    ax2.add_patch(c1)
    ax2.add_patch(c2)
    ax2.add_patch(p)

    return c1, c2, bbox_patch1, bbox_patch2, p

This method is needed for the matplotlib zoom effect

def zoom_effect02(ax1, ax2, **kwargs):
    """
    ax1 : the main axes
    ax1 : the zoomed axes
    Similar to zoom_effect01.  The xmin & xmax will be taken from the
    ax1.viewLim.
    """

    tt = ax1.transScale + (ax1.transLimits + ax2.transAxes)
    trans = blended_transform_factory(ax2.transData, tt)

    mybbox1 = ax1.bbox
    mybbox2 = TransformedBbox(ax1.viewLim, trans)

    prop_patches = {**kwargs, "ec": "none", "alpha": 0.2}

    c1, c2, bbox_patch1, bbox_patch2, p = connect_bbox(
        mybbox1,
        mybbox2,
        loc1a=2,
        loc2a=3,
        loc1b=1,
        loc2b=4,
        prop_lines=kwargs,
        prop_patches=prop_patches,
    )

    ax1.add_patch(bbox_patch1)
    ax2.add_patch(bbox_patch2)
    ax2.add_patch(c1)
    ax2.add_patch(c2)
    ax2.add_patch(p)

    return c1, c2, bbox_patch1, bbox_patch2, p
TEST - tilt table - #
  • high gain, 1/6 - #

  • 166.7mum - #

  • 21 steps - #

Get raw data from

stream1 = os.path.join(data_dir, "XX.TC120..HH*.D.2018.343")
stream2 = os.path.join(data_dir, "XX.BS1..HJ*.D.2018.343")

at the time

utctime = "2018-12-09T19:48:48.6"

how many seconds of data do you want to read in?

duration = 120

Define the seismometer and rotational sensor stream identifiers

correct_channel = "HH*"
input_channel = "HJ*"

Set some trigger parameters and start to search for steps from second

S = 18.0
# no steps after second
E = 91.0

correct the trigger onset by a constant offset, which was determined visually

c_on = -0.075
c_off = 0.49

define the zoom windows for the plots

zoom00 = 0.0
zoom01 = 110.0
zoom0 = 53.6
zoom1 = 55.6

Geometrical parameters of the experiment horizontal distance of center of seismometer to axis of rotation in m

l = 0.32  # noqa
# vertical distance of bottom of seismometer to axis of rotation in m
dh = 0.047

define the source stream (ss) and the receiver stream (sr) channels

ch_sr = "HHN"
ch_ss = "HJE"

get the data

vel_orig, rr_orig = get_data(
    stream1,
    stream2,
    utctime,
    duration,
    correct_channel,
    input_channel,
    os.path.join(data_dir, "station.xml"),
    ch_sr,
    ch_ss,
)

make four independent streams containing tilt contaminated acceleration recordings: (1) the original tilt contaminated stream for later comparisons (2) the stream that will be treated with the frequency domain (CaW) correction (3) the stream that will be treated with the frequency domain (coh) correction (4) the stream that will be treated with the time domain correction

sr = vel_orig.copy()
sr.filter("bandpass", freqmin=0.03, freqmax=10, corners=8, zerophase=True)

acc_orig = sr.copy()
acc_orig.differentiate()  # original acc recording (reciever)

rf1 = sr.copy()
rf2 = sr.copy()
rt = sr.copy()
rf1.differentiate()  # reciever for freq-domain (coh) analysis (acc recording)
rf2.differentiate()  # reciever for freq-domain (plain) analysis (acc recording)  # noqa
rt.differentiate()  # reciever for time-domain analysis (acc recording)
<obspy.core.stream.Stream object at 0x1889b7ca0>

make two independent streams containing the tilt angle recording (1) original tilt angle recording for later comparisons (2) tilt angle recording as the source for the corrections

ss = rr_orig.copy()
ss.filter("bandpass", freqmin=0.03, freqmax=10, corners=8, zerophase=True)

ra_orig = ss.copy()
ra_orig.integrate()  # original rotation angle recording (source)

ts = ss.copy()
ts.integrate()  # source for correction (tilt angle recording)
<obspy.core.stream.Stream object at 0x188afdd60>

In this example, we are treating the North-axis of acceleration and the East-axis of rotation angle. Thus, for a positive rotation, the tilt induced accelertion shows into the same direction as the horizontal ground movement accelertion. We account for this in the subsequent analysis by setting

par = True

Now, lets do the tilt corrections!

frequency domain (coh)#

fmin = None
fmax = None
acc_corr_freq1_data = remove_tilt(
    rf1[0].data,
    ts[0].data,
    rf1[0].stats.delta,
    fmin,
    fmax,
    parallel=par,
    smooth=100.0 / 164.0,
    method="coh",
)

acc_corr_freq1 = acc_orig.copy()
acc_corr_freq1[0].data = acc_corr_freq1_data

acc_corr_freq1.detrend("demean")

vel_corr_freq1 = acc_corr_freq1.copy()
vel_corr_freq1.integrate()

# -----------------------------------------------------------------------------
# frequency domain (plain)
# -----------------------------------------------------------------------------

fmin = None
fmax = None
acc_corr_freq2_data = remove_tilt(
    rf2[0].data,
    ts[0].data,
    rf2[0].stats.delta,
    fmin,
    fmax,
    parallel=par,
    smooth=100.0 / 164.0,
    method="freq",
)

acc_corr_freq2 = acc_orig.copy()
acc_corr_freq2[0].data = acc_corr_freq2_data

acc_corr_freq2.detrend("demean")

vel_corr_freq2 = acc_corr_freq2.copy()
vel_corr_freq2.integrate()

# -----------------------------------------------------------------------------
# time domain
# -----------------------------------------------------------------------------

acc_corr_time_data = remove_tilt(
    rt[0].data, ts[0].data, rt[0].stats.delta, parallel=par
)
acc_corr_time = acc_orig.copy()
acc_corr_time[0].data = acc_corr_time_data

acc_corr_time.detrend("demean")

vel_corr_time = acc_corr_time.copy()
vel_corr_time.integrate()
<obspy.core.stream.Stream object at 0x188b14c70>

Due to the very well known geometry in the tilt table experiment, we can play some games e.g. try to locate the proof mass of the seismometer and compare the output of our corrections with theortically expercted movements.

on, off = trigger(rr_orig[0], 10, 140, 6.0, 5.0, c_on, c_off, S, E, plot=False)

# define a time axis
sec = np.arange(len(acc_orig[0].data)) / (acc_orig[0].stats.sampling_rate)

print(len(on))
print(len(off))

# calculate residual displacement

alpha = get_angle(ts, on, off)  # angle steps recorded by BS1
21
21

in theory: according to Steffen position of the mass is approximately at the middle of the housing

h_m = 0.0575  # m
h = h_m  # m

std_m = 0.01

r, centr = theo_resid_disp(ts[0].data, l, h, dh, rr_orig[0].data)
trr = Stream(traces=Trace(data=r, header=ts[0].stats))
trr.differentiate()
trr_a = trr.copy()
trr_a.differentiate()
time_ttheo, disp_ttheo, mean_disp_ttheo, sigma_ttheo = calc_residual_disp(
    trr, on, off, np.zeros(len(r)), theo=True
)
h_ttheo, std_ttheo = calc_height_of_mass(mean_disp_ttheo, l, dh, alpha)

tcentr = Stream(traces=Trace(data=centr, header=ts[0].stats))
tcentr.detrend("demean")
tcentr.detrend("linear")
tcentr.integrate()
tcentr.detrend("demean")
tcentr.detrend("linear")
tcentr.integrate()
<obspy.core.stream.Stream object at 0x188f5dd00>

correct for residual displacement in time domain

acc_corr_time2 = acc_corr_time.copy()
acc_corr_time2[0].data = acc_corr_time2[0].data - trr_a[0].data

vel_corr_time2 = acc_corr_time2.copy()
vel_corr_time2.integrate()

tr1_vel = vel_corr_freq1.copy()
tr2_vel = vel_corr_freq2.copy()
tt_vel = vel_corr_time.copy()
tt2_vel = vel_corr_time2.copy()

tr1_new = vel_corr_freq1.copy()
tr2_new = vel_corr_freq2.copy()
tt_new = vel_corr_time.copy()
tt2_new = vel_corr_time2.copy()

time_tr1, disp_tr1, mean_disp_tr1, sigma_tr1 = calc_residual_disp(tr1_new, on, off, r)

time_tr2, disp_tr2, mean_disp_tr2, sigma_tr2 = calc_residual_disp(tr2_new, on, off, r)

time_tt, disp_tt, mean_disp_tt, sigma_tt = calc_residual_disp(tt_new, on, off, r)

time_tt2, disp_tt2, mean_disp_tt2, sigma_tt2 = calc_residual_disp(tt2_new, on, off, r)

calculate the position of the seismometer mass

h_tr1, std_tr1 = calc_height_of_mass(mean_disp_tr1, l, dh, alpha)
h_tr2, std_tr2 = calc_height_of_mass(mean_disp_tr2, l, dh, alpha)
h_tt, std_tt = calc_height_of_mass(mean_disp_tt, l, dh, alpha)

disp_corr_freq1 = vel_corr_freq1.copy()
disp_corr_freq1.integrate()

disp_corr_freq2 = vel_corr_freq2.copy()
disp_corr_freq2.integrate()

disp_corr_time = vel_corr_time.copy()
disp_corr_time.integrate()

disp_corr_time2 = vel_corr_time2.copy()
disp_corr_time2.integrate()

# -----------------------------------------------------------------------------
# OUTPUT
# -----------------------------------------------------------------------------
scale = 1.0e3
scaled = 1.0e6

print("-----------------------------------------------")
print("mean residual displacement:")
print(
    "frequency domain (coh): %.3f +/- %.3f mm"
    % (mean_disp_tr1 * scale, sigma_tr1 * scale)
)
print(
    "frequency domain (CaW): %.3f +/- %.3f mm"
    % (mean_disp_tr2 * scale, sigma_tr2 * scale)
)
print(
    "time domain           : %.3f +/- %.3f mm"
    % (mean_disp_tt * scale, sigma_tt * scale)
)
print("")
print("-----------------------------------------------")
print("height of seismometer mass:")
print("frequency domain (coh): %.3f +/- %.3f mm" % (h_tr1 * scale, std_tr1 * scale))
print("frequency domain (CaW): %.3f +/- %.3f mm" % (h_tr2 * scale, std_tr2 * scale))
print("time domain           : %.3f +/- %.3f mm" % (h_tt * scale, std_tt * scale))
print("theoretical           : %.3f +/- %.3f mm" % (h_ttheo * scale, std_ttheo * scale))
print("measured              : %.3f +/- %.3f mm" % (h_m * scale, std_m * scale))
print("-----------------------------------------------")
print("")
-----------------------------------------------
mean residual displacement:
frequency domain (coh): 0.042 +/- 0.005 mm
frequency domain (CaW): 0.016 +/- 0.002 mm
time domain           : 0.042 +/- 0.005 mm

-----------------------------------------------
height of seismometer mass:
frequency domain (coh): 57.400 +/- 0.235 mm
frequency domain (CaW): -6.458 +/- 0.091 mm
time domain           : 57.400 +/- 0.235 mm
theoretical           : 57.161 +/- 0.234 mm
measured              : 57.500 +/- 10.000 mm
-----------------------------------------------

Plots#

Uncomment the following lines in case you want to use latex for type setting

# params = {
#     'text.usetex': True,
#     'text.latex.preamble': [
#         r'\usepackage{cmbright}', r'\usepackage{amsmath}']}
# plt.rcParams.update(params)
plt.rcParams["figure.figsize"] = 7.1, 9.6
sizeOfFont = 12
fontProperties = {"weight": "normal", "size": sizeOfFont}
rc("font", **fontProperties)

# -----------------------------------------------------------------------------
# colors and linestyles
# define colors
al_trig = 0.1

c_trig_on = (0, 0, 0)
c_trig_off = (0, 0, 0)

c_angle = (1, 0, 0)
c_vel = (0, 0, 0)

c_time = (0, 0, 1)
c_time2 = (0, 0.8, 1)
c_coh = (1, 0.54, 0)
c_freq = (0, 0.8, 0)

c_tdisp = (0, 0, 0)

# define linestyles
ls_trig_on = "-"
ls_trig_off = "-"

ls_angle = "-"
ls_vel = "-"

ls_time = "-"
ls_time2 = "--"
ls_coh = "--"
ls_freq = ":"

ls_tdisp = ":"

# define linewidth
lw_trig_on = 2.0
lw_trig_off = 2.0

lw_angle = 2.0
lw_vel = 2.0

lw_time = 1.5
lw_time2 = 1.5
lw_coh = 2.5
lw_freq = 2.5

lw_tdisp = 2.0

# define labels
l_trig = "step table movement"

l_angle = "tilt angle"
l_vel = "NOT corrected"

l_time = "corr. time domain"
l_time2 = "corr. time domain with disp"
l_coh = "corr. frequency domain (adapted)"
l_freq = "corr. frequency domain (CaW2000)"

l_tdisp = "theo. displacement"
# END colors and linestyles
gridspec = dict(hspace=0.0, height_ratios=[1, 1, 0.2, 1, 1])
fig, axs = plt.subplots(nrows=5, ncols=1, gridspec_kw=gridspec)
axs[2].set_visible(False)

ax0 = axs[0]
ax2 = axs[1]
ax3 = axs[3]
ax31 = ax3.twinx()
ax4 = axs[4]

(line_angle,) = ax0.plot(
    sec,
    ts[0].data * scale,
    color=c_angle,
    linestyle=ls_angle,
    linewidth=lw_angle,
    label=l_angle,
)

for i in range(len(on)):
    p_trig = ax0.axvspan(on[i], off[i], alpha=al_trig, color=c_trig_on)

(line_vel,) = ax2.plot(
    sec,
    vel_orig[0].data * scale,
    color=c_vel,
    linestyle=ls_vel,
    linewidth=lw_vel,
    label=l_vel,
)

(line_time,) = ax2.plot(
    sec,
    tt_vel[0].data * scale,
    color=c_time,
    linestyle=ls_time,
    linewidth=lw_time,
    label=l_time,
)

for i in range(len(on)):
    ax2.axvspan(on[i], off[i], alpha=al_trig, color=c_trig_on)

(line_angle,) = ax31.plot(
    sec,
    ts[0].data * scale,
    color=c_angle,
    linestyle=ls_angle,
    linewidth=lw_angle,
    label=l_angle,
)

for i in range(len(on)):
    p_trig = ax31.axvspan(on[i], off[i], alpha=al_trig, color=c_trig_on)

(line_time,) = ax3.plot(
    sec,
    tt_vel[0].data * scale,
    color=c_time,
    linestyle=ls_time,
    linewidth=lw_time,
    label=l_time,
)

(line_coh,) = ax3.plot(
    sec,
    tr1_vel[0].data * scale,
    color=c_coh,
    linestyle=ls_coh,
    linewidth=lw_coh,
    label=l_coh,
)

(line_freq,) = ax3.plot(
    sec,
    tr2_vel[0].data * scale,
    color=c_freq,
    linestyle=ls_freq,
    linewidth=lw_freq,
    label=l_freq,
)

(line_time2,) = ax3.plot(
    sec,
    vel_corr_time2[0].data * scale,
    color=c_time2,
    linestyle=ls_time2,
    linewidth=lw_time2,
    label=l_time2,
)

for i in range(len(on)):
    ax4.axvspan(on[i], off[i], alpha=al_trig, color=c_trig_on)

    (line_time_d,) = ax4.plot(
        time_tt[i],
        disp_tt[i] * scaled,
        color=c_time,
        linestyle=ls_time,
        linewidth=lw_time,
        label=l_time,
    )

    (line_coh_d,) = ax4.plot(
        time_tr1[i],
        disp_tr1[i] * scaled,
        color=c_coh,
        linestyle=ls_coh,
        linewidth=lw_coh,
        label=l_coh,
    )

    (line_freq_d,) = ax4.plot(
        time_tr2[i],
        disp_tr2[i] * scaled,
        color=c_freq,
        linestyle=ls_freq,
        linewidth=lw_freq,
        label=l_freq,
    )

    (line_time_d2,) = ax4.plot(
        time_tt2[i],
        disp_tt2[i] * scaled,
        color=c_time2,
        linestyle=ls_time2,
        linewidth=lw_time2,
        label=l_time2,
    )

(line_theo_d,) = ax4.plot(
    sec, r * scaled, color=c_tdisp, linestyle=ls_tdisp, label=l_tdisp
)

ax0.set_ylabel("rotation angle [mrad]", color=c_angle)

ax2.set_ylabel("velocity [mm/s]")

ax3.set_ylabel("velocity [mm/s]")
ax31.set_ylabel("rotation angle [mrad]", color=c_angle)

ax4.set_ylabel("displacement [mum]")
ax4.set_xlabel("time [s]")

ax0.tick_params("y", colors=c_angle)
ax31.tick_params("y", colors=c_angle)

ax0.tick_params(direction="in")
ax2.tick_params(direction="in")
ax3.tick_params(direction="in")
ax31.tick_params(direction="in")
ax4.tick_params(direction="in")

ax0.set_xticklabels([])
ax31.set_xticklabels([])
ax3.set_xticklabels([])

ax0.text(-16, 0.26, "(a)")
ax3.text(53.32, 0.13, "(b)")

# legend
ba = (-0.098, 3.2)
lines = (
    p_trig,
    line_angle,
    line_vel,
    line_theo_d,
    line_coh,
    line_freq,
    line_time,
    line_time2,
)
labels = (l_trig, l_angle, l_vel, l_tdisp, l_coh, l_freq, l_time, l_time2)
plt.legend(
    lines, labels, loc=ba, bbox_transform=None, borderaxespad=0.0, frameon=False, ncol=2
)

ax0.set_xlim(zoom00, zoom01)
ax2.set_xlim(zoom00, zoom01)
ax3.set_xlim(zoom0, zoom1)
ax31.set_xlim(zoom0, zoom1)
ax4.set_xlim(zoom0, zoom1)

plt.subplots_adjust(top=0.868, bottom=0.053, left=0.118, right=0.88)

zoom_effect01(ax2, ax3, 54.16, 55.00)

plt.gcf().savefig("tilt_correction_step_table.png")

plt.show()
tilt

Total running time of the script: ( 0 minutes 1.744 seconds)

Gallery generated by Sphinx-Gallery