Fourier analysis icon - Frequency spectrum representation

Fourier Analysis of Seismic Data

Task Description

In this tutorial, our aim is to compute the Fourier spectra within the signal and the noise windows on the waveforms. This process involves a series of crucial pre-processing steps which enhance the quality of the data and ensure that the subsequent Fourier spectra analysis is applied at a specific portion of the seismic recordings. The pre-processing steps can be summarized as follows:

  • Apply a bandpass filter (1 to 3 Hz) to eliminate unwanted noise from the seismic signal, ensuring that only the relevant frequency band is retained for further analysis.
  • Select the P & S wave arrival times to define the edges of the signal and the noise windows later.
  • Set the window edges according to the selected arrivals (signal window: from S arrival to S arrival plus window length, noise window: P arrival minus window length to P arrival).
  • Cut the waveforms into signal and noise windows. Both windows will have a consistent duration (window length) of 10 seconds.
  • Apply a smoothing algorithm (tapering) on the waveforms in order to ensure zero acceleration values at the edges.
  • Compute the fourier spectra on the processed waveforms

Getting data

In this article we will make use of the ObsPy Python library to apply the seismological computations and the Python Matplotlib library to plot the results. For this reason, we will start by initializing the libraries that we will use throughout the rest of the article:

    import matplotlib.pyplot as plt
    from obspy.core import read, UTCDateTime
    import numpy as np

To proceed, start by reading a seismic file which has a MiniSEED file format:

    st = read("20150724_095849_KRL1.mseed")
    print(st)
    3 Trace(s) in Stream:
    .KRL1..E | 2015-07-24T09:58:49.000000Z - 2015-07-24T10:01:39.000000Z | 100.0 Hz, 17001 samples
    .KRL1..N | 2015-07-24T09:58:49.000000Z - 2015-07-24T10:01:39.000000Z | 100.0 Hz, 17001 samples
    .KRL1..Z | 2015-07-24T09:58:49.000000Z - 2015-07-24T10:01:39.000000Z | 100.0 Hz, 17001 samples

To illustrate the waveforms, we will use the plot() method of the Stream object:

    # plot the recordings
    st.plot()

Application of the pre-processing steps

Applying a bandpass filter

Initiate the process by applying a bandpass filter to the records within the frequency range of 1 to 3 Hz. This step aims to eliminate surrounding noise and facilitate the arrival selection. Utilize the ObsPy filter() function for this purpose:

    # apply an inplace bandpass filter of 1-3 Hz
    st.filter('bandpass', freqmin=1, freqmax=5)

    # plot the recordings
    st.plot()

Recordings of a record after applying a bandpass filter of 1-3 Hz Recordings after applying a 1-3 Hz bandpass filter, showing time (x-axis) and acceleration (y-axis) for three components.

Selection of the P and S wave arrivals

It’s clear from the vertical filtered component (Z), that the P wave arrival occurs roughly at 09:59:03 and the S wave arrival at 09:59:21. Convert these into ObsPy UTCDateTime objects and convert the arrival values as total seconds from the starting date:

    # Define the P and S wave arrivals as UTCDateTime objects
    Parr = UTCDateTime('2015-07-24 09:59:03')
    Sarr = UTCDateTime('2015-07-24 09:59:21')

    # get the first trace
    first_trace = st.traces[0]

    # Get the starting date of the records from the first trace of the stream object
    start_date = first_trace.stats.starttime

    # Get the arrivals in seconds from the start date
    Parr_sec = Parr - start_date
    Sarr_sec = Sarr - start_date

To plot the P-wave and S-wave arrivals on the waveforms, we use Matplotlib’s axvline() function, to mark these time points with vertical lines. Here’s an example of how you can do it in Python:

    # Initialize a matplotlib figure and axes with the total
    # number of plots equal to the number of traces len(st)
    fig, ax = plt.subplots(len(st), 1)

    # Loop through all the traces in the stream object (st)
    for n, tr in enumerate(st):
        # get the time series time information of the current trace
        xdata = tr.times()

        # get the acceleration data of the current trace
        ydata = tr.data

        # plot the graph with legend, the trace component or channel
        ax[n].plot(xdata, ydata, label=tr.stats.channel, lw=1)

        # add two vertical lines that represent the arrivals
        ax[n].axvline(x=Parr_sec, ymin=0, ymax=1, lw=2, ls='--', color='red', label='P arrival')
        ax[n].axvline(x=Sarr_sec, ymin=0, ymax=1, lw=2, ls='--', color='black', label='S arrival')

        # add the legend on the plot
        ax[n].legend(loc='upper right')

    # adjust the subplots so they do not overlap
    plt.tight_layout()

Arrivals of the P and the S waves Arrivals of the P (red dashed line) and the S (black dashed line) waves represented by two vertical lines

Initializing the signal and noise windows

At this stage we draw two windows using Matplotlib’s fill_betweenx() function. We utilize these windows, to calculate the Fourier Spectra, for the signal part of the waveform and for the noise part. Both windows will share the same duration or length of 10 seconds. The first window will start from the S wave arrival and it will end 10 seconds later and the second one (noise) will begin 10 seconds before the P wave arrival and it will end at the P arrival. Let’s illustrate these windows:

    # set the window length
    window_length = 10

    # Initialize a matplotlib figure and axes with the total
    # number of plots equal to the number of traces len(st)
    fig, ax = plt.subplots(len(st), 1)

    # Loop through all the traces in the stream object (st)
    for n, tr in enumerate(st):
        # get the time series time information of the current trace
        xdata = tr.times()

        # get the acceleration data of the current trace
        ydata = tr.data

        # plot the graph with legend, the trace component or channel
        ax[n].plot(xdata, ydata, label=tr.stats.channel)

        # add two vertical lines that represent the arrivals
        ax[n].axvline(x=Parr_sec, ymin=0, ymax=1, lw=2, ls='--', color='red', label='P arrival')
        ax[n].axvline(x=Sarr_sec, ymin=0, ymax=1, lw=2, ls='--', color='black', label='S arrival')

        # get the min and max acceleration values
        min_y_value = tr.data.min()
        max_y_value = tr.data.max()

        # create the signal and the noise window on the waveforms
        ax[n].fill_betweenx([min_y_value,max_y_value], x1=Sarr_sec, x2=Sarr_sec+window_length, alpha=0.5, facecolor='orange', zorder=2, label='signal window')
        ax[n].fill_betweenx([min_y_value,max_y_value], x1=Parr_sec-window_length, x2=Parr_sec, alpha=0.5, facecolor='red', zorder=2, label='noise window')


        # add the legend on the plot
        ax[n].legend(loc='upper right')

    # adjust the subplots so they do not overlap
    plt.tight_layout()

generation of signal and noise windows to calculate the fourier spectra Define the signal and the noise windows to calculate the Fourier Spectra. Both will have the same duration of 10 seconds. The signal window starts at the S wave arrival and ends 10 seconds later. Similarly, the noise window initiates 10 seconds prior the P wave arrival and ends at the P wave arrival.

Trimming the waveforms

To continue, we trim the waveforms between the previously defined, windows using the ObsPy trim() function. Because the trimming happens inplace, create copies of the orginal stream using the copy() method:

    # noise window: from Parr-window_length to Parr
    # signal window: from Sarr to Sarr+window_length
    # Trim the stream at the two windows
    st_signal = st.copy().trim(starttime=start_date+Sarr_sec, endtime=start_date+Sarr_sec+window_length)
    st_noise = st.copy().trim(starttime=start_date+Parr_sec-window_length, endtime=start_date+Parr_sec)

Then plot the trimmed waveforms for each window and for each component (3 components for the signal part and 3 for the noise part):

    # Initialize a matplotlib figure and set the total number of rows equal to
    # the number of traces plus 3 (3 signal components and 3 noise components)
    fig, ax = plt.subplots(len(st)+3, 1, figsize=(8,10))

    # Loop through all the traces in the noise Stream
    # and plot them in the first 3 plots of the figure
    for n, tr_noise in enumerate(st_noise):
        # get the time series time information as seconds from the starting date
        xdata = tr_noise.times()

        # get the acceleration data of the current trace
        ydata = tr_noise.data

        # plot the graph
        ax[n].plot(xdata, ydata, label=f'noise window, channel: {tr_noise.stats.channel}')

        # add the legend
        ax[n].legend(loc='upper left')

    # Loop through all the traces in the signal Stream
    # and plot them in the next 3 plots of the figure
    for n, tr_signal in enumerate(st_signal, start=3):
        # get the time series time information as seconds from the starting date
        xdata = tr_signal.times()

        # get the acceleration data of the current trace
        ydata = tr_signal.data

        # plot the graph
        ax[n].plot(xdata, ydata, label=f'signal window, channel: {tr_signal.stats.channel}')

        # add the legend
        ax[n].legend(loc='upper left')

    # adjust the subplots so they do not overlap
    plt.tight_layout()

Trimmed waveforms at the noise and the signal windows for all the components Trimmed waveforms at the two windows (signal, noise), for each component (E, N, Z)

Applying a smoothing algorithm

At the last step, generate another copy of two obects from the previous trimmed recordings and smooth the left and the right side of the waveforms, using the obspy taper() function:

    # taper the waveforms at the respective windows 30% on the left and 30% on the right side
    # again use the copy() function to apply the taper on new stream object
    st_signal_taper = st_signal.copy().taper(side='both', max_percentage=0.3)
    st_noise_taper = st_noise.copy().taper(side='both', max_percentage=0.3)

Lastly, show the results:

    # Initialize a matplotlib figure and axes with the total  number of rows equal to
    # the number of traces plus 3 (3 signal traces and 3 noise traces)
    fig, ax = plt.subplots(len(st)+3, 1, figsize=(8,10))

    # Loop through the number of traces in the noise Stream object
    for i in range(len(st_noise)):
        # plot the trimmed waveforms with blue color
        ax[i].plot(st_noise[i].times(), st_noise[i].data, label=f'trimmed noise window, channel: {st_noise[i].stats.channel}', color="blue")

        # plot the tapered waveforms with orange color
        ax[i].plot(st_noise_taper[i].times(), st_noise_taper[i].data, label='tapered', lw=3, color="orange")

        # add the legend
        ax[i].legend(loc='upper center')

    # Loop through the number of traces in the signal Stream object
    for i in range(len(st_signal)):
        # plot the trimmed waveforms with blue color
        ax[i+3].plot(st_signal[i].times(), st_signal[i].data, label=f'trimmed signal window, channel: {st_signal[i].stats.channel}', color="blue")

        # plot the tapered waveforms with orange color
        ax[i+3].plot(st_signal_taper[i].times(), st_signal_taper[i].data, label='tapered', lw=3, color="orange")

        # add the legend
        ax[i+3].legend(loc='upper center')

    # adjust the subplots so they do not overlap
    plt.tight_layout()

Trimmed and tappered waveforms at the noise and the signal window for all the components Trimmed (blue) and tappered (orange) waveforms at the noise and the signal window for all the components

Computation of the fourier spectra

Finally, we can calculate the Fourier Spectra at the noise and the signal window:

    # get the first trace of the signal Stream object
    first_trace = st_signal_taper.traces[0]

    # get the starting date of the recordings
    starttime = first_trace.stats.starttime

    # get the sampling frequency and the sample distance
    fs = first_trace.stats["sampling_rate"]
    delta = first_trace.stats["delta"]

    # calculate the nyquist frequency
    fnyq = fs / 2

    # initialize a dictionary to save the outputs
    fourier_data_dict = {}

    # loop through the traces
    for i in range(len(st_signal_taper)):
        # get a copy of the trace for the singal
        df_s = st_signal_taper[i].copy()

        # get the current trace channel
        channel = df_s.stats.channel

        # get the number of sample points after the trimming
        npts = df_s.stats["npts"]

        # calculate the number of frequencies on the frequency spectra
        sl = int(npts / 2)

        # calculate the frequnecy array to plot the fourier
        freq_x = np.linspace(0 , fnyq , sl)

        # save the frequencies in a variable to use it later at the plotting
        xdata = freq_x

        # compute the fft of the signal
        yf_s = np.fft.fft(df_s.data[:npts])
        y_write_s = delta * np.abs(yf_s)[0:sl]

        # do the same also for the noise part
        # get a copy of the trace for the noise
        df_p = st_noise_taper[i].copy()

        # calculate the fft
        yf_p = np.fft.fft(df_p.data[:npts])
        y_write_p = delta * np.abs(yf_p)[0:sl]

        # add the fft of the signal and noise part to the dictionary for the current trace
        fourier_data_dict[channel] = {"signal": y_write_s, "noise": y_write_p}

And we present the results:

    # initialize a Matplotlib figure to plot the Fourier data
    figfourier, axfourier = plt.subplots(1, len(fourier_data_dict),  figsize=(19,6))

    # create a hex colors list to style the curves
    colors = ['#5E62FF', '#B9BBFF', '#0a0a0a', '#969595', '#b50421', '#fc8b9e']

    color_i = 0
    # loop through each component at the dictionary
    for n, compo in enumerate(fourier_data_dict):
        # loop through the 'noise' and the 'signal' part
        for s_n in fourier_data_dict[compo]:
            # define the ydata with data located in the dictionary created earlier
            ydata = fourier_data_dict[compo][s_n]

            # create a variable to save the legend name of the curves
            label_name = f"{compo}-{s_n}"

            # plot the frequencies and the data
            axfourier[n].plot(xdata, ydata, color=colors[color_i], lw=1, label=label_name)
            color_i += 1

        # style the graphs
        axfourier[n].legend(loc='upper left', fontsize=11, facecolor='w')
        axfourier[n].set_xscale('log')
        axfourier[n].set_yscale('log')
        axfourier[n].set_xlabel("frequency [Hz]", fontsize=14)
        axfourier[n].grid(color='grey', ls='-', which='both', alpha=0.1)
        axfourier[n].tick_params(axis='both', which='both', labelsize=14)

    plt.tight_layout()

Fourier Spectra of the signal (bold curve) and the noise (muted curve) window for each component of the tappered Stream object Fourier Spectra of the signal (bold curve) and the noise (muted curve) window for each component of the tappered Stream object