2. Compute travel times

This notebook computes the travel times from all the points of a three-dimensional grid to each seismic station. These travel times are necessary for time-shifting the seismic traces when evaluating the beamformed network response at every location of the grid.

This tutorial utilizes the pykonal package to compute travel times given a velocity model. The package documentation and installation procedure are described in the pykonal package documentation. Please acknowledge White et al. (2020) if using pykonal.

Note: although pykonal handles computing the travel times in a three-dimensional velocity model, the example below uses a one-dimensional velocity model.

Contents

[2]:
import h5py as h5
import numpy as np
import os
import pandas as pd
import tqdm

from matplotlib import pyplot as plt

from obspy import read, read_inventory
from pykonal.solver import PointSourceSolver
from pykonal.transformations import geo2sph

Read velocity model

We wrote the velocity model of Karabulut et al. (2011) in a csv file that we read with pandas. The model is given in meters for the depth and in m/s for the speed values. Everything is converted to km for compatibility with pykonal.

[3]:
FILEPATH_VELOCITY = "../data/velocity_model_Karabulut2011.csv"
[4]:
# Read velocity model
velocity_layers = pd.read_csv(
    FILEPATH_VELOCITY,
    usecols=[1, 2, 4],
    names=["depth", "P", "S"],
    skiprows=1,
    index_col="depth",
    )

# Convert meters to kilometers
velocity_layers *= 1e-3
velocity_layers.index *= 1e-3

# Show table
velocity_layers.T
[4]:
depth -2.0 0.0 1.0 2.0 3.0 4.0 5.0 6.0 8.0 10.0 12.0 14.0 15.0 20.0 22.0 25.0 32.0 77.0
P 2.90 3.0 5.60 5.70 5.80 5.90 5.95 6.05 6.10 6.15 6.20 6.25 6.30 6.40 6.50 6.70 8.00 8.045
S 1.67 1.9 3.15 3.21 3.26 3.41 3.42 3.44 3.48 3.56 3.59 3.61 3.63 3.66 3.78 3.85 4.65 4.650
[5]:
ax = velocity_layers.plot(
    drawstyle="steps-post",
    ylabel="Wave Velocity (km/s)",
    xlabel="Depth (km)",
    title="1D velocity model from Karabulut et al. 2011",
    grid=True,
)
../../_images/tutorial_notebooks_2_travel_times_5_0.png

Interpolate velocity model at depth

We compute the travel times on a finer grid than the model grid, so we need to interpolate the model. We define 30 depths between 30 km and -2 km.

[6]:
depths = np.linspace(30.0, -2.0, 32)

Then we interpolate the velocity at the given depths using pandas.DataFrame method reindex.

[7]:
velocity_layers_interp = velocity_layers.reindex(depths, method="ffill")

We can then compare the natural (layered) and interpolated models as a function of depth

[8]:
velocity_layers.plot(drawstyle="steps-post")
velocity_layers_interp.sort_values("depth").plot(
    drawstyle="steps-post",
    xlabel="Depth (km)",
    ylabel="Speed (km/s)",
    title="1D velocity model from Karabulut et al. 2011",
    ax=plt.gca(),
    grid=True,
    figsize=(12, 8),
    marker="s",
    ls=""
)

# Labels and legends
plt.axvspan(depths.min(), depths.max(), alpha=0.2)
plt.legend(["P", "S", "P interpolated", "S interpolated", "Domain"])
plt.show()
../../_images/tutorial_notebooks_2_travel_times_11_0.png

Expand model laterally

Because pykonal uses three-dimensional coordinate systems, we need to cast the one-dimensional velocity model onto a three-dimensional grid. The following define the grid in the longitude and latitude dimensions.

[9]:
velocity_layers_interp
[9]:
P S
depth
30.000000 6.70 3.85
28.967742 6.70 3.85
27.935484 6.70 3.85
26.903226 6.70 3.85
25.870968 6.70 3.85
24.838710 6.50 3.78
23.806452 6.50 3.78
22.774194 6.50 3.78
21.741935 6.40 3.66
20.709677 6.40 3.66
19.677419 6.30 3.63
18.645161 6.30 3.63
17.612903 6.30 3.63
16.580645 6.30 3.63
15.548387 6.30 3.63
14.516129 6.25 3.61
13.483871 6.20 3.59
12.451613 6.20 3.59
11.419355 6.15 3.56
10.387097 6.15 3.56
9.354839 6.10 3.48
8.322581 6.10 3.48
7.290323 6.05 3.44
6.258065 6.05 3.44
5.225806 5.95 3.42
4.193548 5.90 3.41
3.161290 5.80 3.26
2.129032 5.70 3.21
1.096774 5.60 3.15
0.064516 3.00 1.90
-0.967742 2.90 1.67
-2.000000 2.90 1.67
[10]:
longitudes = np.linspace(30.20, 30.45, 25)
# sample latitudes in decreasing order to get corresponding colatitudes in increasing order (see explanation further)
latitudes = np.linspace(40.76, 40.60, 16)

We then expand the velocity vector in the longitude and latitude dimensions with xarray. This operation is automatically done with the expand_dim() method.

[11]:
velocity_P = np.zeros((len(depths), len(latitudes), len(longitudes)), dtype=np.float32)
velocity_S = np.zeros((len(depths), len(latitudes), len(longitudes)), dtype=np.float32)
# use numpy's broadcasting rules
velocity_P[...] = velocity_layers_interp["P"].values[:, None, None]
velocity_S[...] = velocity_layers_interp["S"].values[:, None, None]
# store the P- and S-wave velocity models in a dictionary
velocity_model = {
    "P": velocity_P,
    "S": velocity_S
}

Station coordinates

We extract the station coordinates from the XML files.

[12]:
# Get inventories
inventory = read_inventory("../data/processed/*xml")

# Extract stations
stations = [sta for net in inventory for sta in net]
attrs = "longitude", "latitude", "elevation", "code"
stations = [{item: getattr(sta, item) for item in attrs} for sta in stations]

# Turn into dataframe
network = pd.DataFrame(stations).set_index("code")
network["depth"] = -1e-3 * network.elevation

# Save network metadata
network.to_csv("../data/network.csv")

# Show
network

[12]:
longitude latitude elevation depth
code
DC06 30.265751 40.616718 555.0 -0.555
DC07 30.242170 40.667080 164.0 -0.164
DC08 30.250130 40.744438 162.0 -0.162
DD06 30.317770 40.623539 182.0 -0.182
DE07 30.411539 40.679661 40.0 -0.040
DE08 30.406469 40.748562 31.0 -0.031
SAUV 30.327200 40.740200 170.0 -0.170
SPNC 30.308300 40.686001 190.0 -0.190

Show model and stations

This cell selects a slice of the velocity model with the DataArray.sel() and plots it.

[14]:
# Select slice at first latitude index
phase = "S"
velocities_slice = velocity_model[phase][:, 0, :]

# Show velocities
fig = plt.figure(figsize=(12, 8))
img = plt.pcolormesh(longitudes, depths, velocities_slice, cmap="RdBu")
cb = plt.colorbar(img)

# Show stations
plt.plot(network.longitude, network.depth, "wv")

# Labels
ax = plt.gca()
ax.set_xlabel("Longitude (degrees)")
ax.set_ylabel("Depth (km)")
ax.set_title("Velocity model slice from 3D grid")
cb.set_label(f"{phase} velocity (km/s)")
ax.invert_yaxis()

../../_images/tutorial_notebooks_2_travel_times_20_0.png

Compute travel times

The travel times are computed for every station with the Eikonal solver of pykonal. The travel times are then saved into a h5 file for later use.

Warning: For pykonal, we need to give the velocity grid in spherical coordinates \((r, \theta, \varphi)\), which is why we built the grid with decreasing depths and latitudes.

Spherical coordinates: - \(r\): Distance from center or Earth in km (= decreasing depth). - \(\theta\): Polar angle in radians (= co-latitude or, equivalently, decreasing latitude). - \(\varphi\): Azimuthal angle in radians (= longitude).

[15]:
STATION_ENTRIES = ["latitude", "longitude", "depth"]

# Initialize travel times
travel_times = {}

# Reference point
reference_point = geo2sph((latitudes.max(), longitudes.min(), depths.max()))
node_intervals = (
    np.abs(depths[1] - depths[0]),
    np.deg2rad(np.abs(latitudes[1] - latitudes[0])),
    np.deg2rad(longitudes[1] - longitudes[0]),
)

# Loop over stations and phases
for phase in velocity_model:
    travel_times[phase] = {}
    for station in tqdm.tqdm(network.index, desc=f"Travel times {phase}"):

        # Initialize Eikonal solver
        solver = PointSourceSolver(coord_sys="spherical")
        solver.velocity.min_coords = reference_point
        solver.velocity.node_intervals = node_intervals
        velocity = velocity_model[phase]
        solver.velocity.npts = velocity.shape
        solver.velocity.values = velocity.copy()

        # Source
        src_loc = network.loc[station][STATION_ENTRIES].values
        solver.src_loc = np.array(geo2sph(src_loc).squeeze())

        # Solve Eikonal equation
        solver.solve()

        # Update the travel_times dictionary
        tt = solver.tt.values
        # pykonal might produce a singularity at origin
        tt[np.isinf(tt)] = 0
        travel_times[phase][station] = tt
Travel times P:   0%|          | 0/8 [00:00<?, ?it/s]
Travel times P: 100%|██████████| 8/8 [00:03<00:00,  2.01it/s]
Travel times S: 100%|██████████| 8/8 [00:03<00:00,  2.08it/s]

Save travel times and grid coordinates

Save the travel times as a hdf5 file. This format preserves a self-explanatory data structure and supports compression.

[16]:
# build 3D gridded coordinates from depths, latitudes and longitudes vectors
# these are the coordinates of the points in the travel time grid
depths_g, latitudes_g, longitudes_g = np.meshgrid(
    depths, latitudes, longitudes, indexing="ij"
)
[17]:
with h5.File("../data/travel_times.h5", mode="w") as ftt:
    ftt.create_group("source_coordinates")
    ftt["source_coordinates"].create_dataset("depth", data=depths_g)
    ftt["source_coordinates"].create_dataset("latitude", data=latitudes_g)
    ftt["source_coordinates"].create_dataset("longitude", data=longitudes_g)
    for phase in ["P", "S"]:
        ftt.create_group(phase)
        for sta in travel_times[phase]:
            ftt[phase].create_dataset(sta, data=travel_times[phase][sta])

Show travel times at a given station

[18]:
CONTOUR_LEVELS = 20
SEISMIC_PHASE = "S"
station = network.loc["DC06"]

# Show
latitude_id = np.abs(latitudes - station.latitude).argmin()
time_delays = travel_times[SEISMIC_PHASE][station.name]
time_delays = time_delays[:, latitude_id, :]
fig = plt.figure(figsize=(12, 8))
img = plt.contourf(longitudes, depths, time_delays, cmap="RdPu", levels=CONTOUR_LEVELS)

# Colorbar
cb = plt.colorbar(img)
cb.set_label(f"Travel times {SEISMIC_PHASE} (seconds)")

# Station
plt.plot(station.longitude, station.depth, "k.")

# Labels
ax = plt.gca()
ax.invert_yaxis()
ax.set_xlabel("Longitude (degrees)")
ax.set_ylabel("Depth (km)")
ax.set_title(f"Travel times from the seismic station {station.name}")
plt.show()

../../_images/tutorial_notebooks_2_travel_times_27_0.png