Tutorial#

In this tutorial, I will introduce the primary usage of PyFK. The package can be divided into the configuration part, the Green’s function calculation part, and the synthetic waveform generation part.

Configurations#

First, we should generate three instances from the configuration classes, including SourceModel, SeisModel, and Config. SourceModel specifies the source information, such as the source type and the source depth. SeisModel specifies the 1D layered Earth model. Config defines the simulation details, such as the number of points and the epicenter distance. For more detail about the three classes, you can refer to pyfk.config.config.Note the three classes have already been imported into the upper-most level of the package, so you can directly import them as

from pyfk import SourceModel, SeisModel, Config

The SourceModel contains the information of the earthquake depth, the source type, and the source mechanism as has been discussed before. However, when calculating the Green’s function, there is no need to specify the source mechanism (but you can provide one anyway), so you can just run:

source_prem = SourceModel(sdep=16.5, srcType="dc")
print(source_prem)
SourceModel(sdep=16.5, srcType=dc, source_mechanism=None)

source_prem is a SourceModel whose depth is 16.5km, and the source type is double-couple.

And you will also need to define the velocity and attenuation for the model, just as in FK. The model contains the information of the thickness, the S wave speed \(V_s\), the P wave speed \(V_p\), the density \(\rho\), and the attenuation \(Q_s\) and \(Q_p\). To initialize the SeisModel, we have to prepare a numpy array that stores the model information.The model is a 2D numpy array following the same format as in FK, and you can directly use np.loadtxt to load the model file in FK. Each row in the model representsa layer and the columns are for \(thickness\) , \(V_s\) , \(V_p\) , \(\rho\) , \(Q_s\) , \(Q_p\) . Similar to FK, you can use less than 6 columns.

Here we use the PREM model as an example:

from os.path import dirname, join
from pyfk.tests.taup.test_taup import TestFunctionTaup

# we use a function in tests to generate the prem model
prem_data = TestFunctionTaup.gen_test_model("prem")

prem_data[:5,:]
array([[  15.     ,    3.2    ,    5.8    ,    2.6    ,  600.     ,
        1456.     ],
       [   9.4    ,    3.9    ,    6.8    ,    2.9    ,  600.     ,
        1350.     ],
       [  15.6    ,    4.49094,    8.11061,    3.38076,  600.     ,
        1446.     ],
       [  20.     ,    4.48486,    8.10119,    3.37906,  600.     ,
        1446.     ],
       [  20.     ,    4.47715,    8.08907,    3.37688,  600.     ,
        1447.     ]])

We can see it prints the top 5 layers of the PREM model. And based on the model data, we can initialize the SeisModel:

model_prem = SeisModel(model=prem_data)
print(model_prem)
SeisModel(layers=44, flattening=False)

There is also an option to control whether we should flatten the model, described by the keyword flattening. Note this keyword should be specified when initilizing the SeisModel. There is also an option to control whether we are using \(\frac{V_p}{V_s}\) ratio to get \(V_p\) in the model data, controled by the keywork use_kappa. By default, they are all False.

Based on the SourceModel and SeisModel, we can now initialize the Config class. The Config stores the information such as the receiver depth, the epicenter distance, and the number of points in the simulation. Generally speaking, they are similar to the flags in FK, but in a more pythonic way.

import numpy as np

config_prem = Config(
        model=model_prem,
        source=source_prem,
        npt=512,
        dt=0.1,
        receiver_distance=np.arange(10, 40, 10))
print(config_prem)
Config(model=SeisModel(layers=45, flattening=False), source=SourceModel(sdep=16.5, srcType=dc, source_mechanism=None), receiver_distance=[10. 20. 30.], taper=0.3, filter=(0, 0), npt=512, dt=0.1, dk=0.3, smth=1.0, pmin=0.0, pmax=1.0, kmax=15.0, rdep=0.0, updn=all, samples_before_first_arrival=50, suppression_sigma=2.0)

For this example, we are using the model_prem and source_prem defined previously. And our output should be 512 points with 0.1 s interval. The receiver distances are 10km, 20km, and 30km. If you are planning to use degrees instead, simply set degrees=True, and the receiver_distance will be automatically converted to the corresponding distance in km. The default values are set to be the same as FK. One thing to note is that model_prem, is deep copied into config_prem, so you can reuse it in the future without wondering influencing config_prem. However, source_prem is shared with config_prem.

Calculate Green’s function#

After the configuration part, you can simply call pyfk.calculate_gf to calculate the Green’s function.

from pyfk import calculate_gf

gf = calculate_gf(config_prem)
print(gf)
[<obspy.core.stream.Stream object at 0x7efd2a37eb20>, <obspy.core.stream.Stream object at 0x7efd64372610>, <obspy.core.stream.Stream object at 0x7efd297a5490>]

gf is a list of Stream in obspy, the order of Stream is consistent with the order of receiver_distance. Each Stream consists of a list of Trace, and each one represents a component of Green’s function. The order of Trace is the same as FK. For the meaning of these Green’s functions, you might refer to The introduction from Seisman (In Chinese).

Each component of the Green’s function also contains the header information:

print(gf[0][0].stats)
         network: 
         station: 
        location: 
         channel: 
       starttime: 1969-12-31T23:59:58.280712Z
         endtime: 1970-01-01T00:00:49.380712Z
   sampling_rate: 10.0
           delta: 0.1
            npts: 512
           calib: 1.0
             sac: AttribDict({'delta': 0.1, 'b': -1.71928822781415, 'e': 49.480711772185856, 'o': 0.0, 'dist': 10.0, 't1': 3.28071177218585, 't2': 5.927151680565519, 'user1': 143.31697531118135, 'user2': 141.8427788468466, 'npts': 512})

The header information is the same as the result from FK. The event origin time is at 1970-01-01T00:00:00.00000Z, and the starting time of this Trace is calculated based on the first arrival time and the sample numbers before the first arrival, which means you can use the starttime from the header to subtract 1970-01-01T00:00:00.00000Z to get the first arrival. However, this information has alread been stored. For the sac header, t1 is the first arrival of P wave and t2 is the first arrival of S wave. user1 and user2 represent the corresponding trace’s emission angle in degrees.

If we set npt=1 or npt=2 in the Config, calculate_gf will not generate Stream for each distance, but a 1D array. The 1D array represents the static displacement, similar to FK.

config_prem_static = Config(
        model=model_prem,
        source=source_prem,
        npt=2,
        dt=0.1,
        receiver_distance=np.arange(10, 40, 10))
gf_static = calculate_gf(config_prem_static)
print(gf_static[0])
[ 1.82673702e-05  1.17737927e-05  0.00000000e+00  1.15497936e-05
  8.98594656e-06 -1.35277715e-06 -3.14703318e-06 -3.09000029e-06
  9.47091370e-07]

The dt will be automatically set as 1000 in such the case if the user provided dt is smaller than 1000.

Now we can have a view of the Green’s function:

gf[0][0].plot();
../_images/tutorial_8_0.png

Generate synthetic waveform#

By convolving the Green’s function with the source time function, and also consideing the possible azimuth angle influence, we can generate the synthetic waveform. There are mainly two ways to set up the source time function. The first way is similar to FK to create a trapezoid-shaped source time function. The second way is to provide a user-defined Trace containing the source information (only the data attribute is needed at the moment). Note the sampling rate of the Green’s function and the source time function should be identical. As PyFK will not automatically convert the dt in the source time function to be the same as the Green’s function.

In this example, we simply call pyfk.generate_source_time_function to generate a trapezoid shaped source:

from pyfk import generate_source_time_function

source_time_function=generate_source_time_function(dura=4, rise=0.5, delta=gf[0][0].stats.delta)

And we also need to provide a source mechanism. In FK, the source mechanism is provided as a series of number (-M flag in syn). The source mechanism in PyFK should be a list of these numbers. The length of this list should be the same as required by the source type. A more convenient way is that you can provide a global CMT solution file:

from pyfk.tests.sync.test_sync import TestFunctionCalculateSync

test_gcmt_path = TestFunctionCalculateSync.get_sample_gcmt_file_path()
with open(test_gcmt_path,"r") as f:
    for line in f:
        print(line,end='')
 MLI 1976  1  1  1 29 39.60 -28.6100 -177.6400  59.0 6.2 0.0 KERMADEC ISLANDS REGION
event name:     010176A
time shift:     13.8000
half duration:   9.4000
latitude:      -29.2500
longitude:    -176.9600
depth:          16.5000
Mrr:       7.680000e+26
Mtt:       9.000000e+24
Mpp:      -7.770000e+26
Mrt:       1.390000e+26
Mrp:       4.520000e+26
Mtp:      -3.260000e+26

We can read this global CMT solution file, and attach it to source_prem. Something to notice here is that source_prem has the same content as config_prem.source, but config_prem.model is a deep copy of prem_model.

import obspy
from pyfk import calculate_sync

event=obspy.read_events(test_gcmt_path)[0]
source_prem.update_source_mechanism(event)
sync_result = calculate_sync(gf, config_prem, 30, source_time_function)

print(sync_result[0])
3 Trace(s) in Stream:
... | 1969-12-31T23:59:58.280712Z - 1970-01-01T00:00:49.380712Z | 10.0 Hz, 512 samples
... | 1969-12-31T23:59:58.280712Z - 1970-01-01T00:00:49.380712Z | 10.0 Hz, 512 samples
... | 1969-12-31T23:59:58.280712Z - 1970-01-01T00:00:49.380712Z | 10.0 Hz, 512 samples

The three traces in each Stream is ordered as Z, R, and T components. We can view the synthetic waveform in the Z component:

sync_result[0][0].plot();
../_images/tutorial_12_0.png

It’s not strange to have the amplitude around several hundred, as the unit here is cm. But we do see lots of numerical noise. The synthetics calculated from FK will also have this kind of noise. The possible reason might be our largest distance is relative small, thus the upper bound for the integration of the wave number will be relative small. It reminds us even we want to calculate some waveform with a relative small epicenter distance, it is adviced to test several largest epicenter distances to stablize the simulatation result.

In FK, it provides the flag to do the waveform integration, the differentiation, and the band-pass filter. Here we can just use obspy to process the waveform. For this example, our highest frequency is defined by the Nyquist sampling rate, which is 5HZ. If we want to bandpass the waveform between 0.5HZ and 3HZ, we can:

sync_result_to_filter=sync_result[0][0].copy()
sync_result_to_filter.detrend("linear")
sync_result_to_filter.taper(max_percentage=0.05, type='hann')
sync_result_to_filter.filter("bandpass", freqmin=1/8, freqmax=0.5, corners=2, zerophase=True)
sync_result_to_filter.plot();
../_images/tutorial_13_0.png

It is the waveform after performing the bandpass filter, with a second stage and two side filter (zerophase=True).

Congratulations! You have finished reading this tutorial. For more information about the algorithm in FK, you can read the source code in API Reference. The package has been tested for several cases using the PREM model with FK, and the the result is close enough. The numerical difference might be mainly due to that we are using double but not float as in FK, or the difference between the programming languages.

For more details about how to speed up the calculation using MPI or CUDA, you can refer to the other parts of this document.