Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add harmonic decomposition to rf #48

Merged
merged 17 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
dev:
* add calculation of harmonic decomposition and corresponding plot (see #48)
* update multitaper deconvolution to use the multitaper package instead of outdated mtspec (see #49)
* add option to only plot stacked receiver function (see #48)
1.0.3:
* fix: use ax.sharey instead of join method which is unsupported in recent mpl versions
1.0.2:
Expand Down
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2013-2019 Tom Eulenfeld
Copyright (c) 2013-2024 Tom Eulenfeld, Hannah Mark

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand All @@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
1. Calculate receiver functions - minimal example ([notebook][nb1])
2. Calculate receiver functions and stack them by common conversion points to create a profile ([notebook][nb2])
3. Compare different deconvolution methods ([notebook][nb3])
4. Harmonic deconvolution with synthetics - minimal example ([notebook][nb4])

[nb1]: https://nbviewer.jupyter.org/github/trichter/notebooks/blob/master/receiver_function_minimal_example.ipynb
[nb2]: https://nbviewer.jupyter.org/github/trichter/notebooks/blob/master/receiver_function_profile_chile.ipynb
[nb3]: https://nbviewer.jupyter.org/github/hfmark/notebooks/blob/main/rf_comparison.ipynb
[nb4]: https://nbviewer.jupyter.org/github/hfmark/notebooks/blob/main/rf_harmonics.ipynb

##### Get help and discuss: [ObsPy Related Projects category](https://discourse.obspy.org/c/obspy-related-projects/rf/14) in the ObsPy forum

##### Contribute:

All contributions are welcome ... e.g. report bugs, discuss or add new features.
For example, the package could profit from more advanced deconvolution techniques.
New deconvolution functions can be tested by just passing them to deconvolve method (see docs).

##### Citation:

Expand Down
5 changes: 5 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ API Documentation

.. automodule:: rf.simple_model

:mod:`!harmonics` Module
---------------------------

.. automodule:: rf.harmonics

:mod:`!util` Module
-------------------------

Expand Down
7 changes: 4 additions & 3 deletions rf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@
for a more detailed description.
RFStream provides the possibility to perform moveout correction,
piercing point calculation and profile stacking.
There are also functions included for calculating back-azimuthal harmonics
from an RFStream.

Tutorials
---------
Expand All @@ -203,6 +205,7 @@
to create a profile (notebook2_)
3. Calculate and compare receiver functions calculated with different
deconvolution methods (notebook3_)
4. Harmonic deconvolution with synthetics - minimal example (notebook4_)

Command line tool for batch processing
--------------------------------------
Expand Down Expand Up @@ -264,6 +267,7 @@
.. _notebook1: http://nbviewer.jupyter.org/github/trichter/notebooks/blob/master/receiver_function_minimal_example.ipynb
.. _notebook2: http://nbviewer.jupyter.org/github/trichter/notebooks/blob/master/receiver_function_profile_chile.ipynb
.. _notebook3: https://nbviewer.jupyter.org/github/hfmark/notebooks/blob/main/rf_comparison.ipynb
.. _notebook4: https://nbviewer.jupyter.org/github/hfmark/notebooks/blob/main/rf_harmonics.ipynb

.. _GitHub: https://github.com/trichter/rf/
"""
Expand All @@ -273,6 +277,3 @@
from rf.profile import get_profile_boxes
from rf.rfstream import read_rf, RFStream, rfstats
from rf.util import iter_event_data, IterMultipleComponents

if 'dev' not in __version__: # get image for correct version from travis-ci
__doc__ = __doc__.replace('branch=master', 'branch=v%s' % __version__)
185 changes: 185 additions & 0 deletions rf/harmonics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Copyright 2024 Hannah Mark, MIT license
"""
Harmonic decomposition
"""
import numpy as np
import warnings
from rf.util import _add_processing_info


@_add_processing_info
def harmonics(stream, components='R', azim=0, method='time', **kwargs):
"""
Perform harmonic decomposition of PRFs.

This implements the method described in Bianchi et al 2010,
doi:10.1029/2009JB007061 (equations in supplemental material) and in
Park and Levin 2016, doi:10.1093/gji/ggw323 (equations 44/45 and 47).

.. The equations in the two papers are equivalent, just written
out slightly differently. Park and Levin have some sign errors.

The harmonic components are returned in an array with rows for
constant, cos, sin, cos2, and sin2 terms.
The stats dictionaries of the traces in the input stream must have a
'back_azimuth' entry and an 'event_time' entry.

.. 'event_time' is used for trace sorting, under the assumption that
it's a unique key and can be used to make sure that different
components (e.g. R and T) are indexed in a common order.

:param stream: RFStream including components to be decomposed
:param components: names of components to use in decomposition;
can be R, RT, Q, QT, or T (for PRFs)
:param azim: azimuth along which to decompose the RFs (default: 0)
:param method: domain for harmonic decomposition. Options are
'time' -> time domain (Bianchi et al. 2010)
'freq' -> frequency domain (Park and Levin 2016)
:param \*\*kwargs: other kwargs are passed to underlying functions for
building jacobians
:return: harmonic components in a Stream

.. note::
The number of traces returned depends on the number of components.
Two-component decomposition returns 10 harmonics: 5 modeled, 5
unmodeled. One-component decomposition returns only 5 modeled harmonics
for that single component.
"""
# various checks for components and inputs
if components not in ['RT', 'R', 'Q', 'QT', 'T']: # TODO L/Z for SRFs?
raise NotImplementedError('Component choice %s not supported' % components)
for c in components:
nrf = len(stream.select(channel='*%s' % c))
if nrf == 0:
raise ValueError('Component %s not in stream' % c)
if nrf < 4: # we probably shouldn't try to fit sinusoids with very few points
warnings.warn('Not enough %s RFs for robust azimuthal fit' % c)
if method not in ['time', 'freq']:
raise NotImplementedError('Supported methods are time and freq')

# short names to save some typing
R = components[0]
T = False
if len(components) == 2:
T = True # and the second is T

# get back azimuths (in degrees) with evt time as unique key
baz = np.array([tr.stats.back_azimuth for tr in
stream.select(channel='*%s' % R).sort(['event_time'])])
# get the RF trace data
rfR = np.array([tr.data for tr in stream.select(channel='*%s' % R).sort(['event_time'])])
if T:
rfT = np.array([tr.data for tr in stream.select(channel='*T').sort(['event_time'])])

# transform if working in frequency domain
if method == 'freq':
rfR = np.array([np.fft.fft(rfR[i]) for i in range(len(rfR))])
if T:
rfT = np.array([np.fft.fft(rfT[i]) for i in range(len(rfT))])

# use methods to get the jacobian
if T:
jac = decomp_two(baz, azim=azim, **kwargs)
else:
jac = decomp_one(baz, azim=azim, **kwargs)

# set up array for outputs
npts = stream[0].stats.npts
nharm = int(len(components)*5)
if method == 'time':
hd = np.zeros((nharm, npts))
if method == 'freq':
hd = np.zeros((nharm, npts), dtype=complex)

# loop time points and fit at each one
for i in range(npts):
if not T:
rfv = rfR[:, i] # make data vector
if T:
rfv = np.hstack((rfR[:, i], rfT[:, i]))
hd[:, i], _, _, _ = np.linalg.lstsq(jac, rfv, rcond=None)

if method == 'freq': # transform back if needed
for i in range(nharm):
hd[i] = np.fft.ifft(hd[i])
hd = np.real(hd)

# make the stream to return, with enough header info to get times later on
out = stream[:1].copy() # start a new stream with one trace
for k in ['processing', 'event_id', 'event_depth', 'event_latitude', 'event_longitude',
'event_magnitude', 'inclination', 'slowness', 'event_time', 'back_azimuth']:
try: # try to clean out some stats that no longer apply
_ = out[0].stats.pop(k)
except KeyError:
pass
out[0].stats['type'] = 'harmonic'
out[0].data = hd[0] # place harmonics in traces
for i in range(1, nharm):
new_trace = out[0].copy()
new_trace.data = hd[i]
out.append(new_trace)
terms = ['constant', 'cos', 'sin', 'cos2', 'sin2']
for i in range(5): # add some metadata, replace channel and loc for sorting later
out[i].stats['channel'] = str(i)
out[i].stats['term'] = terms[i]
out[i].stats['location'] = 'mod'
out[i].stats['components'] = components
if T:
for i in range(5, 10):
out[i].stats['channel'] = str(i-5)
out[i].stats['term'] = terms[i-5]
out[i].stats['location'] = 'unmod'
out[i].stats['components'] = components
return out


def decomp_two(baz, azim=0, scalars=(3, 0.3)):
""" Build jacobian for harmonic decomposition with two components of RFs.

:param baz: array of back azimuths for RFs in degrees
:param azim: azimuth along which to decompose the RFs.
This can be used with some kind of optimization minimize components
:param scalars: scalar multipliers for R/Q and T constant terms.
Park and Levin (2016) recommend 3 and 0.3 to bias toward radial component
conversions, especially for datasets with patchy back-azimuthal coverage.
(1,1) would weight Q/R and T equally in the regression.
:return: jacobian matrix as np.ndarray, 10xN where N is the time series length
"""
jacr = np.array([scalars[0]*np.ones(len(baz)),
np.cos(np.radians(baz-azim)),
np.sin(np.radians(baz-azim)),
np.cos(2*np.radians(baz-azim)),
np.sin(2*np.radians(baz-azim)),
np.zeros(len(baz)),
np.cos(np.radians(baz-azim)),
np.sin(np.radians(baz-azim)),
np.cos(2*np.radians(baz-azim)),
np.sin(2*np.radians(baz-azim))])
jact = np.array([np.zeros(len(baz)),
-np.sin(np.radians(baz-azim)),
np.cos(np.radians(baz-azim)),
-np.sin(2*np.radians(baz-azim)),
np.cos(2*np.radians(baz-azim)),
scalars[1]*np.ones(len(baz)),
np.sin(np.radians(baz-azim)),
-np.cos(np.radians(baz-azim)),
np.sin(2*np.radians(baz-azim)),
-np.cos(2*np.radians(baz-azim))])
jac = np.hstack((jacr, jact))
return jac.T


def decomp_one(baz, azim=0):
""" Build jacobian for harmonic decomposition for one RF component.

:param baz: array of back azimuths for RFs in degrees
:param azim: azimuth along which to decompose the RFs.
This can be used with some kind of optimization minimize components
:return: jacobian matrix as np.ndarray, 5xN where N is time series length
"""
jac = np.array([np.ones(len(baz)),
np.cos(np.radians(baz-azim)),
np.sin(np.radians(baz-azim)),
np.cos(2*np.radians(baz-azim)),
np.sin(2*np.radians(baz-azim))])
return jac.T
Loading