diff --git a/02-FFT_and_Basic_Filtering/3-instrument_response.ipynb b/02-FFT_and_Basic_Filtering/3-instrument_response.ipynb new file mode 100644 index 0000000..128a646 --- /dev/null +++ b/02-FFT_and_Basic_Filtering/3-instrument_response.ipynb @@ -0,0 +1,557 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "
\n", + "
\n", + "
Applied Seismology
\n", + "
Lab: Instrument Response
\n", + "
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Seismo-Live: http://seismo-live.org\n", + "\n", + "##### Authors:\n", + "* Carl Tape ([@carltape](https://github.com/carltape))\n", + "* Lion Krischer ([@krischer](https://github.com/krischer))\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "based on *GEOS 626: Applied Seismology from Carl Tape*\n", + "\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This cell does two things: \n", + "# (1) make plots appear in the notebook and (2) creates prettier plots.\n", + "# Make sure to execute it at least once!\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "plt.style.use('ggplot')\n", + "plt.rcParams['figure.figsize'] = 10, 5" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### L1 Instructions\n", + "\n", + "This is a Python and ObsPy version of the above mentioned lab. We will perform the same calculations and create similar plots but we will be using different services, data formats, and tools.\n", + "\n", + "It aims to teach students how to calculate, visualize and use instruments responses." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### L2 Computing and plotting instrument response, $I(ω)$ " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Acquiring Data\n", + "\n", + "The first step when working with instrument data is to acquire it. Nowadays this is mostly done using the *station* web service defined by the [FDSN](http://www.fdsn.org/) (International Federation of Digital Seismograph Networks). Once you know how to use this you can use it to download data from data centers across the globe including IRIS, ORFEUS, ETH, RESIF, GEONET, and many more. Notable exceptions are most Asian countries and countries that don't freely share their data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With ObsPy this works by first importing the FDSN `Client` class and then initializing it with the data center you want to download from. For this lab we will use data from the Geoscope project, which can be downloaded from IRIS (its \"home\" data center is the IPGP in Paris which you can also use - just replace `\"IRIS\"` with `\"IPGP\"` in the following code box - but most of you will be more familiar with IRIS).\n", + "\n", + "See the [documentation of obspy.clients.fdsn](http://docs.obspy.org/packages/obspy.clients.fdsn.html) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from obspy.clients.fdsn import Client\n", + "# ObsPy knows which website to connect to for many data center.\n", + "# For others you can also pass the full URL.\n", + "c = Client(\"IRIS\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Station information is time dependent due to for example changed sensors, new calibrations, or other changes. It is thus important to specify the time frame for which to request response information.\n", + "\n", + "ObsPy handles absolute times with the `UTCDateTime` class. See the [documentation](http://docs.obspy.org/packages/autogen/obspy.core.utcdatetime.UTCDateTime.html) and the [tutorial](http://docs.obspy.org/tutorial/code_snippets/utc_date_time.html) for more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import obspy\n", + "# Create two times which we will later use to download time series data.\n", + "starttime = obspy.UTCDateTime(2004, 12, 26, 0, 58, 50)\n", + "# Create a new time object by adding 5 days to the previous ones. Time\n", + "# differences are in seconds.\n", + "endtime = starttime + 86400 * 5" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will use the `CAN` station which is located in Canberra, Australia, and featured in [*Park et al.* (2005, Figure 1)](http://dx.doi.org/10.1126/science.1112305). Please note that in many cases the station code is not unique and in practice the network code is also required. See the ObsPy tutorials for more details about these concepts. The `get_station()` method of the FDSN `Client` object returns station/instrument information. Once again see its [documentation](http://docs.obspy.org/packages/autogen/obspy.clients.fdsn.client.Client.get_stations.html) for more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This will get all stations that satisfy all constraints simultaneosly.\n", + "inv = c.get_stations(network=\"G\", station=\"CAN\", channel=\"LHZ\", \n", + " level=\"response\", \n", + " starttime=starttime, endtime=endtime)\n", + "print(inv)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `station` services return `StationXML` files which are the most modern and future proof data format for station and instrument information. **If possible at all: Always try to use StationXML files!**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Plotting the Instrument Response\n", + "\n", + "We can now plot the instrument response with the `plot_response()` method. The `min_freq` argument determines the lowest plotted frequency - the maximum plotted frequency is always the Nyquist frequency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inv.plot_response(min_freq=0.001);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These are [Bode plots](https://en.wikipedia.org/wiki/Bode_plot) and show the amplitude $A(\\omega)$ and the phase $\\phi(\\omega)$ response where\n", + "\n", + "$$\n", + "I(\\omega) = A(\\omega) e^{i \\phi(\\omega)}\n", + "$$\n", + "\n", + "Note that the formulas use $\\omega$ but the plots show the frequency $f$ with $\\omega = 2 \\pi f$. The first variant usually simplifies notation whereas the latter one facilitates physical interpretation.\n", + "\n", + "This plot does not just show the instrument response - it shows the response of the whole recording chain from observed ground motion to actually stored data. This includes the analoge response of the actual seismometer, the analog-to-digitial converter (DAC), and possible digital filter stages." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The sampling rate of this channel is 1 sample per second ($\\Delta T$ = 1s), so the **Nyquist frequency**, which is defined as\n", + "\n", + "$$\n", + "f_{Nyq} = 1 / (2 \\Delta t),\n", + "$$\n", + "\n", + "is $f_{Nyq}$ = 0.5 Hz. The above plot does not show frequencies higher than the Nyquist frequency (the vertical dashed line) as these frequencies do not exist in the data and thus have no physical meaning." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Have a look at the details of the response information of this channel:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inv[0][0][0].response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we see that it consists of multiple stages that convert from one unit to another. The final instrument response is the multipication (in the frequency domain) of all of these.\n", + "\n", + "**QUESTION:** What are the input and output units of the whole instrument? What does that mean physically?\n", + "\n", + "**QUESTION:** How does one conceptually remove the instrument response from recorded data?\n", + "\n", + "##### Incomplete Responses\n", + "\n", + "You can also only plot the response of a range of stages. The plotted sensitivity in Obspy 1.0.1 is the overall sensitivity of all stages and not only the chosen ones - thus the plot looks a bit strange. This will be rectified [soon](https://github.com/obspy/obspy/issues/1368)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inv[0][0][0].plot(0.001, start_stage=1, end_stage=1);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the major differences (except a constant factor) are close to the Nyquist frequency - these are due to the here not plotted decimation and FIR filter stages. In many cases it is sufficient to only use Poles & Zeros information but there are stations where this is not true. As it is tedious to manually check everytime: **There is no reason to not use the full chain instrument response if it is available!**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Output Units" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modern seismometers usually record the velocity of ground motion, strong motions sensor record acceleration.\n", + "\n", + "**QUESTION:** Why is that?\n", + "\n", + "The instrument response describes how incoming data (in whatever the instrument measures) it transformed into what is finally stored in a file. Restoring original ground motion requires the inversion of that process. The deconvolution of the instrument response as written for example in a StationXML file will convert data back to its original units.\n", + "\n", + "If your analysis requires other units it is best to perform the integration or differentiation during the instrument deconvolution. That deconvolution is usually performed in the frequency domain - once there integration/differentiation is almost free and the most accurate it gets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following plots show the instrument response $I_d(\\omega)$ from displacement, velocity $I_v(\\omega)$, and acceleration $I_a(\\omega)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Displacement\")\n", + "inv.plot_response(0.001, output=\"DISP\")\n", + "print(\"Velocity\")\n", + "inv.plot_response(0.001, output=\"VEL\")\n", + "print(\"Acceleration\")\n", + "inv.plot_response(0.001, output=\"ACC\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "Basic properties of the Fourier transform result in \n", + "\n", + "$$\n", + "\\begin{align}\n", + "X_v(\\omega) &= (i \\omega) X_d(\\omega) \\\\\n", + "X_a(\\omega) &= (i \\omega) X_v(\\omega)\n", + "\\end{align}\n", + "$$\n", + "\n", + "where $X_d$, $X_v$, and $X_a$ are the Fourier transforms of displacement $x_d(t)$, velocity $x_v(t)$, and acceleration $x_a(t)$.\n", + "\n", + "We also have the relationship between ground motion, instrument response, and the output form the seismometer:\n", + "\n", + "$$\n", + "\\begin{align}\n", + "x_a(t) * i_a(t) &= c(t) \\\\\n", + "X_a(\\omega) I_a (\\omega) &= C(\\omega) \n", + "\\end{align}\n", + "$$\n", + "\n", + "Here we have assumed that the instrument response is described with respect to acceleration. But we could alternatively consider the instrument response with respect to velocity or displacement; **the key point is that what comes out of the seismometer,** $C(\\omega)$**, is fixed.** But we can descibe the input ground motion as displacement, velocity, or acceleration. Showing all three together and omitting explicit $\\omega$ dependence, we have\n", + "\n", + "$$\n", + "\\begin{align}\n", + "C &= X_a I_a = X_v I_v = X_d I_d\\\\\n", + " &= (i\\omega) X_v I_a = (i \\omega) X_d I_V = X_d I_d \\\\\n", + " &= (i\\omega)^2 X_d I_a = (i \\omega) X_d I_V = X_d I_d \\\\\n", + "I_v &= I_d \\ (i\\omega)\\\\\n", + "I_a&= I_v \\ (i\\omega)\\\\\n", + "\\end{align}\n", + "$$\n", + "\n", + "It turns out that the effect of differentiation in the time domain leads to an *increase by a factor of one* in the slope of the amplitude spectrum, $|H(\\omega)|$, in log-log space, for example, by changing from $X_d(\\omega)$ to $X_v(\\omega)$. But when we are looking at the *instrument response*, the slope will *decrease by a factor of one* when changing from, say, $I_d(\\omega)$ to $I_v(\\omega)$.\n", + "\n", + "We see this in the above figure. Consider the flat segment in the velocity correction spectrum: the displacement shows a corresponding slope increase whereas the acceleration show a slope decrease.\n", + "\n", + "**QUESTION:** What is meant by a \"broadband\" seismometer?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### L3 Deconvolve instrument response for a seismogram" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will download some data and remove its instrument response.\n", + "\n", + "The first step is to get some data - we will once again use the FDSN web services but this time also to download waveform data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "otime = obspy.UTCDateTime(2004, 12, 26, 0, 58, 50)\n", + "duration = 5 * 86400\n", + "starttime = otime\n", + "endtime = starttime + duration\n", + "\n", + "st = c.get_waveforms(network=\"G\", station=\"CAN\", location=\"\", channel=\"LH*\",\n", + " starttime=starttime, endtime=endtime)\n", + "\n", + "# Do it once again to also get the response information of the vertical channels.\n", + "inv = c.get_stations(network=\"G\", station=\"CAN\", location=\"\", channel=\"LH*\",\n", + " starttime=starttime, endtime=endtime, level=\"response\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now show what the main arrival looks like at bandpass 50-500s with and without the instrument deconvolution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot the first hour - slice() will leave the original \n", + "# data stream intact and just returns a new view on the\n", + "# data.\n", + "st.slice(endtime=otime + 3600).copy().taper(0.05).filter(\n", + " \"bandpass\", freqmin=1.0 / 500.0, freqmax=1.0 / 50.0).plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Now plot the same but with the instrument removed.\n", + "# Note the copy() here as remove_response() would otherwise\n", + "# remove the contents of the original data stream.\n", + "st.slice(endtime=otime + 3600).copy().remove_response(\n", + " inventory=inv, output=\"VEL\").taper(0.05).filter(\n", + " \"bandpass\", freqmin=1.0 / 500.0, freqmax=1.0 / 50.0).plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What are the differences? What is their cause?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now plot the amplitude spectrum over 0.2 - 1.0 mHz to show the gravest normal mode peaks. We will only use the vertical component to simplify things.\n", + "\n", + "We will first do it without instrument correction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "tr = st.select(component=\"Z\")[0].copy().taper(0.05)\n", + "\n", + "# rfft() is a varient of the FFT for real valued input.\n", + "# The negative frequencies of a DFT for purely real valued\n", + "# input are just the complex conjugates of the corresponding\n", + "# positive frequencies and thus redundant.\n", + "D = np.fft.rfft(tr.data)\n", + "# Return the corresponding frequencies.\n", + "freqs = np.fft.rfftfreq(tr.stats.npts, d=tr.stats.delta)\n", + "\n", + "plt.plot(freqs, np.abs(D))\n", + "plt.xlim(0.2E-3, 1.0E-3)\n", + "plt.ylim(0, 1E7)\n", + "plt.xlabel(\"Frequency [mHz]\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instrument removal is, in practice, a tricky and unstable operation. Have a look at the [documentation of the remove_response() method](http://docs.obspy.org/packages/autogen/obspy.core.trace.Trace.remove_response.html) and try to understand the meaning of the various parameters and when you would want to use each." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Remember the copy()!\n", + "tr = st.select(component=\"Z\").copy().remove_response(inventory=inv, \n", + " output=\"DISP\")[0]\n", + "\n", + "D = np.fft.rfft(tr.data)\n", + "freqs = np.fft.rfftfreq(tr.stats.npts, d=tr.stats.delta)\n", + "\n", + "plt.plot(freqs, np.abs(D))\n", + "plt.xlim(0.2E-3, 1.0E-3)\n", + "plt.ylim(0, 5.0)\n", + "plt.xlabel(\"Frequency [mHz]\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Are there any difference to the previous versions? If yes - what are they? Keep in mind that the previous plot is based on velocity data, whereas this one has been corrected to displacement." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are interested in the spectrum of the data so we don't really need to use instrument corrected data to calculate the spectrum - we can just calculate the spectrum and divide by the response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tr = st.select(component=\"Z\")[0].copy().taper(0.05)\n", + "\n", + "D = np.fft.rfft(tr.data)\n", + "freqs = np.fft.rfftfreq(tr.stats.npts, d=tr.stats.delta)\n", + "# Advanced ObsPy-foo to directly get the complex response.\n", + "freq_response, _ = \\\n", + " inv.select(channel=\"LHZ\")[0][0][0].response.get_evalresp_response(\n", + " tr.stats.delta, tr.stats.npts, output=\"DISP\")\n", + "\n", + "plt.plot(freqs, np.abs(D)/ np.abs(freq_response))\n", + "plt.xlim(0.2E-3, 1.0E-3)\n", + "plt.ylim(0, 35.0)\n", + "plt.xlabel(\"Frequency [mHz]\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that this is quite different then the previous spectrum. **Why?** Keep in mind that we are operating far from the passband of the instrument. The following two plots as well as the\n", + "[documentation of the remove_response() method](http://docs.obspy.org/packages/autogen/obspy.core.trace.Trace.remove_response.html) should help you answer that question. Which alternative yields the more correct answer?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inv.select(channel=\"LHZ\").plot_response(min_freq=0.0001);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tr = st.select(component=\"Z\").copy().remove_response(\n", + " inventory=inv, output=\"DISP\", plot=True)[0]" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "b40d316b6eb93f8d3cd4b30bf241a4edb2b8ca4ea8a7206ab7ccdcc909e9c051" + }, + "jupytext": { + "encoding": "# -*- coding: utf-8 -*-" + }, + "kernelspec": { + "display_name": "Python 3.6.13 ('obspy')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README.md b/README.md index d8621e7..7be9dfd 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ The content of this repository is licensed under Creative Commons Attribution 4. ### 01 - Python Introduction -This has been forked from [Seismo Live](http://seismo-live.org). The source code is available at https://github.com/krischer/seismo_live (licensed under a ***CC BY-NC-SA 4.0 License***. © 2015-2019 Lion Krischer). +This has been forked from [Seismo Live](http://seismo-live.org). The source code is available at https://github.com/krischer/seismo_live (licensed under a ***CC BY-NC-SA 4.0 License***. © 2015-2022 Lion Krischer). ### 02 - Fourier-Transform and Basic Filtering -This has been forked from [Seismo Live](http://seismo-live.org). The source code is available at https://github.com/krischer/seismo_live (licensed under a ***CC BY-NC-SA 4.0 License***. © 2015-2019 Lion Krischer). +This has been forked from [Seismo Live](http://seismo-live.org). The source code is available at https://github.com/krischer/seismo_live (licensed under a ***CC BY-NC-SA 4.0 License***. © 2015-2022 Lion Krischer).