Short background

Ambient noise tomography of regional and continental scales is much benefited from the system composed of the atmosphere, ocean and the solid earth. Ambient noise below 1Hz is largely due to oceans, and interactions between ocean waves and/or the sea floor generate pressure impulse propagating into the solid earth. Land seismic stations are seizing seismic waves which originating from microseisms, and these seismic waves include surface waves and body waves. Individuals can easily retrieve surface waves from ambient noise cross-correlations. Body wave extraction from ambient noise, however, remains difficult. We often explore body waves from ambient noise using array stacking techniques for array stacking leads to high signal-to-noise ratios. Here we perform the body wave source location utilizing beamforming and back-projection methods, and we process one-day seismic recordings by a seismic array in China, which reveals that the source location is in the North Atlantic Ocean, the south of Iceland.

Data and Methods

We collect one-day continuous seismic data of vertical component(here BHZ, 2008-03-10) recorded by a seismic array nearly in the center part of China (see the following figure), and compute the cross-spectral matrix using one-hour length segment with a overlap of 50% (1800 seconds): $$ C(\omega) = \frac{1}{N}\sum_{n=1}^N\boldsymbol{u}_n(\omega) \cdot \boldsymbol{u}_n^H(\omega), \tag{1} $$ in which, $\boldsymbol{u}_n(\omega)=[u^1_n(\omega), u^2_n(\omega), \cdots, u^M_n]^T$ is the Fourier spectra of the $n_{th}$ segment data recorded by $M$ stations, $H$ denotes Hermitian transpose, and $N$ is the total number of data segments. Then we create steering vector as follows, $$ \boldsymbol{a}(\omega; \boldsymbol{s}, \boldsymbol{x}) = [e^{-i\omega \boldsymbol{s} \cdot \boldsymbol{x}_1}, e^{-i\omega \boldsymbol{s} \cdot \boldsymbol{x}_2}, \cdots, e^{-i\omega \boldsymbol{s} \cdot \boldsymbol{x}_M}]^T, \tag{2} $$

where, $\boldsymbol{s}$ and $\boldsymbol{x}_m$ separately indicate horizontal slowness (ray parameter for body waves) and position of the $m_{th}$ station. The beam power is given by $$ B(\omega; \boldsymbol{s}) = \boldsymbol{a}^H \boldsymbol{C}(\omega) \boldsymbol{a}. \tag{3} $$ For more details about beamforming, someone can refer to this blog.

An Example of Locating the distant P wave source

We first import required Python libraries.

1
2
3
4
5
6
7
8
import numpy as np
import matplotlib.pyplot as plt
from obspy import read
from glob import glob
from mpl_toolkits.basemap import Basemap
from scipy.signal.windows import hann
import matplotlib as mpl
from obspy.taup import TauPyModel

The 13 seismic stations are installed in the center part of China, and we collect the data of March 10, 2008 to perform this work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
files = sorted(glob('2008-03-10/*BHZ.SAC'))
sta = {}
with open('X4_IRIS_BHZ.txt', 'r') as fin:
    for line in fin.readlines():
        tmp = line.strip().split('|')
        sta[tmp[1]] = [float(tmp[3]), float(tmp[2])]
xy = []        
for f in files:
    tmp = f.split('/')[-1].split('.')[1]
    xy.append(sta[tmp])
xy = np.array(xy)

plt.figure(figsize=(12, 4.5))
plt.subplot2grid((1, 3), (0, 0), rowspan=1, colspan=2)
bmap1 = Basemap(projection='cyl', lon_0=0)
bmap1.shadedrelief(scale=0.2)
xx, yy = bmap1(106, 34)
bmap1.scatter(xx, yy, marker='v', s=200, facecolor='#87CEFA', edgecolor='k', lw=1, alpha=1)
bmap1.drawmeridians(meridians=np.linspace(-180, 180, 7),
                  labels=[0, 0, 0, 1], color='none', fontsize=17)
bmap1.drawparallels(circles=np.linspace(-90, 90, 7),
                  labels=[1, 0, 0, 0], color='none', fontsize=17)

plt.subplot2grid((1, 3), (0, 2), rowspan=1, colspan=1)
bmap2 = Basemap(projection='merc', llcrnrlat=30, llcrnrlon=100,
              urcrnrlon=110, urcrnrlat=39)
bmap2.shadedrelief(scale=1)
xx, yy = bmap2(xy[:, 0], xy[:, 1])
bmap2.drawcoastlines(color='gray')
bmap2.scatter(xx, yy, marker='v', s=150, facecolor='#87CEFA', edgecolor='k', lw=1, alpha=1)
bmap2.drawmeridians(meridians=np.linspace(100, 110, 6),
                  labels=[0, 0, 0, 1], color='none', fontsize=13)
bmap2.drawparallels(circles=np.linspace(30, 40, 6),
                  labels=[1, 0, 0, 0], color='none', fontsize=13)
plt.show()

station map

It is worth noting that we compute the beam power in Cartesian coordinate system while we only have the longitudes and latitudes of these stations. Thus, we have to convert these longitudes and latitudes to Cartesian coordinates, so we define the following useful functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def distance(la1, lo1, LA2, LO2, R=6371):
    d2r = np.pi / 180
    dla = (LA2-la1) * d2r
    dlo = (LO2-lo1) * d2r
    a = np.sin(dla/2) ** 2 + np.cos(la1*d2r) * np.cos(LA2*d2r) * np.sin(dlo/2) ** 2
    c = 2 * np.arctan2(a**0.5, (1-a)**0.5)
    return R * c * 1e3

def azimuth(la1, lo1, LA2, LO2):
    d2r = np.pi / 180
    dla = (LA2 - la1) * d2r
    dlo = (LO2 - lo1) * d2r
    y = np.sin(dlo) * np.cos(LA2*d2r)
    x = np.cos(la1*d2r) * np.sin(LA2*d2r) - np.sin(la1*d2r) * np.cos(LA2*d2r) * np.cos(dlo)
    th = np.arctan2(y, x)
    return (th / d2r + 360) % 360

def lola2xy(xy):
    n = xy.shape[0]
    lo0 = xy[:, 0].mean()
    la0 = xy[:, 1].mean()
    dist = distance(la0, lo0, xy[:, 1], xy[:, 0])
    az = azimuth(la0, lo0, xy[:, 1], xy[:, 0]) * np.pi / 180
    new_xy = np.zeros_like(xy)
    new_xy[:, 0] = np.sin(az) * dist
    new_xy[:, 1] = np.cos(az) * dist
    return new_xy
def process_data(d, operator, p=0.01):
    nd = len(d)
    pn = int(nd*p)
    h = hann(2*pn)
    d -= d.mean()
    d[:pn] *= h[:pn]
    d[-pn:] *= h[-pn]
    fd = np.fft.fft(d)
    fd /= np.convolve(np.abs(fd), operator, 'same')
    fd[np.isnan(fd)] = 0.
    return fd

We need to estimate the average cross-spectral matrix of seismic data by this array before compute the beam power.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
st = read('2008-03-10/*BHZ.SAC')
utcb = st[0].stats.starttime
utce = st[0].stats.endtime
dt = st[0].stats.delta; fs = 1 / dt
xy = []
for tr in st:
    if utcb > tr.stats.starttime:
        utcb = tr.stats.starttime
    if utce < tr.stats.endtime:
        utce = tr.stats.endtime
    xy.append(sta[tr.stats.station])
xy = np.array(xy)
xy = lola2xy(xy)
n = xy.shape[0]
nd = int((utce-utcb)/dt)
data = np.zeros((n, nd+50))
for ti, tr in enumerate(st):
    n0 = int((tr.stats.starttime-utcb)/dt)
    data[ti, n0: n0+tr.stats.npts] = tr.data

pt = 3600; pn = int(pt/dt); on = pn // 2
hn = 15; operator = np.ones(hn*2+1) / (hn*2+1)
tmp_data = []
n1 = 0
while n1 < data.shape[1]-pn:
    tmp = []
    n2 = n1 + pn
    for i in range(n):
        tmp.append(process_data(data[i, n1:n2], operator))
    tmp_data.append(tmp)
    n1 += on
tmp_data = np.array(tmp_data)
f1 = 1 / 9; f2 = 1 / 5
fn1 = int(f1*pn/fs); fn2 = int(f2*pn/fs); nf = fn2 - fn1 +1
c = np.zeros((nf, n, n), dtype=complex)
u = np.zeros((n, 1), dtype=complex)
for i in range(fn1, fn2+1):
    for j in range(tmp_data.shape[0]):
        u[:, 0] = tmp_data[j, :, i]
        c[i-fn1] += u @ np.conjugate(u.T)

Now we compute the beam power using formula $(1)$ to estimate the back-azmith and corresponding ray parameter of the potential body wave.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
s1 = 0; s2 = 10; ns = 101
g = 111194.92664455
s = np.linspace(s1, s2, ns) / g
b1 = 0; b2 = 360; nb = 181
b = np.linspace(b1, b2, nb) / 180 * np.pi
p1 = np.zeros((ns, nb), dtype=complex)
a = np.zeros((n, 1), dtype=complex)
for i in range(ns):
    for j in range(nb):
        shift = -s[i] * (np.sin(b[j])*xy[:, 0] + np.cos(b[j])*xy[:, 1])
        for k in range(fn1, fn2+1):
            w = 2 * np.pi * k * fs / pn
            a[:, 0] = np.exp(-1j*w*shift)
            cc = c[k-fn1].copy(); cc /= np.abs(cc); cc[np.isnan(cc)] = 0.
            p1[i, j] += np.conjugate(a.T) @ cc @ a
p1 = np.abs(p1) / nf / n**2

Someone may find that we normalize the beam power using the number of frequency points and the number of stations, which results in the coherence of signals caught by this array. beam coherence

The maximum of backazimuth-slowness beam coherence panel suggests that the signal source comes from the direction ranging 315-360 degress with horizonral slowness ranging 4-6 sec/degree, and thus we identify this seismic phase as P wave. We calculate the ray parameter of P wave using the taup module, and the slowness-distance curve is shown by the following figure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
model = TauPyModel(model='iasp91')
dd = 0.02
d = np.arange(0, 95+dd, dd)
pw = np.zeros((len(d), 2))
phase = ['P']
for i in range(len(d)):
    ar = model.get_travel_times(source_depth_in_km=0,
                                  distance_in_degree=d[i],
                               phase_list=phase)
    pw[i] = np.array([d[i], ar[0].ray_param])
pw[:, 1] = pw[:, 1] * np.pi / 180

plt.figure(figsize=(8, 4))
plt.plot(pw[:, 0], pw[:, 1], lw=2.5, color='#00BFFF')
plt.xlabel('Distance (degree)', fontsize=18)
plt.ylabel('Ray parameter (sec/degree)', fontsize=18)
plt.gca().tick_params(labelsize=15)
plt.xlim(30, 95)
plt.ylim(4, 10)
plt.grid(ls=(10, (6, 5)))
plt.title('Ray parameter of seismic phase P', fontsize=17)
plt.show()

slowness-distance

According to the beam coherence and the slowness-distance curve of P wave, we estimate that the distance between the source location the seismic array center is about 70 degrees. We utilize grid-searching rule to find the source location by projecting the backazimuth-slowness beam coherence to geographical coordinates.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def distance_matrix(la1, lo1, LA2, LO2):
    d2r = np.pi / 180
    dla = (LA2-la1) * d2r
    dlo = (LO2-lo1) * d2r
    a = np.sin(dla/2) ** 2 + np.cos(la1*d2r) * np.cos(LA2*d2r) * np.sin(dlo/2) ** 2
    c = 2 * np.arctan2(a**0.5, (1-a)**0.5)
    return c * 180 / np.pi

def azimuth_matrix(la1, lo1, LA2, LO2):
    d2r = np.pi / 180
    dla = (LA2 - la1) * d2r
    dlo = (LO2 - lo1) * d2r
    y = np.sin(dlo) * np.cos(LA2*d2r)
    x = np.cos(la1*d2r) * np.sin(LA2*d2r) - np.sin(la1*d2r) * np.cos(LA2*d2r) * np.cos(dlo)
    th = np.arctan2(y, x)
    return (th / d2r + 360) % 360

xy = []
for tr in st:
    xy.append(sta[tr.stats.station])
xy = np.array(xy)
lo0 = xy[:, 0].mean()
la0 = xy[:, 1].mean()
ns, nb = p1.shape
ss = np.linspace(0, 10, ns)
bb = np.linspace(0, 360, nb)
ds = ss[1] - ss[0]
db = bb[1] - bb[0]
lo1 = -180; lo2 = 180; nlo = 181
la1 = -90; la2 = 90; nla = 91
lo = np.linspace(lo1, lo2, nlo)
la = np.linspace(la1, la2, nla)
LO, LA = np.meshgrid(lo, la)
AZ = azimuth_matrix(la0, lo0, LA, LO)
DIST = distance_matrix(la0, lo0, LA, LO)
bp = np.zeros((nla, nlo))
for i in range(nla):
    for j in range(nlo):
        index = np.argmin(np.abs(DIST[i, j]-pw[:, 0]))
        if np.abs(DIST[i, j]-pw[index, 0]) > 0.05 or pw[index, 1] > ss.max():
            continue
        si = int((pw[index, 1]-ss.min())/ds)
        ai = int((AZ[i, j]-bb.min())/db)
        bp[i, j] = p1[si, ai]

plt.figure(figsize=(12, 6))
bmap = Basemap(projection='cyl', lon_0=0)
plt.contourf(LO, LA, bp, cmap=cmap, levels=101)
cbar = plt.colorbar(shrink=0.8)
cbar.ax.tick_params(labelsize=15)
cbar.set_label('Cohenrence', fontsize=15)
bmap.fillcontinents(color='#3CB371')
bmap.drawcoastlines(linewidth=0.3, color='gray')
xx, yy = bmap(lo0, la0)
bmap.scatter(xx, yy, marker='v', s=150, facecolor='b', alpha=0.6, zorder=2)
bmap.drawparallels(circles=np.linspace(-90, 90, 7), labels=[1, 0, 0, 0], fontsize=17, color='none')
bmap.drawmeridians(meridians=np.linspace(-180, 180, 9), labels=[0, 0, 0, 1], fontsize=17, color='none')
plt.show()

source lacation This back-projection result shows that the possible source location is in the North Atlantic Ocean, which may relate to the irregular topography of the middle ocean rige in this place.

Perspective

We use this simple example to explain the process of using body waves from ambient noise cross-correlations to investigate the potential source locations of these indentified seismic phases. Indeed, it still holds many mysteries to reveal in the seismic ambient noise world. Thus, we will continue the journey of exploring the coupling process among the atmosphere, oceans and our solid planet Earth.