Ob Spy Tutorial
Ob Spy Tutorial
Release 1.1.0
1 Introduction to ObsPy 3
1.1 Python Introduction for Seismologists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 UTCDateTime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Reading Seismograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Waveform Plotting Tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5 Retrieving Data from Data Centers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6 Filtering Seismograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.7 Downsampling Seismograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.8 Merging Seismograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.9 Beamforming - FK Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.10 Seismogram Envelopes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.11 Plotting Spectrograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.12 Trigger/Picker Tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.13 Poles and Zeros, Frequency Response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.14 Seismometer Correction/Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.15 Clone an Existing Dataless SEED File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.16 Export Seismograms to MATLAB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.17 Export Seismograms to ASCII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.18 Anything to MiniSEED . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1.19 Beachball Plot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.20 Basemap Plots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.21 Interfacing R from Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.22 Coordinate Conversions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.23 Hierarchical Clustering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
1.24 Visualizing Probabilistic Power Spectral Densities . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
1.25 Array Response Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
1.26 Continuous Wavelet Transform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
1.27 Time Frequency Misfit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.28 Visualize Data Availability of Local Waveform Archive . . . . . . . . . . . . . . . . . . . . . . . . 46
1.29 Travel Time and Ray Path Plotting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.30 Cross Correlation Pick Correction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.31 Handling custom defined tags in QuakeML and the ObsPy Catalog/Event framework . . . . . . . . . 50
1.32 Handling custom defined tags in StationXML with the Obspy Inventory . . . . . . . . . . . . . . . . 53
1.33 Creating a StationXML file from Scratch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
1.34 Connecting to a SeedLink Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2 Advanced Exercise 61
2.1 Advanced Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Index 73
i
ii
ObsPy Tutorial, Release 1.1.0
This tutorial does not attempt to be comprehensive and cover every single feature. Instead, it introduces many of
ObsPy’s most noteworthy features, and will give you a good idea of the library’s flavor and style.
A pdf version of the Tutorial is available here.
There are also IPython notebooks available online with an introduction to Python (with solutions/output), an introduc-
tion to ObsPy (with solutions/output) and an brief primer on data center access and visualization with ObsPy.
CONTENTS 1
ObsPy Tutorial, Release 1.1.0
2 CONTENTS
CHAPTER
ONE
INTRODUCTION TO OBSPY
Here we want to give a small, incomplete introduction to the Python programming language, with links to useful
packages and further resources. The key features are explained via the following Python script:
1 #!/usr/bin/env python
2 import glob
3 from obspy.core import read
4
Note: The length of all loops in Python is determined by the indentation level. Do not mix spaces and tabs in
your program code for indentation, this produces bugs that are not easy to identify.
Line 6 Uses the read() function from the obspy.core module to read in the seismogram to a Stream object
named st.
Line 7 Assigns the first Trace object of the list-like Stream object to the variable tr.
Line 8-9 A Python counterpart for the well-known C function sprintf is the % operator acting on a format string.
Here we print the header attributes station and starttime as well as the return value of the methods
mean() and std() acting on the data sub-object of the Trace (which are of type numpy.ndarray).
Line 10 Prints content of variable msg to the screen.
As Python is an interpreter language, we recommend to use the IPython shell for rapid development and trying things
out. It supports tab completion, history expansion and various other features. E.g. type help(glob.glob) or
glob.glob? to see the help of the glob() function (the module must be imported beforehand).
3
ObsPy Tutorial, Release 1.1.0
Further Resources
1.2 UTCDateTime
All absolute time values within ObsPy are consistently handled with the UTCDateTime class. It is based on a high
precision POSIX timestamp and not the Python datetime class because precision was an issue.
1.2.1 Initialization
In most cases there is no need to worry about timezones, but they are supported:
>>> UTCDateTime("2012-09-07T12:15:00+02:00")
UTCDateTime(2012, 9, 7, 10, 15)
>>> time.weekday
4
1.2.4 Exercises
• Calculate the number of hours passed since your birth. Optional: Include the correct time zone. The current
date and time can be obtained with
>>> UTCDateTime()
• Get a list of 10 UTCDateTime objects, starting yesterday at 10:00 with a spacing of 90 minutes.
• The first session starts at 09:00 and lasts for 3 hours and 15 minutes. Assuming we want to have the coffee break
1234 seconds and 5 microseconds before it ends. At what time is the coffee break?
• Assume you had your last cup of coffee yesterday at breakfast. How many minutes do you have to survive with
that cup of coffee?
Seismograms of various formats (e.g. SAC, MiniSEED, GSE2, SEISAN, Q, etc.) can be imported into a Stream
object using the read() function.
Streams are list-like objects which contain multiple Trace objects, i.e. gap-less continuous time series and related
header/meta information.
Each Trace object has a attribute called data pointing to a NumPy ndarray of the actual time series and the
attribute stats which contains all meta information in a dictionary-like Stats object. Both attributes starttime
and endtime of the Stats object are UTCDateTime objects.
The following example demonstrates how a single GSE2-formatted seismogram file is read into a ObsPy Stream
object. There exists only one Trace in the given seismogram:
>>> from obspy import read
>>> st = read(’http://examples.obspy.org/RJOB_061005_072159.ehz.new’)
>>> print(st)
1 Trace(s) in Stream:
.RJOB..Z | 2005-10-06T07:21:59.849998Z - 2005-10-06T07:24:59.844998Z | 200.0 Hz, 36000 samples
>>> len(st)
1
>>> tr = st[0] # assign first and only trace to new variable
>>> print(tr)
.RJOB..Z | 2005-10-06T07:21:59.849998Z - 2005-10-06T07:24:59.844998Z | 200.0 Hz, 36000 samples
Seismogram meta data, data describing the actual waveform data, are accessed via the stats keyword on each
Trace:
>>> print(tr.stats)
network:
station: RJOB
location:
channel: Z
starttime: 2005-10-06T07:21:59.849998Z
endtime: 2005-10-06T07:24:59.844998Z
sampling_rate: 200.0
delta: 0.005
npts: 36000
calib: 0.0948999971151
_format: GSE2
gse2: AttribDict({’instype’: ’ ’, ’datatype’: ’CM6’, ’hang’: -1.0, ’auxid’: ’RJOB’,
>>> tr.stats.station
’RJOB’
>>> tr.stats.gse2.datatype
’CM6’
The actual waveform data may be retrieved via the data keyword on each Trace:
>>> tr.data
array([-38, 12, -4, ..., -14, -3, -9])
>>> tr.data[0:3]
array([-38, 12, -4])
>>> len(tr)
36000
Stream objects offer a plot() method for fast preview of the waveform (requires the obspy.imaging module):
>>> st.plot()
Read the files as shown at the Reading Seismograms page. We will use two different ObsPy Stream objects
throughout this tutorial. The first one, singlechannel, just contains one continuous Trace and the other one,
threechannel, contains three channels of a seismograph.
>>> from obspy.core import read
>>> singlechannel = read(’https://examples.obspy.org/COP.BHZ.DK.2009.050’)
>>> print(singlechannel)
1 Trace(s) in Stream:
DK.COP..BHZ | 2009-02-19T00:00:00.025100Z - 2009-02-19T23:59:59.975100Z | 20.0 Hz, 1728000 samples
Using the plot() method of the Stream objects will show the plot. The default size of the plots is 800x250 pixel.
Use the size attribute to adjust it to your needs.
>>> singlechannel.plot()
This example shows the options to adjust the color of the graph, the number of ticks shown, their format and rotation
and how to set the start and end time of the plot. Please see the documentation of method plot() for more details on
all parameters.
>>> dt = singlechannel[0].stats.starttime
>>> singlechannel.plot(color=’red’, number_of_ticks=7,
... tick_rotation=5, tick_format=’%I:%M %p’,
... starttime=dt + 60*60, endtime=dt + 60*60 + 120)
Plots may be saved into the file system by the outfile parameter. The format is determined automatically from the
filename. Supported file formats depend on your matplotlib backend. Most backends support png, pdf, ps, eps and
svg.
>>> singlechannel.plot(outfile=’singlechannel.png’)
If the Stream object contains more than one Trace, each Trace will be plotted in a subplot. The start- and endtime
of each trace will be the same and the range on the y-axis will also be identical on each trace. Each additional subplot
will add 250 pixel to the height of the resulting plot. The size attribute is used in the following example to change
the overall size of the plot.
>>> threechannels.plot(size=(800, 600))
A day plot of a Trace object may be plotted by setting the type parameter to ’dayplot’:
>>> singlechannel.plot(type=’dayplot’)
Event information can be included in the plot as well (experimental feature, syntax might change):
>>> from obspy import read
>>> st = read("https://examples.obspy.org/GR.BFO..LHZ.2012.108")
>>> st.filter("lowpass", freq=0.1, corners=2)
>>> st.plot(type="dayplot", interval=60, right_vertical_labels=False,
... vertical_scaling_range=5e3, one_tick_per_line=True,
... color=[’k’, ’r’, ’b’, ’g’], show_y_UTC_label=False,
... events={’min_magnitude’: 6.5})
A record section can be plotted from a Stream object by setting parameter type to ’section’:
>>> stream.plot(type=’section’)
To plot a record section the ObsPy header trace.stats.distance (Offset) must be de-
fined in meters. Or a geographical location trace.stats.coordinates.latitude &
trace.stats.coordinates.longitude must be defined if the section is plotted in great circle distances
(dist_degree=True) along with parameter ev_coord. For further information please see plot()
Various options are available to change the appearance of the waveform plot. Please see plot() method for all
possible options.
Custom plots can be done using matplotlib, like shown in this minimalistic example (see
http://matplotlib.org/gallery.html for more advanced plotting examples):
import matplotlib.pyplot as plt
from obspy import read
st = read()
tr = st[0]
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(tr.times("matplotlib"), tr.data, "b-")
ax.xaxis_date()
fig.autofmt_xdate()
plt.show()
This section is intended as a small guide to help you choose the best way to download data for a given purpose using
ObsPy. Keep in mind that data centers and web services are constantly changing so this recommendation might not
be valid anymore at the time you read this. For actual code examples, please see the documentation of the various
modules.
Note: The most common use case is likely to download waveforms and event/station meta information. In almost
all cases you will want to use the obspy.clients.fdsn module for this. It supports the largest number of data
centers and uses the most modern data formats. There are still a number of reasons to choose a different module but
please make sure you have one.
If you want to requests waveforms, or station/event meta information you will most likely want to use the
obspy.clients.fdsn module. It is able to request data from any data center implementing the FDSN web
services. Example data centers include IRIS/ORFEUS/INGV/ETH/GFZ/RESIF/... - a curated list can be found here.
As a further advantage it returns data in the most modern and future proof formats.
If you don’t know which data center has what data, use one of the routing services. ObsPy has support for two of
them:
1. The IRIS Federator.
2. The EIDAWS Routing Service.
See the bottom part of the obspy.clients.fdsn module page for usage details.
If you want to download a lot of data across a number of data centers, ObsPy’s mass (or batch) downloader is for you.
You can formulate your queries for example in terms of geographical domains and ObsPy will download waveforms
and corresponding station meta information to produce complete data sets, ready for research, including some basic
quality control.
See the obspy.clients.fdsn.mass_downloader page for more details.
1.5.2 ArcLink
This service is largely deprecated as the data can just as well be requested via the obspy.clients.fdsn module.
1.5.6 NEIC
1.5.7 SeedLink
The following script shows how to filter a seismogram. The example uses a zero-phase-shift low-pass filter with a
corner frequency of 1 Hz using 2 corners. This is done in two runs forward and backward, so we end up with 4 corners
de facto.
The available filters are:
• bandpass
• bandstop
• lowpass
• highpass
import numpy as np
import matplotlib.pyplot as plt
import obspy
# There is only one trace in the Stream object, let’s work on that trace...
tr = st[0]
The following script shows how to downsample a seismogram. Currently, a simple integer decimation is sup-
ported. If not explicitly disabled, a low-pass filter is applied prior to decimation in order to prevent aliasing. For
comparison, the non-decimated but filtered data is plotted as well. Applied processing steps are documented in
trace.stats.processing of every single Trace. Note the shift that is introduced because by default the ap-
plied filters are not of zero-phase type. This can be avoided by manually applying a zero-phase filter and deactivating
automatic filtering during downsampling (no_filter=True).
import numpy as np
import matplotlib.pyplot as plt
import obspy
# There is only one trace in the Stream object, let’s work on that trace...
tr = st[0]
# For comparison also only filter the original data (same filter options as in
# automatically applied filtering during downsampling, corner frequency
# 0.4 * new sampling rate)
tr_filt = tr.copy()
tr_filt.filter(’lowpass’, freq=0.4 * tr.stats.sampling_rate / 4.0)
The following example shows how to merge and plot three seismograms with overlaps, the longest one is taken to be
the right one. Please also refer to the documentation of the merge() method.
import numpy as np
import matplotlib.pyplot as plt
import obspy
# sort
st.sort([’starttime’])
# start time in plot equals 0
dt = st[0].stats.starttime.timestamp
The following code shows how to do an FK Analysis with ObsPy. The data are from the blasting of the AGFA
skyscraper in Munich. We execute array_processing() using the following settings:
• The slowness grid is set to corner values of -3.0 to 3.0 s/km with a step fraction of sl_s = 0.03.
• The window length is 1.0 s, using a step fraction of 0.05 s.
• The data is bandpass filtered, using corners at 1.0 and 8.0 Hz, prewhitening is disabled.
• semb_thres and vel_thres are set to infinitesimally small numbers and must not be changed.
• The timestamp will be written in ’mlabday’, which can be read directly by our plotting routine.
• stime and etime have to be given in the UTCDateTime format.
The output will be stored in out.
The second half shows how to plot the output. We use the output out produced by array_processing(), which
are numpy ndarrays containing timestamp, relative power, absolute power, backazimuth, slowness. The colorbar
corresponds to relative power.
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import obspy
from obspy.core.util import AttribDict
from obspy.imaging.cm import obspy_sequential
from obspy.signal.invsim import corn_freq_2_paz
from obspy.signal.array_analysis import array_processing
# Load data
st = obspy.read("https://examples.obspy.org/agfa.mseed")
’gain’: 1.0})
st[0].stats.coordinates = AttribDict({
’latitude’: 48.108589,
’elevation’: 0.450000,
’longitude’: 11.582967})
st[1].stats.paz = AttribDict({
’poles’: [(-0.03736 - 0.03617j), (-0.03736 + 0.03617j)],
’zeros’: [0j, 0j],
’sensitivity’: 205479446.68601453,
’gain’: 1.0})
st[1].stats.coordinates = AttribDict({
’latitude’: 48.108192,
’elevation’: 0.450000,
’longitude’: 11.583120})
st[2].stats.paz = AttribDict({
’poles’: [(-0.03736 - 0.03617j), (-0.03736 + 0.03617j)],
’zeros’: [0j, 0j],
’sensitivity’: 250000000.0,
’gain’: 1.0})
st[2].stats.coordinates = AttribDict({
’latitude’: 48.108692,
’elevation’: 0.450000,
’longitude’: 11.583414})
st[3].stats.paz = AttribDict({
’poles’: [(-4.39823 + 4.48709j), (-4.39823 - 4.48709j)],
’zeros’: [0j, 0j],
’sensitivity’: 222222228.10910088,
’gain’: 1.0})
st[3].stats.coordinates = AttribDict({
’latitude’: 48.108456,
’elevation’: 0.450000,
’longitude’: 11.583049})
st[4].stats.paz = AttribDict({
’poles’: [(-4.39823 + 4.48709j), (-4.39823 - 4.48709j), (-2.105 + 0j)],
’zeros’: [0j, 0j, 0j],
’sensitivity’: 222222228.10910088,
’gain’: 1.0})
st[4].stats.coordinates = AttribDict({
’latitude’: 48.108730,
’elevation’: 0.450000,
’longitude’: 11.583157})
# Execute array_processing
stime = obspy.UTCDateTime("20080217110515")
etime = obspy.UTCDateTime("20080217110545")
kwargs = dict(
# slowness grid: X min, X max, Y min, Y max, Slow Step
sll_x=-3.0, slm_x=3.0, sll_y=-3.0, slm_y=3.0, sl_s=0.03,
# sliding window properties
win_len=1.0, win_frac=0.05,
# frequency properties
frqlow=1.0, frqhigh=8.0, prewhiten=0,
# restrict output
semb_thres=-1e9, vel_thres=-1e9, timestamp=’mlabday’,
stime=stime, etime=etime
)
out = array_processing(st, **kwargs)
# Plot
labels = [’rel.power’, ’abs.power’, ’baz’, ’slow’]
xlocator = mdates.AutoDateLocator()
fig = plt.figure()
for i, lab in enumerate(labels):
ax = fig.add_subplot(4, 1, i + 1)
ax.scatter(out[:, 0], out[:, i + 1], c=out[:, 1], alpha=0.6,
edgecolors=’none’, cmap=obspy_sequential)
ax.set_ylabel(lab)
ax.set_xlim(out[0, 0], out[-1, 0])
ax.set_ylim(out[:, i + 1].min(), out[:, i + 1].max())
ax.xaxis.set_major_locator(xlocator)
ax.xaxis.set_major_formatter(mdates.AutoDateFormatter(xlocator))
Another representation would be a polar plot, which sums the relative power in gridded bins, each defined by backaz-
imuth and slowness of the analyzed signal part. The backazimuth is counted clockwise from north, the slowness limits
can be set by hand.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colorbar import ColorbarBase
from matplotlib.colors import Normalize
import obspy
from obspy.core.util import AttribDict
from obspy.imaging.cm import obspy_sequential
from obspy.signal.invsim import corn_freq_2_paz
from obspy.signal.array_analysis import array_processing
# Load data
st = obspy.read("https://examples.obspy.org/agfa.mseed")
’longitude’: 11.582967})
st[1].stats.paz = AttribDict({
’poles’: [(-0.03736 - 0.03617j), (-0.03736 + 0.03617j)],
’zeros’: [0j, 0j],
’sensitivity’: 205479446.68601453,
’gain’: 1.0})
st[1].stats.coordinates = AttribDict({
’latitude’: 48.108192,
’elevation’: 0.450000,
’longitude’: 11.583120})
st[2].stats.paz = AttribDict({
’poles’: [(-0.03736 - 0.03617j), (-0.03736 + 0.03617j)],
’zeros’: [0j, 0j],
’sensitivity’: 250000000.0,
’gain’: 1.0})
st[2].stats.coordinates = AttribDict({
’latitude’: 48.108692,
’elevation’: 0.450000,
’longitude’: 11.583414})
st[3].stats.paz = AttribDict({
’poles’: [(-4.39823 + 4.48709j), (-4.39823 - 4.48709j)],
’zeros’: [0j, 0j],
’sensitivity’: 222222228.10910088,
’gain’: 1.0})
st[3].stats.coordinates = AttribDict({
’latitude’: 48.108456,
’elevation’: 0.450000,
’longitude’: 11.583049})
st[4].stats.paz = AttribDict({
’poles’: [(-4.39823 + 4.48709j), (-4.39823 - 4.48709j), (-2.105 + 0j)],
’zeros’: [0j, 0j, 0j],
’sensitivity’: 222222228.10910088,
’gain’: 1.0})
st[4].stats.coordinates = AttribDict({
’latitude’: 48.108730,
’elevation’: 0.450000,
’longitude’: 11.583157})
# Execute array_processing
kwargs = dict(
# slowness grid: X min, X max, Y min, Y max, Slow Step
sll_x=-3.0, slm_x=3.0, sll_y=-3.0, slm_y=3.0, sl_s=0.03,
# sliding window properties
win_len=1.0, win_frac=0.05,
# frequency properties
frqlow=1.0, frqhigh=8.0, prewhiten=0,
# restrict output
semb_thres=-1e9, vel_thres=-1e9,
stime=obspy.UTCDateTime("20080217110515"),
etime=obspy.UTCDateTime("20080217110545")
)
out = array_processing(st, **kwargs)
# Plot
cmap = obspy_sequential
# make output human readable, adjust backazimuth to values between 0 and 360
t, rel_power, abs_power, baz, slow = out.T
baz[baz < 0.0] += 360
# transform to radian
baz_edges = np.radians(baz_edges)
dh = abs(sl_edges[1] - sl_edges[0])
dw = abs(baz_edges[1] - baz_edges[0])
plt.show()
The following script shows how to filter a seismogram and plot it together with its envelope.
This example uses a zero-phase-shift bandpass to filter the data with corner frequencies 1 and 3 Hz, using 2 corners
(two runs due to zero-phase option, thus 4 corners overall). Then we calculate the envelope and plot it together with
the Trace. Data can be found here.
import numpy as np
import matplotlib.pyplot as plt
import obspy
import obspy.signal
st = obspy.read("https://examples.obspy.org/RJOB_061005_072159.ehz.new")
data = st[0].data
npts = st[0].stats.npts
samprate = st[0].stats.sampling_rate
The following lines of code demonstrate how to make a spectrogram plot of an ObsPy Stream object.
Lots of options can be customized, see spectrogram() for more details. For example, the colormap of the plot can
easily be adjusted by importing a predefined colormap from matplotlib.cm, nice overviews of available matplotlib
colormaps are given at:
• http://www.physics.ox.ac.uk/users/msshin/science/code/matplotlib_cm/
• http://matplotlib.org/examples/color/colormaps_reference.html
• https://wiki.scipy.org/Cookbook/Matplotlib/Show_colormaps
import obspy
st = obspy.read("https://examples.obspy.org/RJOB_061005_072159.ehz.new")
st.spectrogram(log=True, title=’BW.RJOB ’ + str(st[0].stats.starttime))
This is a small tutorial that started as a practical for the UNESCO short course on triggering. Test data used in this
tutorial can be downloaded here: trigger_data.zip.
The triggers are implemented as described in [Withers1998]. Information on finding the right trigger parameters for
STA/LTA type triggers can be found in [Trnkoczy2012].
See also:
Please note the convenience method of ObsPy’s Stream.trigger and Trace.trigger objects for triggering.
The data files are read into an ObsPy Trace object using the read() function.
>>> from obspy.core import read
>>> st = read("https://examples.obspy.org/ev0_6.a01.gse2")
>>> st = st.select(component="Z")
>>> tr = st[0]
The data format is automatically detected. Important in this tutorial are the Trace attributes:
tr.data contains the data as numpy.ndarray
tr.stats contains a dict-like class of header entries
tr.stats.sampling_rate the sampling rate
tr.stats.npts sample count of data
As an example, the header of the data file is printed and the data are plotted like this:
>>> print(tr.stats)
network:
station: EV0_6
location:
channel: EHZ
starttime: 1970-01-01T01:00:00.000000Z
endtime: 1970-01-01T01:00:59.995000Z
sampling_rate: 200.0
delta: 0.005
npts: 12000
calib: 1.0
_format: GSE2
gse2: AttribDict({’instype’: ’ ’, ’datatype’: ’CM6’, ’hang’: 0.0, ’auxid’: ’ ’, ’
Using the plot() method of the Trace objects will show the plot.
>>> tr.plot(type="relative")
After loading the data, we are able to pass the waveform data to the following trigger routines defined in
obspy.signal.trigger:
obspy.signal.trigger.recursive_sta_lta
obspy.signal.trigger.carl_sta_trig
obspy.signal.trigger.classic_sta_lta
obspy.signal.trigger.delayed_sta_lta
obspy.signal.trigger.z_detect
z_detect(a, nsta)
Z-detector.
Parameters nsta – Window length in Samples.
See also:
[Withers1998], p. 99
obspy.signal.trigger.pk_baer
See also:
[Baer1987]
obspy.signal.trigger.ar_pick
ar_pick(a, b, c, samp_rate, f1, f2, lta_p, sta_p, lta_s, sta_s, m_p, m_s, l_p, l_s, s_pick=True)
Pick P and S arrivals with an AR-AIC + STA/LTA algorithm.
The algorithm picks onset times using an Auto Regression - Akaike Information Criterion (AR-AIC) method.
The detection intervals are successively narrowed down with the help of STA/LTA ratios as well as STA-LTA
difference calculations. For details, please see [Akazawa2004].
An important feature of this algorithm is that it requires comparatively little tweaking and site-specific settings
and is thus applicable to large, diverse data sets.
Parameters
• a (numpy.ndarray) – Z signal the data.
• b (numpy.ndarray) – N signal of the data.
• c (numpy.ndarray) – E signal of the data.
• samp_rate (float) – Number of samples per second.
• f1 (float) – Frequency of the lower bandpass window.
• f2 (float) – Frequency of the upper .andpass window.
• lta_p (float) – Length of LTA for the P arrival in seconds.
• sta_p (float) – Length of STA for the P arrival in seconds.
• lta_s (float) – Length of LTA for the S arrival in seconds.
• sta_s (float) – Length of STA for the S arrival in seconds.
• m_p (int) – Number of AR coefficients for the P arrival.
• m_s (int) – Number of AR coefficients for the S arrival.
• l_p (float) – Length of variance window for the P arrival in seconds.
• l_s (float) – Length of variance window for the S arrival in seconds.
• s_pick (bool) – If True, also pick the S phase, otherwise only the P phase.
Return type tuple
For all the examples, the commands to read in the data and to load the modules are the following:
>>> from obspy.core import read
>>> from obspy.signal.trigger import plot_trigger
>>> trace = read("https://examples.obspy.org/ev0_6.a01.gse2")[0]
>>> df = trace.stats.sampling_rate
Z-Detect
Carl-Sta-Trig
In this example we perform a coincidence trigger on a local scale network of 4 stations. For the single station triggers
a recursive STA/LTA is used. The waveform data span about four minutes and include four local events. Two are
easily recognizable (Ml 1-2), the other two can only be detected with well adjusted trigger settings (Ml <= 0).
First we assemble a Stream object with all waveform data, the data used in the example is available from our web
server:
>>> from obspy.core import Stream, read
>>> st = Stream()
>>> files = ["BW.UH1..SHZ.D.2010.147.cut.slist.gz",
... "BW.UH2..SHZ.D.2010.147.cut.slist.gz",
... "BW.UH3..SHZ.D.2010.147.cut.slist.gz",
... "BW.UH4..SHZ.D.2010.147.cut.slist.gz"]
>>> for filename in files:
... st += read("https://examples.obspy.org/" + filename)
After applying a bandpass filter we run the coincidence triggering on all data. In the example a recursive STA/LTA is
used. The trigger parameters are set to 0.5 and 10 second time windows, respectively. The on-threshold is set to 3.5,
the off-threshold to 1. In this example every station gets a weight of 1 and the coincidence sum threshold is set to 3.
For more complex network setups the weighting for every station/channel can be customized. We want to keep our
original data so we work with a copy of the original stream:
>>> st.filter(’bandpass’, freqmin=10, freqmax=20) # optional prefiltering
>>> from obspy.signal.trigger import coincidence_trigger
>>> st2 = st.copy()
>>> trig = coincidence_trigger("recstalta", 3.5, 1, st2, 3, sta=0.5, lta=10)
With these settings the coincidence trigger reports three events. For each (possible) event the start time and duration
is provided. Furthermore, a list of station names and trace IDs is provided, ordered by the time the stations have
triggered, which can give a first rough idea of the possible event location. We can request additional information by
specifying details=True:
>>> st2 = st.copy()
>>> trig = coincidence_trigger("recstalta", 3.5, 1, st2, 3, sta=0.5, lta=10,
... details=True)
For clarity, we only display information on the first item in the results here:
>>> pprint(trig[0])
{’cft_peak_wmean’: 19.561900329259956,
’cft_peaks’: [19.535644192544272,
19.872432918501264,
19.622171410201297,
19.217352795792998],
’cft_std_wmean’: 5.4565629691954713,
’cft_stds’: [5.292458320417178,
5.6565387957966404,
5.7582248973698507,
5.1190298631982163],
’coincidence_sum’: 4.0,
’duration’: 4.5299999713897705,
’stations’: [’UH3’, ’UH2’, ’UH1’, ’UH4’],
’time’: UTCDateTime(2010, 5, 27, 16, 24, 33, 190000),
’trace_ids’: [’BW.UH3..SHZ’, ’BW.UH2..SHZ’, ’BW.UH1..SHZ’, ’BW.UH4..SHZ’]}
Here, some additional information on the peak values and standard deviations of the characteristic functions of the
single station triggers is provided. Also, for both a weighted mean is calculated. These values can help to distinguish
certain from questionable network triggers.
For more information on all possible options see the documentation page for coincidence_trigger().
This example is an extension of the common network coincidence trigger. Waveforms with already known event(s)
can be provided to check waveform similarity of single-station triggers. If the corresponding similarity threshold is
exceeded the event trigger is included in the result list even if the coincidence sum does not exceed the specified
minimum coincidence sum. Using this approach, events can be detected that have good recordings on one station
with very similar waveforms but for some reason are not detected on enough other stations (e.g. temporary station
outages or local high noise levels etc.). An arbitrary number of template waveforms can be provided for any station.
Computation time might get significantly higher due to the necessary cross correlations. In the example we use two
three-component event templates on top of a common network trigger on vertical components only.
>>> from obspy.core import Stream, read, UTCDateTime
>>> st = Stream()
>>> files = ["BW.UH1..SHZ.D.2010.147.cut.slist.gz",
... "BW.UH2..SHZ.D.2010.147.cut.slist.gz",
... "BW.UH3..SHZ.D.2010.147.cut.slist.gz",
... "BW.UH3..SHN.D.2010.147.cut.slist.gz",
... "BW.UH3..SHE.D.2010.147.cut.slist.gz",
... "BW.UH4..SHZ.D.2010.147.cut.slist.gz"]
>>> for filename in files:
... st += read("https://examples.obspy.org/" + filename)
>>> st.filter(’bandpass’, freqmin=10, freqmax=20) # optional prefiltering
Here we set up a dictionary with template events for one single station. The specified times are exact P wave onsets,
the event duration (including S wave) is about 2.5 seconds. On station UH3 we use two template events with three-
component data, on station UH1 we use one template event with only vertical component data.
>>> times = ["2010-05-27T16:24:33.095000", "2010-05-27T16:27:30.370000"]
>>> event_templates = {"UH3": []}
>>> for t in times:
... t = UTCDateTime(t)
... st_ = st.select(station="UH3").slice(t, t + 2.5)
... event_templates["UH3"].append(st_)
>>> t = UTCDateTime("2010-05-27T16:27:30.574999")
>>> st_ = st.select(station="UH1").slice(t, t + 2.5)
>>> event_templates["UH1"] = [st_]
The triggering step, including providing of similarity threshold and event template waveforms. Note that the coinci-
dence sum is set to 4 and we manually specify to only use vertical components with equal station coincidence values
of 1.
>>> from obspy.signal.trigger import coincidence_trigger
>>> st2 = st.copy()
>>> trace_ids = {"BW.UH1..SHZ": 1,
... "BW.UH2..SHZ": 1,
... "BW.UH3..SHZ": 1,
... "BW.UH4..SHZ": 1}
>>> similarity_thresholds = {"UH1": 0.8, "UH3": 0.7}
>>> trig = coincidence_trigger("classicstalta", 5, 1, st2, 4, sta=0.5,
... lta=10, trace_ids=trace_ids,
... event_templates=event_templates,
... similarity_threshold=similarity_thresholds)
The results now include two event triggers, that do not reach the specified minimum coincidence threshold but that
have a similarity value that exceeds the specified similarity threshold when compared to at least one of the provided
event template waveforms. Note the values of 1.0 when checking the event triggers where we extracted the event
templates for this example.
>>> from pprint import pprint
>>> pprint(trig)
[{’coincidence_sum’: 4.0,
’duration’: 4.1100001335144043,
’similarity’: {’UH1’: 0.9414944738498271, ’UH3’: 1.0},
’stations’: [’UH3’, ’UH2’, ’UH1’, ’UH4’],
’time’: UTCDateTime(2010, 5, 27, 16, 24, 33, 210000),
’trace_ids’: [’BW.UH3..SHZ’, ’BW.UH2..SHZ’, ’BW.UH1..SHZ’, ’BW.UH4..SHZ’]},
{’coincidence_sum’: 3.0,
’duration’: 1.9900000095367432,
’similarity’: {’UH1’: 0.65228204570577764, ’UH3’: 0.72679293429214198},
’stations’: [’UH3’, ’UH1’, ’UH2’],
’time’: UTCDateTime(2010, 5, 27, 16, 25, 26, 710000),
’trace_ids’: [’BW.UH3..SHZ’, ’BW.UH1..SHZ’, ’BW.UH2..SHZ’]},
{’coincidence_sum’: 3.0,
’duration’: 1.9200000762939453,
’similarity’: {’UH1’: 0.89404458774338103, ’UH3’: 0.74581409371425222},
’stations’: [’UH2’, ’UH1’, ’UH3’],
’time’: UTCDateTime(2010, 5, 27, 16, 27, 2, 260000),
’trace_ids’: [’BW.UH2..SHZ’, ’BW.UH1..SHZ’, ’BW.UH3..SHZ’]},
{’coincidence_sum’: 4.0,
’duration’: 4.0299999713897705,
’similarity’: {’UH1’: 1.0, ’UH3’: 1.0},
’stations’: [’UH3’, ’UH2’, ’UH1’, ’UH4’],
’time’: UTCDateTime(2010, 5, 27, 16, 27, 30, 510000),
’trace_ids’: [’BW.UH3..SHZ’, ’BW.UH2..SHZ’, ’BW.UH1..SHZ’, ’BW.UH4..SHZ’]}]
For more information on all possible options see the documentation page for coincidence_trigger().
Baer Picker
This yields the output 34.47 EPU3, which means that a P pick was set at 34.47s with Phase information EPU3.
AR Picker
This gives the output 30.6350002289 and 31.2800006866, meaning that a P pick at 30.64s and an S pick at 31.28s
were identified.
A more complicated example, where the data are retrieved via ArcLink and results are plotted step by step, is shown
here:
import matplotlib.pyplot as plt
import obspy
from obspy.clients.arclink import Client
from obspy.signal.trigger import recursive_sta_lta, trigger_onset
t = obspy.UTCDateTime("2009-08-24 00:19:45")
st = client.get_waveforms(’BW’, ’RTSH’, ’’, ’EHZ’, t, t + 50)
# For convenience
tr = st[0] # only one trace in mseed volume
df = tr.stats.sampling_rate
Note: For metadata read using read_inventory() into Inventory objects (and the corresponding sub-
objects Network, Station, Channel, Response), there is a convenience method to show Bode plots, see e.g.
Inventory.plot_response() or Response.plot()).
The following lines show how to calculate and visualize the frequency response of a LE-3D/1s seismometer with
sampling interval 0.005s and 16384 points of fft. We want the phase to go from 0 to 2*pi, instead of the output from
angle that goes from -pi to pi .
import numpy as np
import matplotlib.pyplot as plt
plt.figure()
plt.subplot(121)
plt.loglog(f, abs(h))
plt.xlabel(’Frequency [Hz]’)
plt.ylabel(’Amplitude’)
plt.subplot(122)
phase = 2 * np.pi + np.unwrap(np.angle(h))
plt.semilogx(f, phase)
plt.xlabel(’Frequency [Hz]’)
plt.ylabel(’Phase [radian]’)
# ticks and tick labels at multiples of pi
plt.yticks(
[0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi],
[’$0$’, r’$\frac{\pi}{2}$’, r’$\pi$’, r’$\frac{3\pi}{2}$’, r’$2\pi$’])
plt.ylim(-0.2, 2 * np.pi + 0.2)
# title, centered above both subplots
plt.suptitle(’Frequency Response of LE-3D/1s Seismometer’)
# make more room in between subplots for the ylabel of right plot
plt.subplots_adjust(wspace=0.3)
plt.show()
When using the FDSN client the response can directly be attached to the waveforms and then subsequently re-
moved using Stream.remove_response():
from obspy import UTCDateTime
from obspy.clients.fdsn import Client
t1 = UTCDateTime("2010-09-3T16:30:00.000")
t2 = UTCDateTime("2010-09-3T17:00:00.000")
fdsn_client = Client(’IRIS’)
# Fetch waveform from IRIS FDSN web service into a ObsPy stream object
# and automatically attach correct response
st = fdsn_client.get_waveforms(network=’NZ’, station=’BFZ’, location=’10’,
channel=’HHZ’, starttime=t1, endtime=t2,
attach_response=True)
# define a filter band to prevent amplifying noise during the deconvolution
pre_filt = (0.005, 0.006, 30.0, 35.0)
st.remove_response(output=’DISP’, pre_filt=pre_filt)
Using the plot option it is possible to visualize the individual steps during response removal in the frequency domain
to check the chosen pre_filt and water_level options to stabilize the deconvolution of the inverted instrument response
spectrum:
from obspy import read, read_inventory
st = read("/path/to/IU_ULN_00_LH1_2015-07-18T02.mseed")
tr = st[0]
inv = read_inventory("/path/to/IU_ULN_00_LH1.xml")
pre_filt = [0.001, 0.005, 10, 20]
tr.remove_response(inventory=inv, pre_filt=pre_filt, output="DISP",
water_level=60, plot=True)
It is further possible to use evalresp to evaluate the instrument response information from a RESP file.
import matplotlib.pyplot as plt
import obspy
from obspy.core.util import NamedTemporaryFile
from obspy.clients.fdsn import Client as FDSN_Client
from obspy.clients.iris import Client as OldIris_Client
# Fetch waveform from IRIS FDSN web service into a ObsPy stream object
fdsn_client = FDSN_Client("IRIS")
st = fdsn_client.get_waveforms(’NZ’, ’BFZ’, ’10’, ’HHZ’, t1, t2)
# this can be the date of your raw data or any date for which the
# SEED RESP-file is valid
date = t1
# Remove instrument response using the information from the given RESP file
st.simulate(paz_remove=None, pre_filt=pre_filt, seedresp=seedresp)
tr = st[0]
tr_orig = st_orig[0]
time = tr.times()
plt.subplot(211)
plt.plot(time, tr_orig.data, ’k’)
plt.ylabel(’STS-2 [counts]’)
plt.subplot(212)
plt.plot(time, tr.data, ’k’)
plt.ylabel(’Displacement [m]’)
plt.xlabel(’Time [s]’)
plt.show()
A Parser object created using a Dataless SEED file can also be used. For each trace the respective RESP response
data is extracted internally then. When using Stream/Trace‘s simulate() convenience methods the “date”
parameter can be omitted (each trace’s start time is used internally).
import obspy
from obspy.io.xseed import Parser
st = obspy.read("https://examples.obspy.org/BW.BGLD..EH.D.2010.037")
parser = Parser("https://examples.obspy.org/dataless.seed.BW_BGLD")
st.simulate(seedresp={’filename’: parser, ’units’: "DIS"})
The following script shows how to simulate a 1Hz seismometer from a STS-2 seismometer with the given poles
and zeros. Poles, zeros, gain (A0 normalization factor) and sensitivity (overall sensitivity) are specified as keys of a
dictionary.
import obspy
from obspy.signal.invsim import corn_freq_2_paz
paz_sts2 = {
’poles’: [-0.037004 + 0.037016j, -0.037004 - 0.037016j, -251.33 + 0j,
- 131.04 - 467.29j, -131.04 + 467.29j],
’zeros’: [0j, 0j],
’gain’: 60077000.0,
’sensitivity’: 2516778400.0}
paz_1hz = corn_freq_2_paz(1.0, damp=0.707) # 1Hz instrument
paz_1hz[’sensitivity’] = 1.0
st = obspy.read()
# make a copy to keep our original data
st_orig = st.copy()
st_orig.plot()
st.plot()
For more customized plotting we could also work with matplotlib manually from here:
import numpy as np
import matplotlib.pyplot as plt
tr = st[0]
tr_orig = st_orig[0]
t = np.arange(tr.stats.npts) / tr.stats.sampling_rate
plt.subplot(211)
plt.plot(t, tr_orig.data, ’k’)
plt.ylabel(’STS-2 [counts]’)
plt.subplot(212)
plt.plot(t, tr.data, ’k’)
plt.ylabel(’1Hz Instrument [m/s]’)
plt.xlabel(’Time [s]’)
plt.show()
The following code example shows how to clone an existing DatalessSEED file (dataless.seed.BW_RNON) and
use it as a template to build up a DatalessSEED file for a new station.
First of all, we have to make the necessary imports and read the existing DatalessSEED volume (stored on our examples
webserver):
>>> from obspy import UTCDateTime
>>> from obspy.io.xseed import Parser
>>>
>>> p = Parser("https://examples.obspy.org/dataless.seed.BW_RNON")
>>> blk = p.blockettes
Now we can adapt the information only appearing once in the DatalessSEED at the start of the file, in this case
Blockette 50 and the abbreviations in Blockette 33:
>>> blk[50][0].network_code = ’BW’
>>> blk[50][0].station_call_letters = ’RMOA’
>>> blk[50][0].site_name = "Moar Alm, Bavaria, BW-Net"
>>> blk[50][0].latitude = 47.761658
>>> blk[50][0].longitude = 12.864466
>>> blk[50][0].elevation = 815.0
>>> blk[50][0].start_effective_date = UTCDateTime("2006-07-18T00:00:00.000000Z")
>>> blk[50][0].end_effective_date = ""
>>> blk[33][1].abbreviation_description = "Lennartz LE-3D/1 seismometer"
After that we have to change the information for all of the three channels involved:
>>> mult = len(blk[58])/3
>>> for i, cha in enumerate([’Z’, ’N’, ’E’]):
... blk[52][i].channel_identifier = ’EH%s’ % cha
... blk[52][i].location_identifier = ’’
... blk[52][i].latitude = blk[50][0].latitude
... blk[52][i].longitude = blk[50][0].longitude
Note: FIR coefficients are not set in this example. In case you require correct FIR coefficients, either clone from an
existing dataless file with the same seismometer type or set the corresponding blockettes with the correct values.
At the end we can write the adapted DatalessSEED volume to a new file:
>>> p.write_seed("dataless.seed.BW_RMOA")
The following example shows how to read in a waveform file with Python and save each Trace in the resulting
Stream object to one MATLAB .MAT file. The data can the be loaded from within MATLAB with the load
function.
from scipy.io import savemat
import obspy
st = obspy.read("https://examples.obspy.org/BW.BGLD..EH.D.2010.037")
for i, tr in enumerate(st):
mdict = {k: str(v) for k, v in tr.stats.iteritems()}
mdict[’data’] = tr.data
savemat("data-%d.mat" % i, mdict)
You may directly export waveform data to any ASCII format available by ObsPy using the write() method on the
generated Stream object.
• TSPAIR, a ASCII format where data is written in time-sample pairs (see also TSPAIR format
description):
TIMESERIES BW_RJOB__EHZ_D, 6001 samples, 200 sps, 2009-08-24T00:20:03.000000, TSPAIR, INTEGER,
2009-08-24T00:20:03.000000 288
2009-08-24T00:20:03.005000 300
2009-08-24T00:20:03.010000 292
2009-08-24T00:20:03.015000 285
2009-08-24T00:20:03.020000 265
2009-08-24T00:20:03.025000 287
...
In the following, a small Python script is shown which converts each Trace of a seismogram file to an ASCII file
with a custom header. Waveform data will be multiplied by a given calibration factor and written using NumPy‘s
savetxt() function.
"""
USAGE: export_seismograms_to_ascii.py in_file out_file calibration
"""
from __future__ import print_function
import sys
import numpy as np
import obspy
try:
in_file = sys.argv[1]
out_file = sys.argv[2]
calibration = float(sys.argv[3])
except:
print(__doc__)
raise
st = obspy.read(in_file)
for i, tr in enumerate(st):
f = open("%s_%d" % (out_file, i), "w")
f.write("# STATION %s\n" % (tr.stats.station))
f.write("# CHANNEL %s\n" % (tr.stats.channel))
f.write("# START_TIME %s\n" % (str(tr.stats.starttime)))
f.write("# SAMP_FREQ %f\n" % (tr.stats.sampling_rate))
f.write("# NDAT %d\n" % (tr.stats.npts))
np.savetxt(f, tr.data * calibration, fmt="%f")
f.close()
The following lines show how you can convert anything to MiniSEED format. In the example, a few lines of a weather
station output are written to a MiniSEED file. The correct meta information starttime, the sampling_rate,
station name and so forth are also encoded (Note: Only the ones given are allowed by the MiniSEED standard).
Converting arbitrary ASCII to MiniSEED is extremely helpful if you want to send log messages, output of meteoro-
logic stations or anything else via the SeedLink protocol.
from __future__ import print_function
import numpy as np
from obspy import UTCDateTime, read, Trace, Stream
weather = """
00.0000 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000000
00.0002 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000001
00.0005 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000002
00.0008 0.0 ??? 4.7 97.7 1015.4 0.0 010308 000003
00.0011 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000004
00.0013 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000005
00.0016 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000006
00.0019 0.0 ??? 4.7 97.7 1015.0 0.0 010308 000007
"""
The following lines show how to create a graphical representation of a focal mechanism.
from obspy.imaging.beachball import beachball
Simple Basemap plots of e.g. Inventory or Catalog objects can be performed with builtin methods, see e.g.
Inventory.plot() or Catalog.plot().
For full control over the projection and map extent, a custom basemap can be set up (e.g. following the examples in
the basemap documentation), and then be reused for plots of e.g. Inventory or Catalog objects:
from mpl_toolkits.basemap import Basemap
import numpy as np
import matplotlib.pyplot as plt
# we need to attach the basemap object to the figure, so that obspy knows about
# it and reuses it
fig.bmap = m
plt.show()
The following example shows how to plot beachballs into a basemap plot together with some stations. The example
requires the basemap package (download site) to be installed. The SRTM file used can be downloaded here. The first
lines of our SRTM data file (from CGIAR) look like this:
ncols 400
nrows 200
xllcorner 12°40’E
yllcorner 47°40’N
xurcorner 13°00’E
yurcorner 47°50’N
cellsize 0.00083333333333333
NODATA_value -9999
682 681 685 690 691 689 678 670 675 680 681 679 675 671 674 680 679 679 675 671 668 664 659 660 656 6
import gzip
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
# create grids and compute map projection coordinates for lon/lat grid
x, y = m(*np.meshgrid(lons, lats))
plt.show()
Some notes:
• The Python package GDAL allows you to directly read a GeoTiff into NumPy ndarray
>>> geo = gdal.Open("file.geotiff")
>>> x = geo.ReadAsArray()
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
m.drawcoastlines()
m.fillcontinents()
m.drawparallels(np.arange(-90., 120., 30.))
m.drawmeridians(np.arange(0., 420., 60.))
m.drawmapboundary()
x, y = m(142.36929, 38.3215)
focmecs = [0.136, -0.591, 0.455, -0.396, 0.046, -0.615]
ax = plt.gca()
b = beach(focmecs, xy=(x, y), width=10, linewidth=1, alpha=0.85)
b.set_zorder(10)
ax.add_collection(b)
plt.show()
The rpy2 package allows to interface R from Python. The following example shows how to convert data
(numpy.ndarray) to an R matrix and execute the R command summary on it.
>>> from obspy.core import read
>>> import rpy2.robjects as RO
>>> import rpy2.robjects.numpy2ri
>>> r = RO.r
>>> st = read("test/BW.BGLD..EHE.D.2008.001")
>>> M = RO.RMatrix(st[0].data)
>>> print(r.summary(M))
Min. 1st Qu. Median Mean 3rd Qu. Max.
-1056.0 -409.0 -393.0 -393.7 -378.0 233.0
Coordinate conversions can be done conveniently using pyproj. After looking up the EPSG codes of source and target
coordinate system, the conversion can be done in just a few lines of code. The following example converts the station
coordinates of two German stations to the regionally used Gauß-Krüger system:
>>> import pyproj
>>> lat = [49.6919, 48.1629]
>>> lon = [11.2217, 11.2752]
>>> proj_wgs84 = pyproj.Proj(init="epsg:4326")
>>> proj_gk4 = pyproj.Proj(init="epsg:31468")
>>> x, y = pyproj.transform(proj_wgs84, proj_gk4, lon, lat)
>>> print(x)
[4443947.179347951, 4446185.667319892]
>>> print(y)
[5506428.401023342, 5336354.054996853]
Another common usage is to convert location information in latitude and longitude to Universal Transverse Mercator
coordinate system (UTM). This is especially useful for large dense arrays in a small area. Such conversion can be
easily done using utm package. Below is its typical usages:
>>> import utm
>>> utm.from_latlon(51.2, 7.5)
(395201.3103811303, 5673135.241182375, 32, ’U’)
An implementation of hierarchical clustering is provided in the SciPy package. Among other things, it allows to build
clusters from similarity matrices and make dendrogram plots. The following example shows how to do this for an
already computed similarity matrix. The similarity data are computed from events in an area with induced seismicity
(using the cross-correlation routines in obspy.signal) and can be fetched from our examples webserver:
First, we import the necessary modules and load the data stored on our webserver:
>>> import io, urllib
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from scipy.cluster import hierarchy
>>> from scipy.spatial import distance
>>>
>>> url = "https://examples.obspy.org/dissimilarities.npz"
>>> with io.BytesIO(urllib.urlopen(url).read()) as fh, np.load(fh) as data:
... dissimilarity = data[’dissimilarity’]
Now, we can start building up the plots. First, we plot the dissimilarity matrix:
>>> plt.subplot(121)
>>> plt.imshow(1 - dissimilarity, interpolation="nearest")
After that, we use SciPy to build up and plot the dendrogram into the right-hand subplot:
>>> dissimilarity = distance.squareform(dissimilarity)
>>> threshold = 0.3
>>> linkage = hierarchy.linkage(dissimilarity, method="single")
>>> clusters = hierarchy.fcluster(linkage, threshold, criterion="distance")
>>>
>>> plt.subplot(122)
>>> hierarchy.dendrogram(linkage, color_threshold=0.3)
>>> plt.xlabel("Event number")
>>> plt.ylabel("Dissimilarity")
>>> plt.show()
The following code example shows how to use the PPSD class defined in obspy.signal. The routine is useful
for interpretation of e.g. noise measurements for site quality control checks. For more information on the topic see
[McNamara2004].
>>> from obspy import read
>>> from obspy.io.xseed import Parser
>>> from obspy.signal import PPSD
Read data and select a trace with the desired station/channel combination:
>>> st = read("https://examples.obspy.org/BW.KW1..EHZ.D.2011.037")
>>> tr = st.select(id="BW.KW1..EHZ")[0]
Metadata can be provided as an Inventory (e.g. from a StationXML file or from a request to a FDSN web service),
a Parser (e.g. from a dataless SEED file), a filename of a local RESP file (or a legacy poles and zeros dictionary).
Then we initialize a new PPSD instance. The ppsd object will then make sure that only appropriate data go into the
probabilistic psd statistics.
>>> parser = Parser("https://examples.obspy.org/dataless.seed.BW_KW1")
>>> ppsd = PPSD(tr.stats, metadata=parser)
Now we can add data (either trace or stream objects) to the ppsd estimate. This step may take a while. The return
value True indicates that the data was successfully added to the ppsd estimate.
>>> ppsd.add(st)
True
We can check what time ranges are represented in the ppsd estimate. ppsd.times contains a sorted list of start
times of the one hour long slices that the psds are computed from (here only the first two are printed).
>>> print(ppsd.times[:2])
[UTCDateTime(2011, 2, 6, 0, 0, 0, 935000), UTCDateTime(2011, 2, 6, 0, 30, 0, 935000)]
>>> print("number of psd segments:", len(ppsd.times))
number of psd segments: 47
Adding the same stream again will do nothing (return value False), the ppsd object makes sure that no overlapping
data segments go into the ppsd estimate.
>>> ppsd.add(st)
False
>>> print("number of psd segments:", len(ppsd.times))
number of psd segments: 47
A (for each frequency bin) cumulative version of the histogram can also be visualized:
>>> ppsd.plot(cumulative=True)
To use the colormap used by PQLX / [McNamara2004] you can import and use that colormap from
obspy.imaging.cm:
>>> from obspy.imaging.cm import pqlx
>>> ppsd.plot(cmap=pqlx)
Below the actual PPSD (for a detailed discussion see [McNamara2004]) is a visualization of the data basis for the
PPSD (can also be switched off during plotting). The top row shows data fed into the PPSD, green patches represent
available data, red patches represent gaps in streams that were added to the PPSD. The bottom row in blue shows the
single psd measurements that go into the histogram. The default processing method fills gaps with zeros, these data
segments then show up as single outlying psd lines.
Note: Providing metadata from e.g. a Dataless SEED or StationXML volume is safer than specifying static poles and
zeros information (see PPSD).
Time series of psd values can also be extracted from the PPSD by accessing the property psd_values and plotted
using the plot_temporal() method (temporal restrictions can be used in the plot, see documentation):
>>> ppsd.plot_temporal([0.1, 1, 10])
The following code block shows how to plot the array transfer function for beam forming as a function of wavenumber
using the ObsPy function obspy.signal.array_analysis.array_transff_wavenumber().
import numpy as np
import matplotlib.pyplot as plt
# coordinates in km
coords /= 1000.
# plot
plt.pcolor(np.arange(kxmin, kxmax + kstep * 1.1, kstep) - kstep / 2.,
np.arange(kymin, kymax + kstep * 1.1, kstep) - kstep / 2.,
transff.T, cmap=obspy_sequential)
plt.colorbar()
plt.clim(vmin=0., vmax=1.)
plt.xlim(kxmin, kxmax)
plt.ylim(kymin, kymax)
plt.show()
The following is a short example for a continuous wavelet transform using ObsPy’s internal routine based on [Kris-
tekova2006].
import numpy as np
import matplotlib.pyplot as plt
import obspy
from obspy.imaging.cm import obspy_sequential
from obspy.signal.tf_misfit import cwt
st = obspy.read()
tr = st[0]
npts = tr.stats.npts
dt = tr.stats.delta
t = np.linspace(0, dt * npts, npts)
f_min = 1
f_max = 50
fig = plt.figure()
ax = fig.add_subplot(111)
x, y = np.meshgrid(
t,
np.logspace(np.log10(f_min), np.log10(f_max), scalogram.shape[0]))
Small script doing the continuous wavelet transform using the mlpy package (version 3.5.0) for infrasound data
recorded at Yasur in 2008. Further details on wavelets can be found at Wikipedia - in the article the omega0 fac-
tor is denoted as sigma. (really sloppy and possibly incorrect: the omega0 factor tells you how often the wavelet fits
into the time window, dj defines the spacing in the scale domain)
import numpy as np
import matplotlib.pyplot as plt
import mlpy
import obspy
from obspy.imaging.cm import obspy_sequential
tr = obspy.read("https://examples.obspy.org/a02i.2008.240.mseed")[0]
omega0 = 8
wavelet_fct = "morlet"
scales = mlpy.wavelet.autoscales(N=len(tr.data), dt=tr.stats.delta, dj=0.05,
wf=wavelet_fct, p=omega0)
spec = mlpy.wavelet.cwt(tr.data, dt=tr.stats.delta, scales=scales,
wf=wavelet_fct, p=omega0)
# approximate scales through frequencies
freq = (omega0 + np.sqrt(2.0 + omega0 ** 2)) / (4 * np.pi * scales[1:])
fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.75, 0.7, 0.2])
ax2 = fig.add_axes([0.1, 0.1, 0.7, 0.60], sharex=ax1)
ax3 = fig.add_axes([0.83, 0.1, 0.03, 0.6])
t = np.arange(tr.stats.npts) / tr.stats.sampling_rate
ax1.plot(t, tr.data, ’k’)
fig.colorbar(img, cax=ax3)
plt.show()
The tf_misfit module offers various Time Frequency Misfit Functions based on [Kristekova2006] and [Kris-
tekova2009].
Here are some examples how to use the included plotting tools:
import numpy as np
# general constants
tmax = 6.
dt = 0.01
npts = int(tmax / dt + 1)
t = np.linspace(0., tmax, npts)
fmin = .5
fmax = 10
Time Frequency Misfits are appropriate for smaller differences of the signals. Continuing the example from above:
from scipy.signal import hilbert
from obspy.signal.tf_misfit import plot_tf_misfits
# reference signal
st2 = st1.copy()
plt.show()
Time Frequency GOFs are appropriate for large differences of the signals. Continuing the example from above:
from obspy.signal.tf_misfit import plot_tf_gofs
plt.show()
For multi component data and global normalization of the misfits, the axes are scaled accordingly. Continuing the
example from above:
# amplitude error
amp_fac = 1.1
# reference signals
st2_1 = st1.copy()
st2_2 = st1.copy() * 5.
st2 = np.c_[st2_1, st2_2].T
Local normalization allows to resolve frequency and time ranges away from the largest amplitude waves, but tend to
produce artifacts in regions where there is no energy at all. In this analytical example e.g. for the high frequencies
before the onset of the signal. Manual setting of the limits is thus necessary:
# amplitude and phase error
amp_fac = 1.1
# reference signal
st2 = st1.copy()
plt.show()
Often, you have a bunch of data and want to know which station is available at what time. For this purpose, ObsPy
ships the obspy-scan script (automatically available after installation), which detects the file format (MiniSEED,
SAC, SACXY, GSE2, SH-ASC, SH-Q, SEISAN, etc.) from the header of the data files. Gaps are plotted as vertical
red lines, start times of available data are plotted as crosses - the data itself are plotted as horizontal lines.
The script can be used to scan through 1000s of files (already used with 30000 files, execution time ca. 45min),
month/year ranges are plotted automatically. It opens an interactive plot in which you can zoom in ...
Execute something like following line from the command prompt, use e.g. wildcards to match the files:
$ obspy-scan /bay_mobil/mobil/20090622/1081019/*_1.*
The following lines show how to use the convenience wrapper function plot_travel_times() to plot the travel
times for a given distance range and selected phases, calculated with the iasp91 velocity model.
from obspy.taup import plot_travel_times
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax = plot_travel_times(source_depth=10, ax=ax, fig=fig,
phase_list=[’P’, ’PP’, ’S’], npoints=200)
The following lines show how to plot the ray paths for a given distance, and phase(s). The ray paths are calculated
with the iasp91 velocity model, and plotted on a Cartesian map, using the plot_rays() method of the class
obspy.taup.tau.Arrivals.
from obspy.taup import TauPyModel
model = TauPyModel(model=’iasp91’)
arrivals = model.get_ray_paths(500, 140, phase_list=[’PP’, ’SSS’])
arrivals.plot_rays(plot_type=’cartesian’, phase_list=[’PP’, ’SSS’],
plot_all=False, legend=True)
The following lines show how to plot the ray paths for a given distance, and phase(s). The ray paths are calculated
with the iasp91 velocity model, and plotted on a spherical map, using the plot_rays() method of the class
obspy.taup.tau.Arrivals.
from obspy.taup import TauPyModel
model = TauPyModel(model=’iasp91’)
arrivals = model.get_ray_paths(500, 140, phase_list=[’Pdiff’, ’SS’])
arrivals.plot_rays(plot_type=’spherical’, phase_list=[’Pdiff’, ’SS’],
legend=True)
The following lines plot the ray paths for several epicentral distances, and phases. The rays are calculated with the
iasp91 velocity model, and the plot is made using the convenience wrapper function plot_ray_paths().
from obspy.taup.tau import plot_ray_paths
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw=dict(polar=True))
ax = plot_ray_paths(source_depth=100, ax=ax, fig=fig, phase_list=[’P’, ’PKP’],
npoints=25)
For examples with rays for a single epicentral distance, try the plot_rays() method in the previous section. The
following is a more advanced example with a custom list of phases and distances:
import numpy as np
import matplotlib.pyplot as plt
PHASES = [
# Phase, distance
(’P’, 26),
(’PP’, 60),
(’PPP’, 94),
(’PPS’, 155),
(’p’, 3),
(’pPcP’, 100),
(’PKIKP’, 170),
(’PKJKP’, 194),
(’S’, 65),
(’SP’, 85),
(’SS’, 134.5),
(’SSS’, 204),
(’p’, -10),
(’pP’, -37.5),
(’s’, -3),
(’sP’, -49),
(’ScS’, -44),
(’SKS’, -82),
(’SKKS’, -120),
]
model = TauPyModel(model=’iasp91’)
fig, ax = plt.subplots(subplot_kw=dict(polar=True))
# Annotate regions
ax.text(0, 0, ’Solid\ninner\ncore’,
horizontalalignment=’center’, verticalalignment=’center’,
bbox=dict(facecolor=’white’, edgecolor=’none’, alpha=0.7))
ocr = (model.model.radius_of_planet -
(model.model.s_mod.v_mod.iocb_depth +
model.model.s_mod.v_mod.cmb_depth) / 2)
ax.text(np.deg2rad(180), ocr, ’Fluid outer core’,
horizontalalignment=’center’,
bbox=dict(facecolor=’white’, edgecolor=’none’, alpha=0.7))
mr = model.model.radius_of_planet - model.model.s_mod.v_mod.cmb_depth / 2
ax.text(np.deg2rad(180), mr, ’Solid mantle’,
horizontalalignment=’center’,
bbox=dict(facecolor=’white’, edgecolor=’none’, alpha=0.7))
plt.show()
This example shows how to align the waveforms of phase onsets of two earthquakes in order to correct the original
pick times that can never be set perfectly consistent in routine analysis. A parabola is fit to the concave part of the
cross correlation function around its maximum, following the approach by [Deichmann1992].
To adjust the parameters (i.e. the used time window around the pick and the filter settings) and to validate and check
the results the options plot and filename can be used to open plot windows or save the figure to a file.
See the documentation of xcorr_pick_correction() for more details.
The example will print the time correction for pick 2 and the respective correlation coefficient and open a plot window
for correlations on both the original and preprocessed data:
No preprocessing:
Time correction for pick 2: -0.014459
Correlation coefficient: 0.92
Bandpass prefiltering:
Time correction for pick 2: -0.013025
Correlation coefficient: 0.98
import obspy
from obspy.signal.cross_correlation import xcorr_pick_correction
# estimate the time correction for pick 2 without any preprocessing and open
# a plot window to visually validate the results
dt, coeff = xcorr_pick_correction(t1, tr1, t2, tr2, 0.05, 0.2, 0.1, plot=True)
print("No preprocessing:")
print(" Time correction for pick 2: %.6f" % dt)
print(" Correlation coefficient: %.2f" % coeff)
# estimate the time correction with bandpass prefiltering
dt, coeff = xcorr_pick_correction(t1, tr1, t2, tr2, 0.05, 0.2, 0.1, plot=True,
filter="bandpass",
filter_options={’freqmin’: 1, ’freqmax’: 10})
print("Bandpass prefiltering:")
print(" Time correction for pick 2: %.6f" % dt)
print(" Correlation coefficient: %.2f" % coeff)
1.31 Handling custom defined tags in QuakeML and the ObsPy Cata-
log/Event framework
QuakeML allows use of custom elements in addition to the ‘usual’ information defined by the QuakeML standard. It
allows a) custom namespace attributes to QuakeML namespace tags and b) custom namespace subtags to QuakeML
namespace elements. ObsPy can handle both basic custom tags in event type objects (a) and custom attributes (b)
during input/output to/from QuakeML. The following basic example illustrates how to output a valid QuakeML file
with custom xml tags/attributes:
from obspy import Catalog, UTCDateTime
cat = Catalog()
cat.extra = extra
cat.write(’my_catalog.xml’, format=’QUAKEML’,
nsmap={’my_ns’: ’http://test.org/xmlns/0.1’})
All custom information to be stored in the customized QuakeML has to be stored in form of a dict or AttribDict
object as the extra attribute of the object that should carry the additional custom information (e.g. Catalog,
Event, Pick). The keys are used as the name of the xml tag, the content of the xml tag is defined in a simple
dictionary: ’value’ defines the content of the tag (the string representation of the object gets stored in the textual
xml output). ’namespace’ has to specify a custom namespace for the tag. ’type’ can be used to specify whether
the extra information should be stored as a subelement (’element’, default) or as an attribute (’attribute’).
Attributes to custom subelements can be provided in form of a dictionary as ’attrib’. If desired for better (human-
)readability, namespace abbreviations in the output xml can be specified during output as QuakeML by providing a
dictionary of namespace abbreviation mappings as nsmap parameter to Catalog.write(). The xml output of the
above example looks like:
<?xml version=’1.0’ encoding=’utf-8’?>
<q:quakeml xmlns:q=’http://quakeml.org/xmlns/quakeml/1.2’
xmlns:ns0=’http://some-page.de/xmlns/1.0’
xmlns:my_ns=’http://test.org/xmlns/0.1’
xmlns=’http://quakeml.org/xmlns/bed/1.2’>
<eventParameters publicID=’smi:local/b425518c-9445-40c7-8284-d1f299ed2eac’
my_ns:my_attribute=’my_attribute_value’>
<ns0:my_tag ns0:my_attrib1=’123.4’ ns0:my_attrib2=’567’>true</ns0:my_tag>
<my_ns:my_tag_4>2013-01-02T13:12:14.600000Z</my_ns:my_tag_4>
<ns0:my_tag_2>True</ns0:my_tag_2>
<ns0:my_tag_3>1</ns0:my_tag_3>
</eventParameters>
</q:quakeml>
When reading the above xml again, using read_events(), the custom tags get parsed and attached to the respective
Event type objects (in this example to the Catalog object) as .extra. Note that all values are read as text strings:
from obspy import read_events
cat = read_events(’my_catalog.xml’)
print(cat.extra)
1.31. Handling custom defined tags in QuakeML and the ObsPy Catalog/Event framework 51
ObsPy Tutorial, Release 1.1.0
u’value’: ’True’},
u’my_tag_3’: {u’namespace’: u’http://some-page.de/xmlns/1.0’,
u’value’: ’1’}})
ns = ’http://some-page.de/xmlns/1.0’
my_tag = AttribDict()
my_tag.namespace = ns
my_tag.value = AttribDict()
my_tag.value.my_nested_tag1 = AttribDict()
my_tag.value.my_nested_tag1.namespace = ns
my_tag.value.my_nested_tag1.value = 1.23E+10
my_tag.value.my_nested_tag2 = AttribDict()
my_tag.value.my_nested_tag2.namespace = ns
my_tag.value.my_nested_tag2.value = True
cat = Catalog()
cat.extra = AttribDict()
cat.extra.my_tag = my_tag
cat.write(’my_catalog.xml’, ’QUAKEML’)
The output xml can be read again using read_events() and the nested tags can be retrieved in the following way:
from obspy import read_events
cat = read_events(’my_catalog.xml’)
print(cat.extra.my_tag.value.my_nested_tag1.value)
print(cat.extra.my_tag.value.my_nested_tag2.value)
12300000000.0
true
The order of extra tags can be controlled by using an OrderedDict for the extra attribute (using a plain dict or
AttribDict can result in arbitrary order of tags):
from collections import OrderedDict
from obspy.core.event import Catalog, Event
ns = ’http://some-page.de/xmlns/1.0’
event = Event()
cat = Catalog(events=[event])
event.extra = OrderedDict()
event.extra[’myFirstExtraTag’] = my_tag2
event.extra[’mySecondExtraTag’] = my_tag1
cat.write(’my_catalog.xml’, ’QUAKEML’)
1.32 Handling custom defined tags in StationXML with the Obspy In-
ventory
StationXML allows use of custom elements in addition to the ‘usual’ information defined by the StationXML stan-
dard. It allows a) custom namespace attributes to StationXML namespace tags and b) custom namespace subtags to
StationXML namespace elements. ObsPy can handle both basic custom tags in all main elements (Network, Station,
Channel, etc.) (a) and custom attributes (b) during input/output to/from StationXML. The following basic example
illustrates how to output a StationXML file that contains additional xml tags/attributes:
from obspy import Inventory, UTCDateTime
from obspy.core.inventory import Network
from obspy.core.util import AttribDict
extra = AttribDict({
’my_tag’: {
’value’: True,
’namespace’: ’http://some-page.de/xmlns/1.0’,
’attrib’: {
’{http://some-page.de/xmlns/1.0}my_attrib1’: ’123.4’,
’{http://some-page.de/xmlns/1.0}my_attrib2’: ’567’
}
},
’my_tag_2’: {
’value’: u’True’,
’namespace’: ’http://some-page.de/xmlns/1.0’
},
’my_tag_3’: {
’value’: 1,
’namespace’: ’http://some-page.de/xmlns/1.0’
},
’my_tag_4’: {
’value’: UTCDateTime(’2013-01-02T13:12:14.600000Z’),
’namespace’: ’http://test.org/xmlns/0.1’
},
’my_attribute’: {
’value’: ’my_attribute_value’,
’type’: ’attribute’,
’namespace’: ’http://test.org/xmlns/0.1’
}
})
1.32. Handling custom defined tags in StationXML with the Obspy Inventory 53
ObsPy Tutorial, Release 1.1.0
inv.write(’my_inventory.xml’, format=’STATIONXML’,
nsmap={’my_ns’: ’http://test.org/xmlns/0.1’,
’somepage_ns’: ’http://some-page.de/xmlns/1.0’})
All custom information to be stored in the customized StationXML has to be stored in form of a dict or
AttribDict object as the extra attribute of the object that should carry the additional custom information (e.g.
Network, Station, Channel). The keys are used as the name of the xml tag, the content of the xml tag is
defined in a simple dictionary: ’value’ defines the content of the tag (the string representation of the object
gets stored in the textual xml output). ’namespace’ has to specify a custom namespace for the tag. ’type’
can be used to specify whether the extra information should be stored as a subelement (’element’, default) or
as an attribute (’attribute’). Attributes to custom subelements can be provided in form of a dictionary as
’attrib’. If desired for better (human-)readability, namespace abbreviations in the output xml can be specified
during output as StationXML by providing a dictionary of namespace abbreviation mappings as nsmap parameter to
Inventory.write(). The xml output of the above example looks like:
<?xml version=’1.0’ encoding=’UTF-8’?>
<FDSNStationXML xmlns:my_ns="http://test.org/xmlns/0.1" xmlns:somepage_ns="http://some-page.de/xmlns/
<Source>XX</Source>
<Module>ObsPy 1.0.2</Module>
<ModuleURI>https://www.obspy.org</ModuleURI>
<Created>2016-10-17T18:32:28.696287+00:00</Created>
<Network code="XX">
<somepage_ns:my_tag somepage_ns:my_attrib1="123.4" somepage_ns:my_attrib2="567">True</somepage_ns
<my_ns:my_tag_4>2013-01-02T13:12:14.600000Z</my_ns:my_tag_4>
<my_ns:my_attribute>my_attribute_value</my_ns:my_attribute>
<somepage_ns:my_tag_2>True</somepage_ns:my_tag_2>
<somepage_ns:my_tag_3>1</somepage_ns:my_tag_3>
</Network>
</FDSNStationXML>
When reading the above xml again, using read_inventory(), the custom tags get parsed and attached to the
respective Network type objects (in this example to the Inventory object) as .extra. Note that all values are read as
text strings:
from obspy import read_inventory
inv = read_inventory(’my_inventory.xml’)
print(inv[0].extra)
AttribDict({
u’my_tag’: AttribDict({
’attrib’: {
’{http://some-page.de/xmlns/1.0}my_attrib2’: ’567’,
’{http://some-page.de/xmlns/1.0}my_attrib1’: ’123.4’
},
’namespace’: ’http://some-page.de/xmlns/1.0’,
’value’: ’True’
}),
u’my_tag_4’: AttribDict({
’namespace’: ’http://test.org/xmlns/0.1’,
’value’: ’2013-01-02T13:12:14.600000Z’
}),
u’my_attribute’: AttribDict({
’namespace’: ’http://test.org/xmlns/0.1’,
’value’: ’my_attribute_value’
}),
u’my_tag_2’: AttribDict({
’namespace’: ’http://some-page.de/xmlns/1.0’,
’value’: ’True’
}),
u’my_tag_3’: AttribDict({
’namespace’: ’http://some-page.de/xmlns/1.0’,
’value’: ’1’
})
})
ns = ’http://some-page.de/xmlns/1.0’
my_tag = AttribDict()
my_tag.namespace = ns
my_tag.value = AttribDict()
my_tag.value.my_nested_tag1 = AttribDict()
my_tag.value.my_nested_tag1.namespace = ns
my_tag.value.my_nested_tag1.value = 1.23E+10
my_tag.value.my_nested_tag2 = AttribDict()
my_tag.value.my_nested_tag2.namespace = ns
my_tag.value.my_nested_tag2.value = True
The output xml can be read again using read_inventory() and the nested tags can be retrieved in the following
way:
from obspy import read_inventory
inv = read_inventory(’my_inventory.xml’)
print(inv[0].extra.my_tag.value.my_nested_tag1.value)
print(inv[0].extra.my_tag.value.my_nested_tag2.value)
1.32. Handling custom defined tags in StationXML with the Obspy Inventory 55
ObsPy Tutorial, Release 1.1.0
12300000000.0
True
Creating a custom StationXML file is a task that sometimes comes up in seismology. This section demonstrates how
to it with ObsPy. Please note that this is not necessarily easier or more obvious then directly editing an XML file but
it does provider tighter integration with the rest of ObsPy and can guarantee a valid result at the end.
Note that this assumes a certain familiarity with the FDSN StationXML standard. We’ll create a fairly simplistic
StationXML file and many arguments are optional. ObsPy will validate the resulting StationXML file against its
schema upon writing so the final file is assured to be valid against the StationXML schema.
The following illustration shows the basic structure of ObsPy’s internal representation.
Each big box will be an object and all objects will have to be hierarchically linked to form a single Inventory object.
An inventory can contain any number of Network objects, which in turn can contain any number of Station
objects, which once again in turn can contain any number of Channel objects. For each channel, the instrument
response can be stored as the response attribute.
Instrument Response can be looked up and attached to the channels from the IRIS DMC Library of Nominal Responses
for Seismic Instruments (NRL) using ObsPy’s NRL client.
import obspy
from obspy.core.inventory import Inventory, Network, Station, Channel, Site
from obspy.clients.nrl import NRL
# We’ll first create all the various objects. These strongly follow the
# hierarchy of StationXML files.
inv = Inventory(
# We’ll add networks later.
networks=[],
# The source should be the id whoever create the file.
source="ObsPy-Tutorial")
net = Network(
# This is the network code according to the SEED standard.
code="XX",
# A list of stations. We’ll add one later.
stations=[],
description="A test stations.",
# Start-and end dates are optional.
start_date=obspy.UTCDateTime(2016, 1, 2))
sta = Station(
# This is the station code according to the SEED standard.
code="ABC",
latitude=1.0,
longitude=2.0,
elevation=345.0,
creation_date=obspy.UTCDateTime(2016, 1, 2),
site=Site(name="First station"))
cha = Channel(
# This is the channel code according to the SEED standard.
code="HHZ",
# This is the location code according to the SEED standard.
location_code="",
# Note that these coordinates can differ from the station coordinates.
latitude=1.0,
longitude=2.0,
elevation=345.0,
depth=10.0,
azimuth=0.0,
dip=-90.0,
sample_rate=200)
# By default this accesses the NRL online. Offline copies of the NRL can
# also be used instead
nrl = NRL()
# The contents of the NRL can be explored interactively in a Python prompt,
# see API documentation of NRL submodule:
# http://docs.obspy.org/packages/obspy.clients.nrl.html
# Here we assume that the end point of data logger and sensor are already
# known:
response = nrl.get_response( # doctest: +SKIP
sensor_keys=[’Streckeisen’, ’STS-1’, ’360 seconds’],
datalogger_keys=[’REF TEK’, ’RT 130 & 130-SMA’, ’1’, ’200’])
The obspy.clients.seedlink module provides a Python implementation of the SeedLink client protocol. The
obspy.clients.seedlink.easyseedlink submodule contains a high-level interface to the SeedLink im-
plementation that facilitates the creation of a SeedLink client.
The easiest way to connect to a SeedLink server is using the create_client() function to create a new instance
of the EasySeedLinkClient class. It accepts as an argument a function that handles new data received from the
SeedLink server, for example:
def handle_data(trace):
print(’Received the following trace:’)
print(trace)
print()
This function can then be passed to create_client() together with a SeedLink server URL to create a client
instance:
client = create_client(’geofon.gfz-potsdam.de’, on_data=handle_data)
The client instance can be used to send SeedLink INFO requests to the server:
# Send the INFO:ID request
client.get_info(’ID’)
# Returns:
# <?xml version="1.0"?>\n<seedlink software="SeedLink v3.2 (2014.071)" organization="GEOFON" started=
The responses to INFO requests are in XML format. The client provides a shortcut to retrieve and parse the server’s
capabilities (via an INFO:CAPABILITIES request):
>>> client.capabilities
[’dialup’, ’multistation’, ’window-extraction’, ’info:id’, ’info:capabilities’, ’info:stations’, ’inf
The capabilities are fetched and parsed when the attribute is first accessed and are cached after that.
In order to start receiving waveform data, a stream needs to be selected. This is done by calling the
select_stream() method:
client.select_stream(’BW’, ’MANZ’, ’EHZ’)
After having selected the streams, the client is ready to enter streaming mode:
client.run()
This starts streaming data from the server. Upon every complete trace that is received from the server, the function
defined above is called with the trace object:
Received new data:
BW.MANZ..EHZ | 2014-09-04T19:47:25.625000Z - 2014-09-04T19:47:26.770000Z | 200.0 Hz, 230 samples
The create_client() function also accepts functions to be called when the connection terminates or when a
SeedLink error is received. See the documentation for details.
For advanced use cases, subclassing the EasySeedLinkClient class allows for finer control over the instance.
Implementing the same client as above:
class DemoClient(EasySeedLinkClient):
"""
A custom SeedLink client
"""
def on_data(self, trace):
"""
Override the on_data callback
"""
print(’Received trace:’)
print(trace)
print()
TWO
ADVANCED EXERCISE
In the advanced exercise we show how ObsPy can be used to develop an automated processing workflow. We start out
with very simple tasks and then automate the routine step by step. For all exercises solutions are provided.
This practical intends to demonstrate how ObsPy can be used to develop workflows for data processing and analysis
that have a short, easy to read and extensible source code. The overall task is to automatically estimate local magnitudes
of earthquakes using data of the SED network. We will start with simple programs with manually specified, hard-coded
values and build on them step by step to make the program more flexible and dynamic. Some details in the magnitude
estimation should be done a little bit different technically but we rather want to focus on the general workflow here.
61
ObsPy Tutorial, Release 1.1.0
Fetch a list of events from EMSC for the region of Valais/SW-Switzerland on 3rd April of 2012. Use the Client
provided in obspy.clients.fdsn. Note down the catalog origin times, epicenters and magnitudes.
1. Use the file LKBD_WA_CUT.MSEED to read MiniSEED waveform data of the larger earthquake. These data
have already been simulated to (demeaned) displacement on a Wood-Anderson seismometer (in meter) and
trimmed to the right time span. Compute the absolute maximum for both North and East component and use
the larger value as the zero-to-peak amplitude estimate. Estimate the local magnitude 𝑀lh used at the Swiss
Seismological Service (SED) using a epicentral distance of 𝑑epi = 20 (km), 𝑎 = 0.018 and 𝑏 = 2.17 with the
following formula (mathematical functions are available in Python’s math module):
(︁ mm )︁
𝑀lh = log10 𝑎𝑚𝑝 · 1000 + 𝑎 · 𝑑epi + 𝑏
m
2. Calculate the epicentral distance from the station coordinates (46.387°N, 7.627°E) and catalog epi-
center fetched above (46.218°N, 7.706°E). Some useful routines for such tasks are included in
obspy.core.util.geodetics.
1. Modify the existing code and use the file LKBD.MSEED to read the original MiniSEED waveform data in
counts. Set up two dictionaries containing the response information of both the original instrument (a LE3D-
5s) and the Wood-Anderson seismometer in poles-and-zeros formulation. Please note that for historic reasons
the naming of keys differs from the usual naming. Each PAZ dictionary needs to contain sensitivity (overall
sensitivity of seismometer/digitizer combination), gain (A0 / normalization factor), poles and zeros. Check that
the value of water_level is not too high, to avoid overamplified low frequency noise at short-period stations.
After the instrument simulation, trim the waveform to a shorter time window around the origin time (2012-04-
03T02:45:03) and calculate 𝑀lh like before. Use the following values for the PAZ dictionaries:
– LE3D-5s Wood-Anderson
poles -0.885+0.887j -0.885-0.887j -0.427+0j -6.2832-4.7124j -6.2832+4.7124j
zeros 0j, 0j, 0j 0j
gain 1.009 1
sensitivity 167364000.0 2800
2. Instead of the hard-coded values, read the response information from a locally stored dataless SEED
LKBD.dataless. Use the Parser of module obspy.xseed to extract the poles-and-zeros information of the
used channel.
3. We can also request the response information from WebDC using the ArcLink protocol. Use the Client provided
in obspy.arclink module (specify e.g. user=”sed-workshop@obspy.org”).
1. Modify the existing code and fetch waveform data around the origin time given above for station LKBD (network
CH) via ArcLink from WebDC using obspy.arclink. Use a wildcarded channel=”EH*” to fetch all three compo-
nents. Use keyword argument metadata=True to fetch response information and station coordinates along with
the waveform. The PAZ and coordinate information will get attached to the Stats object of all traces in the
returned Stream object during the waveform request automatically. During instrument simulation use keyword
argument paz_remove=’self’ to use every trace’s attached PAZ information fetched from WebDC. Calculate 𝑀lh
like before.
2. Use a list of station names (e.g. LKBD, SIMPL, DIX) and perform the magnitude estimation in a loop for
each station. Use a wildcarded channel=”[EH]H*” to fetch the respective streams for both short-period and
broadband stations. Compile a list of all station magnitudes and compute the network magnitude as its median
(available in numpy module).
3. Extend the network magnitude estimate by using all available stations in network CH. Get a list of stations using
the ArcLink client and loop over this list. Use a wildcarded channel=”[EH]H[ZNE]”, check if there are three
traces in the returned stream and skip to next station otherwise (some stations have inconsistent component
codes). Put a try/except around the waveform request and skip to the next station and avoid interruption of the
routine in case no data can be retrieved and an Exception gets raised. Also add an if/else and use 𝑎 = 0.0038
and 𝑏 = 3.02 in station magnitude calculation for epicentral distances of more than 60 kilometers.
In this additional advanced exercise we can enhance the routine to be independent of a-priori known origin times by
using a coincidence network trigger for event detection.
• fetch a few hours of Z component data for 6 stations in Valais / SW-Switzerland
• run a coincidence trigger like shown in the Trigger Tutorial
• loop over detected network triggers, store the coordinates of the closest station as the epicenter
• loop over triggers, use the trigger time to select the time window and use the network magnitude estimation
code like before
2.1.6 Solutions
import obspy
import obspy.clients.fdsn
client = obspy.clients.fdsn.Client("neries")
st = read("../data/LKBD_WA_CUT.MSEED")
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
epi_dist = 20
a = 0.018
b = 2.17
ml = log10(ampl * 1000) + a * epi_dist + b
print(ml)
st = read("../data/LKBD_WA_CUT.MSEED")
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = 46.38703
sta_lon = 7.62714
event_lat = 46.218
event_lon = 7.706
a = 0.018
b = 2.17
ml = log10(ampl * 1000) + a * epi_dist + b
print(ml)
st = read("../data/LKBD.MSEED")
t = UTCDateTime("2012-04-03T02:45:03")
st.trim(t, t + 50)
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = 46.38703
sta_lon = 7.62714
event_lat = 46.218
event_lon = 7.706
a = 0.018
b = 2.17
ml = log10(ampl * 1000) + a * epi_dist + b
print(ml)
st = read("../data/LKBD.MSEED")
parser = Parser("../data/LKBD.dataless")
paz_le3d5s = parser.get_paz("CH.LKBD..EHZ")
t = UTCDateTime("2012-04-03T02:45:03")
st.trim(t, t + 50)
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = 46.38703
sta_lon = 7.62714
event_lat = 46.218
event_lon = 7.706
a = 0.018
b = 2.17
st = read("../data/LKBD.MSEED")
client = Client(user="sed-workshop@obspy.org")
t = st[0].stats.starttime
paz_le3d5s = client.get_paz("CH", "LKBD", "", "EHZ", t)
t = UTCDateTime("2012-04-03T02:45:03")
st.trim(t, t + 50)
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = 46.38703
sta_lon = 7.62714
event_lat = 46.218
event_lon = 7.706
a = 0.018
b = 2.17
ml = log10(ampl * 1000) + a * epi_dist + b
print(ml)
client = Client(user="sed-workshop@obspy.org")
t = UTCDateTime("2012-04-03T02:45:03")
st = client.get_waveforms("CH", "LKBD", "", "EH*", t - 300, t + 300,
metadata=True)
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = 46.38703
sta_lon = 7.62714
event_lat = 46.218
event_lon = 7.706
a = 0.018
b = 2.17
ml = log10(ampl * 1000) + a * epi_dist + b
print(ml)
import numpy as np
client = Client(user="sed-workshop@obspy.org")
t = UTCDateTime("2012-04-03T02:45:03")
metadata=True)
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = st[0].stats.coordinates.latitude
sta_lon = st[0].stats.coordinates.longitude
event_lat = 46.218
event_lon = 7.706
a = 0.018
b = 2.17
ml = log10(ampl * 1000) + a * epi_dist + b
print(station, ml)
mags.append(ml)
net_mag = np.median(mags)
print("Network magnitude:", net_mag)
import numpy as np
client = Client(user="sed-workshop@obspy.org")
t = UTCDateTime("2012-04-03T02:45:03")
except Exception:
print(station, "---")
continue
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = st[0].stats.coordinates.latitude
sta_lon = st[0].stats.coordinates.longitude
event_lat = 46.218
event_lon = 7.706
net_mag = np.median(mags)
print("Network magnitude:", net_mag)
import numpy as np
client = Client(user="sed-workshop@obspy.org")
t = UTCDateTime("2012-04-03T01:00:00")
t2 = t + 4 * 3600
st.taper()
st.filter("bandpass", freqmin=1, freqmax=20)
triglist = coincidence_trigger("recstalta", 10, 2, st, 4, sta=0.5, lta=10)
print(len(triglist), "events triggered.")
tr_n = st.select(component="N")[0]
ampl_n = max(abs(tr_n.data))
tr_e = st.select(component="E")[0]
ampl_e = max(abs(tr_e.data))
ampl = max(ampl_n, ampl_e)
sta_lat = st[0].stats.coordinates.latitude
sta_lon = st[0].stats.coordinates.longitude
event_lat = trig[’latitude’]
event_lon = trig[’longitude’]
net_mag = np.median(mags)
print("Network magnitude:", net_mag)
A
ar_pick() (in module obspy.signal.trigger), 22
C
carl_sta_trig() (in module obspy.signal.trigger), 20
classic_sta_lta() (in module obspy.signal.trigger), 21
D
delayed_sta_lta() (in module obspy.signal.trigger), 21
P
pk_baer() (in module obspy.signal.trigger), 21
R
recursive_sta_lta() (in module obspy.signal.trigger), 20
Z
z_detect() (in module obspy.signal.trigger), 21
73