This page has been moved to http://tutorial.obspy.org!
ObsPy Tutorial (Old)
Table of Content
- Python Introduction for Seismologists
- Reading Seismograms
- Waveform Plotting [Separate page]
- Filtering Seismograms
- Downsampling Seismograms
- Seismogram Envelopes
- Seismometer Correction/Simulation
- Plotting Spectrogram
- Poles and Zeros/ Frequency Response
- Retrieving Data via ArcLink
- Trigger/Picker [Separate page]
- ROSE Signal Processing Course with ObsPy examples [External]
- Beachball plot
- Basemap plot with beachballs
- Convert Anything to MiniSEED
- Checking Data availability for data files
- Saving Python objects as MATLAB MAT files
- Interfacing R-cran from Python
- Seismogram to ASCII
- Merging Seismograms
- Beamforming --- FK Analysis
- Array Response Function
- Continuous Wavelet Transform
- Coordinate Conversions
- Hierarchical Clustering
- Building a new dataless-SEED file
- Visualizing Probabilistic Power Spectral Densities (PPSD)
Python Introduction for Seismologists
Here we want to give a small, incomplete intro to the Python programming language, with links to useful packages and further resources. The key features are explained via the following Python script:
#!/usr/bin/env python import glob from obspy.core import read for file in glob.glob("*.z"): st = read(file) tr = st[0] msg = "%s %s %f %f" % (tr.stats.station, str(tr.stats.starttime), tr.data.mean(), tr.data.std()) print msg
- Line 1: Specifies the location of the python interpreter (not necessary on windows).
- Lines 2-3: Import modules/libraries/packages in the current namespace. The glob module, which allows wildcard matching on filenames, is imported here. All functions inside this module can be accessed via glob.function() afterwards. Furthermore, a single function read() from the obspy.core module is imported, which is used to read various different seismogram file formats.
- Line 4: Starts a for-loop using the glob function of the module glob, glob.glob(), on all files ending with z. 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 5: Uses the read function from the obspy.core module to read in the seismogram
- Line 6: Assigns the first Trace object of the list-like Stream object to the variable tr
- Line 7: Python has no printf-like function, but a Python counterpart for the sprintf function is the % operator acting on a format string. Here we print the header attributes station and the string representation of starttime as well as the return value of the methods mean() and std() acting on the data subobjects of the Trace (which are of type numpy.ndarray)
- Line 8: Prints string of line 7
As Python is an interpreter language, we recommend to use a shell for rapid development and trying things out. We recommend the IPython shell, which 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).
Further Resources:
- http://docs.python.org/tutorial/ Official Python tutorial.
- http://docs.python.org/library/index.html Python library reference, highlights:
- Module glob. The function glob.glob allows wildcard matching.
- Module sys. The list sys.argv contains the command line arguments.
- Module os.path. File name manipulation.
- Module subprocess. The function subprocess.call allows system calls.
- Module pdb. Inserting the line import pdb; pdb.set_trace() into a Python script/module/... and running that script starts a debugger on that line.
- http://ipython.scipy.org/moin An enhanced interactive Python shell.
- http://docs.scipy.org NumPy and SciPy are the matrix based computation modules of Python. The allow fast array manipulation (functions in C). NumPy and SciPy provide access to fft, lapack, atlas or blas. That is svd, eigenvalues... ObsPy uses the numpy.ndarrays for storing the data (e.g. tr.data).
- http://matplotlib.sourceforge.net/gallery.html matplotlib is the 2-D plotting package for Python. The gallery is the market place which allows you to go shopping for all kind of figures. The source code for each figure is linked. Note matplotlib has even its own latex renderer.
- http://matplotlib.sourceforge.net/basemap/doc/html/index.html Package plotting 2D data on maps in Python. Similar to GMT.
- http://trac.osgeo.org/gdal/wiki/GdalOgrInPython Package which allows to directly read a GeoTiff which then can be plotted with the basemap toolkit.
- http://www.tramy.us/numpybook.pdf The official numpy reference.
- http://yalmagazine.org/homepage/docs/354 An article on Python and matplotlib in a German Linux Magazine (free).
- http://openbook.galileocomputing.de/python/ An German Python book (free).
- http://svn.geophysik.uni-muenchen.de/trac/mtspecpy Multitaper spectrum bindings for Python
Reading Seismograms
Seismograms of the formats SAC, MINISEED, GSE2 (CM6 compressed) and Q can be read into Python variables. The ObsPy Stream object resembles a data-only SEED (MiniSEED) file. It can contain several Traces, i.e. gap-less continuous sample chunks. Stream objects can be created via the read method.
The following example shows how a single GSE2-formatted seismogram is read into a Stream object. There is only one Trace in the seismogram:
>>> from obspy.core 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
Seismogram meta data are accessed via the stats keyword on each trace:
>>> print st[0].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', 'vang': -1.0, 'calper': 1.0}) >>> st[0].stats.station 'RJOB' >>> st[0].stats.gse2.datatype 'CM6'
The actual waveform data may be retrieved via the data keyword on each trace:
>>> st[0].data [-38 12 -4 ..., -14 -3 -9] >>> st[0].data[0:3] array([-38, 12, -4]) >>> len(st[0]) 36000
The Stream object offers a plotting method for fast previews (requires the obspy.imaging module):
>>> st.plot(color='k')
See WaveformPlotting for more information about plotting seismograms.
Filtering Seismograms
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 from obspy.core import read # Read the seismogram st = read("http://examples.obspy.org/RJOB_061005_072159.ehz.new") # There is only one trace in the Stream object, let's work on that trace... tr = st[0] # Filtering with a lowpass on a copy of the original Trace tr_filt = tr.copy() tr_filt.filter('lowpass', freq=1.0, corners=2, zerophase=True) # Now let's plot the raw and filtered data... t = np.arange(0, tr.stats.npts / tr.stats.sampling_rate, tr.stats.delta) plt.subplot(211) plt.plot(t, tr.data, 'k') plt.ylabel('Raw Data') plt.subplot(212) plt.plot(t, tr_filt.data, 'k') plt.ylabel('Lowpassed Data') plt.xlabel('Time [s]') plt.suptitle(tr.stats.starttime) plt.show()
Downsampling Seismograms
The following script shows how to downsample a seismogram. Currently, a simple integer decimation is supported. If not explicitely 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 applied 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 from obspy.core import read # Read the seismogram st = read("http://examples.obspy.org/RJOB_061005_072159.ehz.new") # There is only one trace in the Stream object, let's work on that trace... tr = st[0] # Downsample the 200 Hz data by a factor of 4 to 50 Hz # Note that this automatically includes a lowpass filtering with corner frequency 20 Hz # We work on a copy of the original data just to demonstrate the effects of downsampling tr_new = tr.copy() tr_new.downsample(decimation_factor=4, strict_length=False) # 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) # Now let's plot the raw and filtered data... t = np.arange(0, tr.stats.npts / tr.stats.sampling_rate, tr.stats.delta) t_new = np.arange(0, tr_new.stats.npts / tr_new.stats.sampling_rate, tr_new.stats.delta) plt.plot(t, tr.data, 'k', label='Raw', alpha=0.3) plt.plot(t, tr_filt.data, 'b', label='Lowpassed', alpha=0.7) plt.plot(t_new, tr_new.data, 'r', label='Lowpassed/Downsampled', alpha=0.7) plt.xlabel('Time [s]') plt.xlim(82, 83.5) plt.suptitle(tr.stats.starttime) plt.legend() plt.show()
Seismogram Envelopes
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 from obspy.core import read import obspy.signal st = read("http://examples.obspy.org/RJOB_061005_072159.ehz.new") data = st[0].data npts = st[0].stats.npts samprate = st[0].stats.sampling_rate # Filtering the Stream object st_filt = st.copy() st_filt.filter('bandpass', freqmin=1, freqmax=3, corners=2, zerophase=True) # Envelope of filtered data data_envelope = obspy.signal.filter.envelope(st_filt[0].data) # The plotting, plain matplotlib t = np.arange(0, npts/samprate, 1/samprate) plt.plot(t, st_filt[0].data, 'k') plt.plot(t, data_envelope, 'k:') plt.title(st[0].stats.starttime) plt.ylabel('Filtered Data w/ Envelope') plt.xlabel('Time [s]') plt.xlim(80, 90) plt.show()
Seismometer Correction/Simulation
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.
from obspy.core import read from obspy.signal import cornFreq2Paz 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 = cornFreq2Paz(1.0, damp=0.707) # 1Hz instrument paz_1hz['sensitivity'] = 1.0 st = read() # make a copy to keep our original data st_orig = st.copy() # Simulate instrument given poles, zeros and gain of # the original and desired instrument st.simulate(paz_remove=paz_sts2, paz_simulate=paz_1hz) # plot original and simulated data 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()
Plotting Spectrogram
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 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.astro.lsa.umich.edu/~msshin/science/code/matplotlib_cm/
http://www.scipy.org/Cookbook/Matplotlib/Show_colormaps
from obspy.core import read st = read("http://examples.obspy.org/RJOB_061005_072159.ehz.new") st.spectrogram(log=True)
Poles and Zeros, Frequency Response
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. Two things have to be taken into account for the phase (actually for the imaginary part of the response):
- the fft that is used is defined as exp(-i*phi), but this minus sign is missing for the visualization, so we have to add it again
- we want the phase to go from 0 to 2*pi, instead of the output from atan2 that goes from -pi to pi
import numpy as np import matplotlib.pyplot as plt from obspy.signal import pazToFreqResp paz = {} paz['poles'] = [-4.440+4.440j, -4.440-4.440j, -1.083+0.0j] paz['zeros'] = [0.0 +0.0j, 0.0 +0.0j, 0.0 +0.0j] paz['gain'] = 0.4 h,f = pazToFreqResp(paz['poles'], paz['zeros'], paz['gain'], 0.005, 16384, freq=True) plt.figure() plt.subplot(121) plt.loglog(f, abs(h)) plt.xlabel('Frequency [Hz]') plt.ylabel('Amplitude') plt.subplot(122) phase = np.unwrap(np.arctan2(-h.imag, h.real)) #take negative of imaginary part plt.semilogx(f, phase) plt.xlabel('Frequency [Hz]') plt.ylabel('Phase [radian]') plt.suptitle('Frequency Response of LE-3D/1s Seismometer') #title, centered above both subplots plt.subplots_adjust(wspace=0.3) #make more room inbetween subplots for the ylabel of right plot plt.show()
Reading Waveforms via ArcLink
The following lines show how to retrieve waveforms and poles and zeros from ArcLink. The poles and zeros are then used to correct for the instrument response and to simulate a 1Hz instrument with damping 0.707. Plots are provided.
import numpy as np import matplotlib.pyplot as plt from obspy.core import UTCDateTime from obspy.arclink import Client from obspy.signal import cornFreq2Paz, seisSim # Retrieve waveforms via ArcLink client = Client(host="webdc.eu", port=18001) t = UTCDateTime("2009-08-24 00:20:03") st = client.getWaveform('BW', 'RJOB', '', 'EHZ', t, t+30) paz = client.getPAZ('BW', 'RJOB', '', 'EHZ', t, t+30) paz = paz.values()[0] # 1Hz instrument one_hertz = cornFreq2Paz(1.0) # Correct for frequency response of the instrument res = seisSim(st[0].data.astype('float32'), st[0].stats.sampling_rate, paz, inst_sim=one_hertz) # Correct for overall sensitivity res = res / paz['sensitivity'] # Plot the seismograms sec = np.arange(len(res)) / st[0].stats.sampling_rate plt.subplot(211) plt.plot(sec,st[0].data, 'k') plt.title("%s %s" % (st[0].stats.station, t)) plt.ylabel('STS-2') plt.subplot(212) plt.plot(sec, res, 'k') plt.xlabel('Time [s]') plt.ylabel('1Hz CornerFrequency') plt.show()
Beachball plot
The following lines show how to create a graphical representation of a focal mechanism.
from obspy.imaging.beachball import Beachball mt = [0.91, -0.89, -0.02, 1.78, -1.55, 0.47] Beachball(mt, size=200, linewidth=2, facecolor='b')
Basemap plot with beachballs
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.
import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.basemap import Basemap from obspy.imaging.beachball import Beach import gzip # read in topo data (on a regular lat/lon grid) # (srtm data from: http://srtm.csi.cgiar.org/) srtm = np.loadtxt(gzip.open("srtm_1240-1300E_4740-4750N.asc.gz"), skiprows=8) # origin of data grid as stated in srtm data file header # create arrays with all lon/lat values from min to max and lats = np.linspace(47.8333, 47.6666, srtm.shape[0]) lons = np.linspace(12.6666, 13.0000, srtm.shape[1]) # create Basemap instance with Mercator projection # we want a slightly smaller region than covered by our srtm data m = Basemap(projection='merc', lon_0=13, lat_0=48, resolution="h", llcrnrlon=12.75, llcrnrlat=47.69, urcrnrlon=12.95, urcrnrlat=47.81) # create grids and compute map projection coordinates for lon/lat grid x, y = m(*np.meshgrid(lons,lats)) # Make contour plot cs = m.contour(x, y, srtm, 40, colors="k", lw=0.5, alpha=0.3) m.drawcountries(color="red", linewidth=1) # Draw a lon/lat grid (20 lines for an interval of one degree) m.drawparallels(np.linspace(47, 48, 21), labels=[1,1,0,0], fmt="%.2f", dashes=[2,2]) m.drawmeridians(np.linspace(12, 13, 21), labels=[0,0,1,1], fmt="%.2f", dashes=[2,2]) # Plot station positions and names into the map # again we have to compute the projection of our lon/lat values lats = [47.761659, 47.7405, 47.755100, 47.737167] lons = [12.864466, 12.8671, 12.849660, 12.795714] names = [" RMOA", " RNON", " RTSH", " RJOB"] x, y = m(lons, lats) m.scatter(x, y, 200, color="r", marker="v", edgecolor="k", zorder=3) for i in range(len(names)): plt.text(x[i], y[i], names[i], va="top", family="monospace", weight="bold") # Add beachballs for two events lats = [47.751602, 47.75577] lons = [12.866492, 12.893850] x, y = m(lons, lats) # Two focal mechanisms for the beachball routine, specified as [strike, dip, rake] focmecs = [[80, 50, 80], [85, 30, 90]] ax = plt.gca() for i in range(len(focmecs)): b = Beach(focmecs[i], xy=(x[i], y[i]), width=1000, linewidth=1) b.set_zorder(10) ax.add_collection(b) plt.show()
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 655 662 666 660 659 659 658 ....
Some notes:
- The Python package GDAL allows you to directly read a GeoTiff into numpy array (geo = gdal.Open("file.geotiff"); x = geo.ReadAsArray())
- GeoTiff elevation data is available e.g. from ASTER
- Shading/Illumination? can be added. See the basemap example plotmap_shaded.py for more info.
Anything to MiniSEED
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 meteorologic stations or anything else via the seedlink protocol.
import numpy as np from obspy.core import read, Trace, Stream, UTCDateTime 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 """ # Convert to numpy character array data = np.fromstring(weather, dtype='|S1') # Fill header attributes stats = {'network': 'BW', 'station': 'RJOB', 'location': '', 'channel': 'WLZ', 'npts': len(data), 'sampling_rate': 0.1, 'mseed' : {'dataquality' : 'D'}} stats['starttime'] = UTCDateTime() #the current time st = Stream([Trace(data=data, header=stats)]) st.write("weather.mseed", format='MSEED', encoding=0, reclen=256) #encoding 0==ASCII # Show that it worked, convert numpy character array back to string st1 = read("weather.mseed") print st1[0].data.tostring()
Checking Data availability for data files
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 automatically detects the format (MiniSEED, SAC, SACXY, GSE2, SH-ASC, SH-Q, SEISAN) 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 (NOT from inside python or ipython), use e.g. wildcards to match the files:
obspy-scan /bay_mobil/mobil/20090622/1081019/*_1.*
Saving Python objects as MATLAB MAT files
The following example shows how to read in a file (MSEED, SAC, SEISAN, GSE2, SH) 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 obspy.core import read from scipy.io import savemat st = read("test/BW.BGLD..EHE.D.2008.001") for i, tr in enumerate(st): mdict = dict([[j, str(k)] for j,k in tr.stats.iteritems()]) mdict['data'] = tr.data savemat("data-%d.mat" % i, mdict)
Interfacing R-cran from Python
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
Seismogram to ASCII
In the following, a small Python script is shown which converts each Trace of a seismogram file (supported formats: MiniSEED, SAC, Seisan, GSE2, SH_ASC, Q) to an ASCII file and multiplies the data by a calibration factor.
""" USAGE: seis2asc.py mseed_file out_file calib_factor """ from obspy.core import read import numpy as np import sys try: in_file = sys.argv[1] of_file = sys.argv[2] calib = float(sys.argv[3]) except: print __doc__ raise st = read(in_file) for i, tr in enumerate(st): f = open("%s_%d" % (of_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 * calib, fmt="%f") f.close()
Merging Seismograms
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 function.
#!/usr/bin/env python from obspy.core import read import numpy as np, matplotlib.pyplot as plt # Read in all files starting with dis. st = read("http://examples.obspy.org/dis.G.SCZ.__.BHE") st += read("http://examples.obspy.org/dis.G.SCZ.__.BHE.1") st += read("http://examples.obspy.org/dis.G.SCZ.__.BHE.2") # Go through the stream object, determine time range in julian seconds # and plot the data with a shared x axis ax = plt.subplot(4,1,1) #dummy for tying axis for i in range(3): plt.subplot(4,1,i+1, sharex=ax) t = np.linspace(st[i].stats.starttime.timestamp, st[i].stats.endtime.timestamp, st[i].stats.npts) plt.plot(t, st[i].data) # Merge the data together and show plot in a similar way st.merge(method=1) plt.subplot(4,1,4, sharex=ax) t = np.linspace(st[0].stats.starttime.timestamp, st[0].stats.endtime.timestamp, st[0].stats.npts) plt.plot(t, st[0].data, 'r') plt.show()
Beamforming --- FK Analysis
The following code shows how to do an FK Analysis with ObsPy. The data are from the blasting of the AGFA skyscraper in Munich (pickle.dumped stream object with paz attached to each Trace, can also be found here). We execute sonic 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.05s. 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 mlabhours, 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'.
import pickle, urllib from obspy.core import UTCDateTime from obspy.signal.array_analysis import sonic from obspy.signal import cornFreq2Paz # # Load data st = pickle.load(urllib.urlopen("http://examples.obspy.org/agfa.dump")) # # Instrument correction to 1Hz corner frequency paz1hz = cornFreq2Paz(1.0, damp=0.707) st.simulate(paz_remove='self', paz_simulate=paz1hz) # # Execute sonic 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 propertieds 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, verbose=True, timestamp='mlabhour', stime=UTCDateTime("20080217110515"), etime=UTCDateTime("20080217110545") ) out = sonic(st, **kwargs)
The output can be plotted by the following code lines. We read the output produced by sonic ('out'), which is: timestamp, rel. power, abs. power, backazimut and slowness. The colorbar corresponds to rel. power.
import matplotlib.pyplot as plt labels = 'rel.power abs.power baz slow'.split() 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') ax.set_ylabel(lab) ax.xaxis_date() fig.autofmt_xdate() fig.subplots_adjust(top=0.95, right=0.95, bottom=0.2, hspace=0)
or by a polar plot. The plot sums the relative power in gridded bins, each defined by backazimuth and slowness of the analysed signal part. The backazimuth is counted clockwise from north, the slowness limits can be set by hand.
import numpy as np import matplotlib.cm as cm import matplotlib.pyplot as plt from matplotlib.colorbar import ColorbarBase from matplotlib.colors import Normalize cmap = cm.hot_r pi = np.pi # # 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 # choose number of fractions in plot (desirably 360°/N is an integer!) N = 30 abins = np.arange(N+1)*360./N sbins = np.linspace(0, 3, N+1) # sum rel power in bins given by abins and sbins hist, baz_edges, sl_edges = np.histogram2d(baz, slow, bins=[abins, sbins], weights=rel_power) # transfrom to gradient baz_edges = baz_edges/180*np.pi # add polar and colorbar axes fig = plt.figure(figsize=(8,8)) cax = fig.add_axes([0.85, 0.2, 0.05, 0.5]) ax = fig.add_axes([0.10, 0.1, 0.70, 0.7], polar=True) dh = abs(sl_edges[1] - sl_edges[0]) dw = abs(baz_edges[1] - baz_edges[0]) # circle through backazimut for i, row in enumerate(hist): bars = ax.bar(left=(pi/2 - (i+1)*dw)*np.ones(N), height=dh*np.ones(N), width=dw, bottom=dh*np.arange(N), color=cmap(row/hist.max())) ax.set_xticks([pi/2, 0, 3./2*pi, pi]) ax.set_xticklabels(['N', 'E' , 'S', 'W']) # set slowness limits ax.set_ylim(0, 3) ColorbarBase(cax, cmap=cmap, norm=Normalize(vmin=hist.min(), vmax=hist.max())) plt.show()
Array Response Function
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 matplotlib.pyplot as plt import numpy as np from obspy.signal.array_analysis import array_transff_wavenumber # generate array coordinates coords = np.array([[ 10., 60., 0.], [ 200., 50., 0.], [-120., 170., 0.], [-100., -150., 0.], [ 30., -220., 0.]]) # coordinates in km coords /= 1000. # set limits for wavenumber differences to analyze klim = 40. kxmin = -klim kxmax = klim kymin = -klim kymax = klim kstep = klim/100. # compute transfer function as a function of wavenumber difference transff = array_transff_wavenumber(coords, klim, kstep, coordsys='xy') # 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) plt.colorbar() plt.clim(vmin=0., vmax=1.) plt.xlim(kxmin, kxmax) plt.ylim(kymin, kymax) plt.show()
Continuous Wavelet Transform
Small script doing the continuous wavelet transform using the mlpy package for infrasound data recorded at Yasur in 2008. Further details on wavelets can be found e.g http://en.wikipedia.org/wiki/Morlet_wavelet (in the wikipedia article the omega0 factor 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]
#!/usr/bin/env python import matplotlib.pyplot as plt from obspy.core import read import numpy as np import mlpy tr = read("http://examples.obspy.org/a02i.2008.240.mseed")[0] omega0 = 8 spec, scale = mlpy.cwt(tr.data, dt=tr.stats.delta, dj=0.05, wf='morlet', p=omega0, extmethod='none', extlength='powerof2') # approximate scales through frequencies freq = (omega0 + np.sqrt(2.0 + omega0**2))/(4*np.pi * scale[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]) 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') img = ax2.imshow(np.abs(spec), extent=[t[0], t[-1], freq[-1], freq[0]], aspect='auto') fig.colorbar(img, cax=ax3) plt.show()
Coordinate Conversions
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, y [4443947.179, 4446185.667] [5506428.401, 5336354.055]
Hierarchical Clustering
An implementation of hierarchical clustering is provided in the package hcluster. Among other things, it allows to build clusters from similarity matrices (e.g. computed using the cross-correlation routines in obspy.signal) 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 in southern Germany and can be fetched from ObsPy's examples page::
import pickle, urllib import matplotlib.pyplot as plt import hcluster dissimilarity = pickle.load(urllib.urlopen("http://examples.obspy.org/dissimilarities.pkl")) plt.subplot(121) plt.imshow(1 - dissimilarity, interpolation="nearest") dissimilarity = hcluster.squareform(dissimilarity) threshold = 0.3 linkage = hcluster.linkage(dissimilarity, method="single") clusters = hcluster.fcluster(linkage, 0.3, criterion="distance") plt.xlabel("Event number") plt.ylabel("Event number") plt.subplot(122) hcluster.dendrogram(linkage, color_threshold=0.3) plt.xlabel("Event number") plt.ylabel("Dissimilarity") plt.show()
Building a new dataless-SEED file
A small script how to clone an existing dataless-SEED file (dataless.seed.BW_RNON) and use this as a template to build up a dataless-SEED file for a new station. [Note that the FIR coefficients are not set, in case you require correct FIR coefficients, clone from an existing dataless file with the same seismometer type]
from urllib import urlopen from obspy.core import UTCDateTime from obspy.xseed import Parser url = "http://examples.obspy.org/dataless.seed.BW_RNON" p = Parser(urlopen(url).read()) blk = p.blockettes 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" 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 blk[52][i].elevation = blk[50][0].elevation blk[52][i].start_date = blk[50][0].start_effective_date blk[52][i].end_date = blk[50][0].end_effective_date blk[53][i].number_of_complex_poles = 3 blk[53][i].real_pole = [-4.444, -4.444, -1.083] blk[53][i].imaginary_pole = [+4.444, -4.444, +0.0] blk[53][i].real_pole_error = [0, 0, 0] blk[53][i].imaginary_pole_error = [0, 0, 0] blk[53][i].number_of_complex_zeros = 3 blk[53][i].real_zero = [0.0, 0.0, 0.0] blk[53][i].imaginary_zero = [0.0, 0.0, 0.0] blk[53][i].real_zero_error = [0, 0, 0] blk[53][i].imaginary_zero_error = [0, 0, 0] blk[53][i].A0_normalization_factor = 1.0 blk[53][i].normalization_frequency = 3.0 # stage sequence number 1, seismometer gain blk[58][i*mult].sensitivity_gain = 400.0 # stage sequence number 2, digitizer gain blk[58][i*mult+1].sensitivity_gain = 1677850.0 # stage sequence number 0, overall sensitivity blk[58][(i+1)*mult-1].sensitivity_gain = 671140000.0 p.writeSEED("dataless.seed.BW_RMOA")
Visualizing Probabilistic Power Spectral Densities
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 McNamara and Buland 2004.
from obspy.core import read from obspy.xseed import Parser from obspy.signal import PPSD st = read("http://examples.obspy.org/BW.KW1..EHZ.D.2011.037") # select a trace with the desired station/channel combination tr = st.select(id="BW.KW1..EHZ")[0] # acquire poles and zeros information, e.g. from a dataless SEED file parser = Parser("http://examples.obspy.org/dataless.seed.BW_KW1") paz = parser.getPAZ(tr.id) # initialize a new PPSD instance. the ppsd object will then make sure that # only appropriate data go into the probabilistic psd statistics. ppsd = PPSD(tr.stats, paz) # now we can add data (either trace or stream objects) to the ppsd estimate # this step may take a while.. ppsd.add(st) # we can check what time ranges are represented in the ppsd estimate.. # ppsd.times contains a sorted list of starttimes of the one hour long slices # that the psds are computed from print ppsd.times # the count of 1 hour long segments in the statistics can be checked via.. print "number of psd segments: ", len(ppsd.times) # adding the same stream again will do nothing, the ppsd object makes sure # that no overlapping data segments go into the ppsd estimate ppsd.add(st) print "number of psd segments: ", len(ppsd.times) # additional information from other files/sources # can be added step by step (like client.getWaveform(...)) st = read("http://examples.obspy.org/BW.KW1..EHZ.D.2011.038") ppsd.add(st) # the graphical representation of the ppsd can be saved to a file like.. ppsd.plot("/tmp/ppsd.png") ppsd.plot("/tmp/ppsd.pdf") # ... or displayed in a matplotlib window ppsd.plot()
Below the actual PPSD (for a detailed discussion see McNamara and Buland 2004) 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 volume is safer than specifying static poles and zeros information (see API)
Attachments
-
correct.png
(56.7 KB) -
added by beyreuth 3 years ago.
-
RJOB_061005_072159.ehz.new
(58.6 KB) -
added by beyreuth 3 years ago.
-
envelope_example.png
(51.7 KB) -
added by megies 3 years ago.
-
beachball.png
(18.8 KB) -
added by barsch 3 years ago.
-
freqresp.png
(40.8 KB) -
added by megies 3 years ago.
tutorial plot of frequency response for LE-3D/1s
-
arclink.png
(57.3 KB) -
added by beyreuth 3 years ago.
-
obspyscan.png
(25.4 KB) -
added by beyreuth 2 years ago.
-
basemap_beachball_demo.jpg
(101.9 KB) -
added by megies 2 years ago.
-
srtm_1240-1300E_4740-4750N.asc.gz
(110.7 KB) -
added by beyreuth 2 years ago.
Adding srtm file
-
merge.png
(92.0 KB) -
added by beyreuth 2 years ago.
-
read.png
(22.6 KB) -
added by megies 23 months ago.
-
filter.png
(61.2 KB) -
added by megies 23 months ago.
-
downsample.png
(76.3 KB) -
added by megies 23 months ago.
-
spectrogram.png
(43.8 KB) -
added by megies 22 months ago.
-
simulate_1.png
(77.8 KB) -
added by megies 22 months ago.
-
simulate_2.png
(74.6 KB) -
added by megies 22 months ago.
-
simulate_3.png
(64.6 KB) -
added by megies 22 months ago.
-
sonic.png
(121.1 KB) -
added by kremers 21 months ago.
-
polarsonic2.png
(115.9 KB) -
added by kremers 21 months ago.
-
cwt.png
(94.7 KB) -
added by beyreuth 20 months ago.
-
polarsonic.jpg
(125.0 KB) -
added by beyreuth 20 months ago.
better polar plot as requested by Tobi
-
hcluster.png
(33.3 KB) -
added by megies 20 months ago.
-
freq_response_le3d_unwrap.png
(36.7 KB) -
added by beyreuth 17 months ago.
-
arraytranff.png
(191.0 KB) -
added by beyreuth 15 months ago.
Correcting arraytransff.png, thanks to Martin van Driel
-
ppsd.png
(63.8 KB) -
added by megies 13 months ago.
PPSD example




















