"""Functions to calculate local magnitudes automatically, and to calcualte \
relative moments for near-repeating earthquakes using singular-value \
decomposition techniques.
:copyright:
Calum Chamberlain, Chet Hopp.
:license:
GNU Lesser General Public License, Version 3
(https://www.gnu.org/copyleft/lesser.html)
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import numpy as np
import warnings
[docs]def dist_calc(loc1, loc2):
"""
Function to calculate the distance in km between two points.
Uses the flat Earth approximation.
:type loc1: tuple
:param loc1: Tuple of lat, lon, depth (in decimal degrees and km)
:type loc2: tuple
:param loc2: Tuple of lat, lon, depth (in decimal degrees and km)
:returns: float, Distance between points.
"""
R = 6371.009 # Radius of the Earth in km
dlat = np.radians(abs(loc1[0] - loc2[0]))
dlong = np.radians(abs(loc1[1] - loc2[1]))
ddepth = abs(loc1[2] - loc2[2])
mean_lat = np.radians((loc1[0] + loc2[0]) / 2)
dist = R * np.sqrt(dlat ** 2 + (np.cos(mean_lat) * dlong) ** 2)
dist = np.sqrt(dist ** 2 + ddepth ** 2)
return dist
[docs]def calc_max_curv(magnitudes, plotvar=False):
"""
Calculate the magnitude of completeness using the maximum curvature method.
:type magnitudes: list
:param magnitudes: List of magnitudes from which to compute the maximum \
curvature which will give an estimate of the magnitude of completeness \
given the assumption of a power-law scaling.
:type plotvar: bool
:param plotvar: Turn plotting on and off
:rtype: float
:return: Magnitude at maximum curvature
.. Note:: Should be used as a guide, often under-estimates Mc. Personally\
not fond of this method.
.. rubric:: Example
>>> from obspy.clients.fdsn import Client
>>> from obspy import UTCDateTime
>>> from eqcorrscan.utils.mag_calc import calc_max_curv
>>> client = Client('IRIS')
>>> t1 = UTCDateTime('2012-03-26T00:00:00')
>>> t2 = t1 + (3 * 86400)
>>> catalog = client.get_events(starttime=t1, endtime=t2, minmagnitude=3)
>>> magnitudes = [event.magnitudes[0].mag for event in catalog]
>>> calc_max_curv(magnitudes, plotvar=False)
3.6000000000000001
"""
from collections import Counter
import matplotlib.pyplot as plt
counts = Counter(magnitudes)
df = np.zeros(len(counts))
mag_steps = np.zeros(len(counts))
grad = np.zeros(len(counts) - 1)
grad_points = grad.copy()
for i, magnitude in enumerate(sorted(counts.keys(), reverse=True)):
mag_steps[i] = magnitude
df[i] = counts[magnitude]
for i, val in enumerate(df):
if i > 0:
grad[i-1] = (val - df[i-1]) / (mag_steps[i] - mag_steps[i-1])
grad_points[i-1] = mag_steps[i] - ((mag_steps[i] -
mag_steps[i-1]) / 2.0)
# Need to find the second order derivative
curvature = np.zeros(len(grad) - 1)
curvature_points = curvature.copy()
for i, _grad in enumerate(grad):
if i > 0:
curvature[i-1] = (_grad - grad[i-1]) / (grad_points[i] -
grad_points[i-1])
curvature_points[i-1] = grad_points[i] - ((grad_points[i] -
grad_points[i-1]) / 2.0)
if plotvar:
plt.scatter(mag_steps, df, c='k', label='Magnitude function')
plt.plot(mag_steps, df, c='k')
plt.scatter(grad_points, grad, c='r', label='Gradient')
plt.plot(grad_points, grad, c='r')
plt.scatter(curvature_points, curvature, c='g', label='Curvature')
plt.plot(curvature_points, curvature, c='g')
plt.legend()
plt.show()
return curvature_points[np.argmax(abs(curvature))]
[docs]def calc_b_value(magnitudes, completeness, max_mag=None, plotvar=True):
"""
Calculate the b-value for a range of completeness magnitudes.
Calculates a power-law fit to given magnitudes for each completeness
magnitude. Plots the b-values and residuals for the fitted catalogue
against the completeness values. Computes fits using numpy.polyfit,
which uses a least-squares technique.
:type magnitudes: list
:param magnitudes: Magnitudes to compute the b-value for.
:type completeness: list
:param completeness: list of completeness values to comptue b-values for.
:type max_mag: float
:param max_mag: Maximum magnitude to attempt to fit in magnitudes.
:type plotvar: bool
:param plotvar: Turn plotting on or off.
:rtype: list
:return: List of tuples of (completeness, b-value, residual,\
number of magnitudes used)
.. rubric:: Example
>>> from obspy.clients.fdsn import Client
>>> from obspy import UTCDateTime
>>> from eqcorrscan.utils.mag_calc import calc_b_value
>>> client = Client('IRIS')
>>> t1 = UTCDateTime('2012-03-26T00:00:00')
>>> t2 = t1 + (3 * 86400)
>>> catalog = client.get_events(starttime=t1, endtime=t2, minmagnitude=3)
>>> magnitudes = [event.magnitudes[0].mag for event in catalog]
>>> b_values = calc_b_value(magnitudes, completeness=np.arange(3, 7, 0.2),
... plotvar=False)
>>> round(b_values[4][1])
1.0
"""
from collections import Counter
import matplotlib.pyplot as plt
from eqcorrscan.utils.plotting import freq_mag
b_values = []
# Calculate the cdf for all magnitudes
counts = Counter(magnitudes)
cdf = np.zeros(len(counts))
mag_steps = np.zeros(len(counts))
for i, magnitude in enumerate(sorted(counts.keys(), reverse=True)):
mag_steps[i] = magnitude
if i > 0:
cdf[i] = cdf[i-1] + counts[magnitude]
else:
cdf[i] = counts[magnitude]
if not max_mag:
max_mag = max(magnitudes)
for m_c in completeness:
if m_c >= max_mag or m_c >= max(magnitudes):
warnings.warn('Not computing completeness at %s, above max_mag' %
str(m_c))
break
complete_mags = []
complete_freq = []
for i, mag in enumerate(mag_steps):
if mag >= m_c <= max_mag:
complete_mags.append(mag)
complete_freq.append(np.log10(cdf[i]))
if len(complete_mags) < 4:
warnings.warn('Not computing completeness above ' + str(m_c) +
', fewer than 4 events')
break
fit = np.polyfit(complete_mags, complete_freq, 1, full=True)
# Calculate the residuals according to the Wiemer & Wys 2000 definition
predicted_freqs = [fit[0][1] - abs(fit[0][0] * M)
for M in complete_mags]
r = 100 - ((np.sum([abs(complete_freq[i] - predicted_freqs[i])
for i in range(len(complete_freq))]) * 100) /
np.sum(complete_freq))
b_values.append((m_c, abs(fit[0][0]), r, str(len(complete_mags))))
# if plotvar:
# freq_mag(magnitudes=magnitudes, completeness=m_c, max_mag=max_mag)
# print('Residual %s' % r)
if plotvar:
fig, ax1 = plt.subplots()
b_vals = ax1.scatter(zip(*b_values)[0], zip(*b_values)[1], c='k')
resid = ax1.scatter(zip(*b_values)[0],
[100 - b for b in zip(*b_values)[2]], c='r')
ax1.set_ylabel('b-value and residual')
plt.xlabel('Completeness magnitude')
ax2 = ax1.twinx()
ax2.set_ylabel('Number of events used in fit')
n_ev = ax2.scatter(zip(*b_values)[0], zip(*b_values)[3], c='g')
fig.legend((b_vals, resid, n_ev),
('b-values', 'residuals', 'number of events'),
'lower right')
ax1.set_title('Possible completeness values')
plt.show()
return b_values
[docs]def _sim_WA(trace, PAZ, seedresp, water_level):
"""
Remove the instrument response from a trace and simulate a Wood-Anderson.
Returns a de-meaned, de-trended, Wood Anderson simulated trace in
it's place.
Works in-place on data and will destroy your original data, copy the \
trace before giving it to this function!
:type trace: obspy.Trace
:param trace: A standard obspy trace, generally should be given without
pre-filtering, if given with pre-filtering for use with
amplitude determiniation for magnitudes you will need to
worry about how you cope with the response of this filter
yourself.
:type PAZ: dict
:param PAZ: Dictionary containing lists of poles and zeros, the gain and
the sensitivity. If unset will expect seedresp.
:type seedresp: dict
:param seedresp: Seed response information - if unset will expect PAZ.
:type water_level: int
:param water_level: Water level for the simulation.
:returns: obspy.Trace
"""
# Note Wood anderson sensitivity is 2080 as per Uhrhammer & Collins 1990
PAZ_WA = {'poles': [-6.283 + 4.7124j, -6.283 - 4.7124j],
'zeros': [0 + 0j], 'gain': 1.0, 'sensitivity': 2080}
from obspy.signal.invsim import simulate_seismometer as seis_sim
# De-trend data
trace.detrend('simple')
# Simulate Wood Anderson
if PAZ:
trace.data = seis_sim(trace.data, trace.stats.sampling_rate,
paz_remove=PAZ, paz_simulate=PAZ_WA,
water_level=water_level, remove_sensitivity=True)
elif seedresp:
trace.data = seis_sim(trace.data, trace.stats.sampling_rate,
paz_remove=None, paz_simulate=PAZ_WA,
water_level=water_level, seedresp=seedresp)
else:
UserWarning('No response given to remove, will just simulate WA')
trace.data = seis_sim(trace.data, trace.stats.sampling_rate,
paz_remove=None, paz_simulate=PAZ_WA,
water_level=water_level)
return trace
[docs]def _max_p2t(data, delta):
"""
Finds the maximum peak-to-trough amplitude and period.
Originally designed to be used to calculate magnitudes (by \
taking half of the peak-to-trough amplitude as the peak amplitude).
:type data: ndarray
:param data: waveform trace to find the peak-to-trough in.
:type delta: float
:param delta: Sampling interval in seconds
:returns: tuple of (amplitude, period, time) with amplitude in the same \
scale as given in the input data, and period in seconds, and time in \
seconds from the start of the data window.
"""
import matplotlib.pyplot as plt
debug_plot = False
turning_points = [] # A list of tuples of (amplitude, sample)
for i in range(1, len(data) - 1):
if (data[i] < data[i-1] and data[i] < data[i+1]) or\
(data[i] > data[i-1] and data[i] > data[i+1]):
turning_points.append((data[i], i))
if len(turning_points) >= 1:
amplitudes = np.empty([len(turning_points)-1],)
half_periods = np.empty([len(turning_points)-1],)
else:
plt.plot(data)
plt.show()
print('Turning points has length: '+str(len(turning_points)) +
' data have length: '+str(len(data)))
return (0.0, 0.0, 0.0)
for i in range(1, len(turning_points)):
half_periods[i-1] = (delta * (turning_points[i][1] -
turning_points[i-1][1]))
amplitudes[i-1] = np.abs(turning_points[i][0]-turning_points[i-1][0])
amplitude = np.max(amplitudes)
period = 2 * half_periods[np.argmax(amplitudes)]
if debug_plot:
plt.plot(data, 'k')
plt.plot([turning_points[np.argmax(amplitudes)][1],
turning_points[np.argmax(amplitudes)-1][1]],
[turning_points[np.argmax(amplitudes)][0],
turning_points[np.argmax(amplitudes)-1][0]], 'r')
plt.show()
return (amplitude, period, delta*turning_points[np.argmax(amplitudes)][1])
[docs]def _GSE2_PAZ_read(GSEfile):
"""
Read the instrument response information from a GSE Poles and \
Zeros file.
Formatted for files generated by the SEISAN program RESP.
Format must be CAL2, not coded for any other format at the moment, \
contact the authors to add others in.
:type GSEfile: string
:param GSEfile: Path to GSE file
:returns: Dict of poles, zeros, gain and sensitivity
"""
import datetime as dt
f = open(GSEfile)
# First line should start with CAL2
header = f.readline()
if not header[0:4] == 'CAL2':
raise IOError('Unknown format for GSE file, only coded for CAL2')
station = header.split()[1]
channel = header.split()[2]
sensor = header.split()[3]
date = dt.datetime.strptime(header.split()[7], '%Y/%m/%d')
header = f.readline()
if not header[0:4] == 'PAZ2':
raise IOError('Unknown format for GSE file, only coded for PAZ2')
gain = float(header.split()[3]) # Measured in nm/counts
kpoles = int(header.split()[4])
kzeros = int(header.split()[5])
poles = []
for i in range(kpoles):
pole = f.readline()
poles.append(complex(float(pole.split()[0]), float(pole.split()[1])))
zeros = []
for i in range(kzeros):
zero = f.readline()
zeros.append(complex(float(zero.split()[0]), float(zero.split()[1])))
# Have Poles and Zeros, but need Gain and Sensitivity
# Gain should be in the DIG2 line:
for line in f:
if line[0:4] == 'DIG2':
sensitivity = float(line.split()[2]) # measured in counts/muVolt
f.close()
PAZ = {'poles': poles, 'zeros': zeros, 'gain': gain,
'sensitivity': sensitivity}
return PAZ, date, station, channel, sensor
[docs]def _find_resp(station, channel, network, time, delta, directory):
"""
Helper function to find the response information.
Works for a given station and \
channel at a given time and return a dictionary of poles and zeros, gain \
and sensitivity.
:type station: str
:param station: Station name (as in the response files)
:type channel: str
:param channel: Channel name (as in the response files)
:type network: str
:param network: Network to scan for, can be a wildcard
:type time: datetime.datetime
:param time: Date-time to look for repsonse information
:type delta: float
:param delta: Sample interval in seconds
:type directory: str
:param directory: Directory to scan for response information
:returns: dict, response information
"""
import glob
from obspy.signal.invsim import evalresp
from obspy import UTCDateTime
import os
possible_respfiles = glob.glob(directory + os.path.sep + 'RESP.' +
network + '.' + station +
'.*.' + channel) # GeoNet RESP naming
possible_respfiles += glob.glob(directory + os.path.sep + 'RESP.' +
network + '.' + channel +
'.' + station) # RDseed RESP naming
possible_respfiles += glob.glob(directory + os.path.sep + 'RESP.' +
station + '.' + network)
# WIZARD resp naming
# GSE format, station needs to be 5 characters padded with _, channel is 4
# characters padded with _
station = str(station)
channel = str(channel)
possible_respfiles += glob.glob(directory + os.path.sep +
station.ljust(5, str('_')) +
channel[0:len(channel)-1].ljust(3,
str('_')) +
channel[-1] + '.*_GSE')
PAZ = []
seedresp = []
for respfile in possible_respfiles:
print('Reading response from: ' + respfile)
if respfile.split(os.path.sep)[-1][0:4] == 'RESP':
# Read from a resp file
seedresp = {'filename': respfile, 'date': UTCDateTime(time),
'units': 'DIS', 'network': network, 'station': station,
'channel': channel, 'location': '*'}
try:
# Attempt to evaluate the response for this information, if not
# then this is not the correct response info!
freq_resp, freqs = evalresp(delta, 100, seedresp['filename'],
seedresp['date'],
units=seedresp['units'], freq=True,
network=seedresp['network'],
station=seedresp['station'],
channel=seedresp['channel'])
except:
print('Issues with RESP file')
seedresp = []
continue
elif respfile[-3:] == 'GSE':
PAZ, pazdate, pazstation, pazchannel, pazsensor =\
_GSE2_PAZ_read(respfile)
# check that the date is good!
if pazdate >= time and pazchannel != channel and\
pazstation != station:
print('Issue with GSE file')
print('date: ' + str(pazdate) + ' channel: ' + pazchannel +
' station: ' + pazstation)
PAZ = []
else:
continue
# Check that PAZ are for the correct station, channel and date
if PAZ or seedresp:
break
if PAZ:
return PAZ
elif seedresp:
return seedresp
[docs]def _pairwise(iterable):
"""
Wrapper on itertools for SVD_magnitude.
"""
import itertools
import sys
a, b = itertools.tee(iterable)
next(b, None)
if sys.version_info.major == 2:
return itertools.izip(a, b)
else:
return zip(a, b)
def Amp_pick_sfile(sfile, datapath, respdir, chans=['Z'], var_wintype=True,
winlen=0.9, pre_pick=0.2, pre_filt=True, lowcut=1.0,
highcut=20.0, corners=4):
"""Depreciated, please use amp_pick_sfile"""
warnings.warn('Depreciation warning: Amp_pick_sfile is depreciated, ' +
'use amp_pick_sfile')
event = amp_pick_sfile(sfile, datapath, respdir, chans, var_wintype,
winlen, pre_pick, pre_filt, lowcut,
highcut, corners)
return event
[docs]def amp_pick_event(event, st, respdir, chans=['Z'], var_wintype=True,
winlen=0.9, pre_pick=0.2, pre_filt=True, lowcut=1.0,
highcut=20.0, corners=4):
"""
Pick amplitudes for local magnitude for a single event.
Looks for maximum peak-to-trough amplitude for a channel in a stream, and \
picks this amplitude and period. There are a few things it does \
internally to stabilise the result
1. Applies a given filter to the data - very necessary for small \
magnitude earthquakes;
2. Keeps track of the poles and zeros of this filter and removes then \
from the picked amplitude;
3. Picks the peak-to-trough amplitude, but records half of this: the \
specification for the local magnitude is to use a peak amplitude on \
a horizontal, however, with modern digital seismometers, the peak \
amplitude often has an additional, DC-shift applied to it, to \
stabilise this, and to remove possible issues with de-meaning data \
recorded during the wave-train of an event (e.g. the mean may not be \
the same as it would be for longer durations), we use half the \
peak-to-trough amplitude;
4. Despite the original definition of local magnitude requiring the \
use of a horizontal channel, more recent work has shown that the \
vertical channels give more consistent magnitude estimations between \
stations, due to a reduction in site-amplification effects, we \
therefore use the vertical channels by default, but allow the user \
to chose which channels they deem appropriate;
5. We do not specify that the maximum amplitude should be the \
S-phase: The original definition holds that the maximum body-wave \
amplitude should be used - while this is often the S-phase, we do not \
discrimiate against the P-phase. We do note that, unless the user \
takes care when assigning winlen and filters, they may end up with \
amplitude picks for surface waves;
6. We use a variable window-length by default that takes into account \
P-S times if available, this is in an effort to include only the \
body waves. When P-S times are not available we hard-wire a P-S \
at 0.34 x hypocentral distance.
:type event: obspy.core.event.Event
:param event: Event to pick
:type st: obspy.core.Stream
:param st: Stream associated with event
:type respdir: str
:param respdir: Path to the response information directory
:type chans: list
:param chans: List of the channels to pick on, defaults to ['Z'] - should \
just be the orientations, e.g. Z,1,2,N,E
:type var_wintype: bool
:param var_wintype: If True, the winlen will be \
multiplied by the P-S time if both P and S picks are \
available, otherwise it will be multiplied by the \
hypocentral distance*0.34 - derived using a p-s ratio of \
1.68 and S-velocity of 1.5km/s to give a large window, \
defaults to True
:type winlen: float
:param winlen: Length of window, see above parameter, if var_wintype is \
False then this will be in seconds, otherwise it is the \
multiplier to the p-s time, defaults to 0.5.
:type pre_pick: float
:param pre_pick: Time before the s-pick to start the cut window, defaults \
to 0.2
:type pre_filt: bool
:param pre_filt: To apply a pre-filter or not, defaults to True
:type lowcut: float
:param lowcut: Lowcut in Hz for the pre-filter, defaults to 1.0
:type highcut: float
:param highcut: Highcut in Hz for the pre-filter, defaults to 20.0
:type corners: int
:param corners: Number of corners to use in the pre-filter
:returns: obspy.core.event
"""
# Hardwire a p-s multiplier of hypocentral distance based on p-s ratio of
# 1.68 and an S-velocity 0f 1.5km/s, deliberately chosen to be quite slow
ps_multiplier = 0.34
from obspy import read
from scipy.signal import iirfilter
from obspy.signal.invsim import paz_2_amplitude_value_of_freq_resp
import warnings
from obspy.core.event import Amplitude, Pick, WaveformStreamID
# Convert these picks into a lists
stations = [] # List of stations
channels = [] # List of channels
picktimes = [] # List of pick times
picktypes = [] # List of pick types
distances = [] # List of hypocentral distances
picks_out = []
for pick in event.picks:
if pick.phase_hint in ['P', 'S']:
picks_out.append(pick) # Need to be able to remove this if there
# isn't data for a station!
stations.append(pick.waveform_id.station_code)
channels.append(pick.waveform_id.channel_code)
picktimes.append(pick.time)
picktypes.append(pick.phase_hint)
arrival = [arrival for arrival in event.origins[0].arrivals
if arrival.pick_id == pick.resource_id][0]
distances.append(arrival.distance)
st.merge() # merge the data, just in case!
# For each station cut the window
uniq_stas = list(set(stations))
del(arrival)
for sta in uniq_stas:
for chan in chans:
print('Working on '+sta+' '+chan)
tr = st.select(station=sta, channel='*'+chan)
if not tr:
# Remove picks from file
# picks_out=[picks_out[i] for i in range(len(picks))\
# if picks_out[i].station+picks_out[i].channel != \
# sta+chan]
warnings.warn('There is no station and channel match in the ' +
'wavefile!')
break
else:
tr = tr[0]
# Apply the pre-filter
if pre_filt:
try:
tr.detrend('simple')
except:
dummy = tr.split()
dummy.detrend('simple')
tr = dummy.merge()[0]
tr.filter('bandpass', freqmin=lowcut, freqmax=highcut,
corners=corners)
sta_picks = [i for i in range(len(stations))
if stations[i] == sta]
pick_id = event.picks[sta_picks[0]].resource_id
arrival = [arrival for arrival in event.origins[0].arrivals
if arrival.pick_id == pick_id][0]
hypo_dist = arrival.distance
CAZ = arrival.azimuth
if var_wintype:
if 'S' in [picktypes[i] for i in sta_picks] and\
'P' in [picktypes[i] for i in sta_picks]:
# If there is an S-pick we can use this :D
S_pick = [picktimes[i] for i in sta_picks
if picktypes[i] == 'S']
S_pick = min(S_pick)
P_pick = [picktimes[i] for i in sta_picks
if picktypes[i] == 'P']
P_pick = min(P_pick)
try:
tr.trim(starttime=S_pick-pre_pick,
endtime=S_pick+(S_pick-P_pick)*winlen)
except ValueError:
break
elif 'S' in [picktypes[i] for i in sta_picks]:
S_pick = [picktimes[i] for i in sta_picks
if picktypes[i] == 'S']
S_pick = min(S_pick)
P_modelled = S_pick - hypo_dist * ps_multiplier
try:
tr.trim(starttime=S_pick-pre_pick,
endtime=S_pick + (S_pick - P_modelled) *
winlen)
except ValueError:
break
else:
# In this case we only have a P pick
P_pick = [picktimes[i] for i in sta_picks
if picktypes[i] == 'P']
P_pick = min(P_pick)
S_modelled = P_pick + hypo_dist * ps_multiplier
try:
tr.trim(starttime=S_modelled - pre_pick,
endtime=S_modelled + (S_modelled - P_pick) *
winlen)
except ValueError:
break
# Work out the window length based on p-s time or distance
elif 'S' in [picktypes[i] for i in sta_picks]:
# If the window is fixed we still need to find the start time,
# which can be based either on the S-pick (this elif), or
# on the hypocentral distance and the P-pick
# Take the minimum S-pick time if more than one S-pick is
# available
S_pick = [picktimes[i] for i in sta_picks
if picktypes[i] == 'S']
S_pick = min(S_pick)
try:
tr.trim(starttime=S_pick - pre_pick,
endtime=S_pick + winlen)
except ValueError:
break
else:
# In this case, there is no S-pick and the window length is
# fixed we need to calculate an expected S_pick based on the
# hypocentral distance, this will be quite hand-wavey as we
# are not using any kind of velocity model.
P_pick = [picktimes[i] for i in sta_picks
if picktypes[i] == 'P']
P_pick = min(P_pick)
hypo_dist = [distances[i] for i in sta_picks
if picktypes[i] == 'P'][0]
S_modelled = P_pick + hypo_dist * ps_multiplier
try:
tr.trim(starttime=S_modelled - pre_pick,
endtime=S_modelled + winlen)
except ValueError:
break
# Find the response information
resp_info = _find_resp(tr.stats.station, tr.stats.channel,
tr.stats.network, tr.stats.starttime,
tr.stats.delta, respdir)
PAZ = []
seedresp = []
if resp_info and 'gain' in resp_info:
PAZ = resp_info
elif resp_info:
seedresp = resp_info
# Simulate a Wood Anderson Seismograph
if PAZ and len(tr.data) > 10:
# Set ten data points to be the minimum to pass
tr = _sim_WA(tr, PAZ, None, 10)
elif seedresp and len(tr.data) > 10:
tr = _sim_WA(tr, None, seedresp, 10)
elif len(tr.data) > 10:
warnings.warn('No PAZ for '+tr.stats.station+' ' +
tr.stats.channel+' at time: ' +
str(tr.stats.starttime))
continue
if len(tr.data) <= 10:
# Should remove the P and S picks if len(tr.data)==0
warnings.warn('No data found for: '+tr.stats.station)
# print 'No data in miniseed file for '+tr.stats.station+\
# ' removing picks'
# picks_out=[picks_out[i] for i in range(len(picks_out))\
# if i not in sta_picks]
break
# Get the amplitude
amplitude, period, delay = _max_p2t(tr.data, tr.stats.delta)
if amplitude == 0.0:
break
print('Amplitude picked: ' + str(amplitude))
# Note, amplitude should be in meters at the moment!
# Remove the pre-filter response
if pre_filt:
# Generate poles and zeros for the filter we used earlier: this
# is how the filter is designed in the convenience methods of
# filtering in obspy.
z, p, k = iirfilter(corners, [lowcut / (0.5 *
tr.stats.
sampling_rate),
highcut / (0.5 *
tr.stats.
sampling_rate)],
btype='band', ftype='butter', output='zpk')
filt_paz = {'poles': list(p),
'zeros': list(z),
'gain': k,
'sensitivity': 1.0}
amplitude /= (paz_2_amplitude_value_of_freq_resp(filt_paz,
1 / period) *
filt_paz['sensitivity'])
# Convert amplitude to mm
if PAZ: # Divide by Gain to get to nm (returns pm? 10^-12)
# amplitude *=PAZ['gain']
amplitude /= 1000
if seedresp: # Seedresp method returns mm
amplitude *= 1000000
# Write out the half amplitude, approximately the peak amplitude as
# used directly in magnitude calculations
# Page 343 of Seisan manual:
# Amplitude (Zero-Peak) in units of nm, nm/s, nm/s^2 or counts
amplitude *= 0.5
# Append an amplitude reading to the event
_waveform_id = WaveformStreamID(station_code=tr.stats.station,
channel_code=tr.stats.channel,
network_code=tr.stats.network)
pick_ind = len(event.picks)
event.picks.append(Pick(waveform_id=_waveform_id,
phase_hint='IAML',
polarity='undecidable',
time=tr.stats.starttime + delay,
evaluation_mode='automatic'))
event.amplitudes.append(Amplitude(generic_amplitude=amplitude /
10**9,
period=period,
pick_id=event.
picks[pick_ind].resource_id,
waveform_id=event.
picks[pick_ind].waveform_id,
unit='m',
magnitude_hint='ML',
type='AML',
category='point'))
return event
[docs]def amp_pick_sfile(sfile, datapath, respdir, chans=['Z'], var_wintype=True,
winlen=0.9, pre_pick=0.2, pre_filt=True, lowcut=1.0,
highcut=20.0, corners=4):
"""
Function to pick amplitudes for local magnitudes from NORDIC s-files.
Reads information from a SEISAN s-file, load the data and the \
picks, cut the data for the channels given around the S-window, simulate \
a Wood Anderson seismometer, then pick the maximum peak-to-trough \
amplitude.
Output will be put into a mag_calc.out file which will be in full S-file \
format and can be copied to a REA database.
:type sfile: str
:param sfile: Path to NORDIC format s-file
:type datapath: str
:param datapath: Path to the waveform files - usually the path to the WAV \
directory
:type respdir: str
:param respdir: Path to the response information directory
:type chans: list
:param chans: List of the channels to pick on, defaults to ['Z'] - should \
just be the orientations, e.g. Z,1,2,N,E
:type var_wintype: bool
:param var_wintype: If True, the winlen will be \
multiplied by the P-S time if both P and S picks are \
available, otherwise it will be multiplied by the \
hypocentral distance*0.34 - derived using a p-s ratio of \
1.68 and S-velocity of 1.5km/s to give a large window, \
defaults to True
:type winlen: float
:param winlen: Length of window, see above parameter, if var_wintype is \
False then this will be in seconds, otherwise it is the \
multiplier to the p-s time, defaults to 0.5.
:type pre_pick: float
:param pre_pick: Time before the s-pick to start the cut window, defaults \
to 0.2
:type pre_filt: bool
:param pre_filt: To apply a pre-filter or not, defaults to True
:type lowcut: float
:param lowcut: Lowcut in Hz for the pre-filter, defaults to 1.0
:type highcut: float
:param highcut: Highcut in Hz for the pre-filter, defaults to 20.0
:type corners: int
:param corners: Number of corners to use in the pre-filter
:returns: obspy.core.event
"""
from eqcorrscan.utils import sfile_util
from obspy import read
import shutil
# First we need to work out what stations have what picks
event = sfile_util.readpicks(sfile)
# Read in waveforms
stream = read(datapath+'/'+sfile_util.readwavename(sfile)[0])
if len(sfile_util.readwavename(sfile)) > 1:
for wavfile in sfile_util.readwavename(sfile):
stream += read(datapath+'/'+wavfile)
stream.merge() # merge the data, just in case!
event_picked = amp_pick_event(event=event, st=stream, respdir=respdir,
chans=chans, var_wintype=var_wintype,
winlen=winlen, pre_pick=pre_pick,
pre_filt=pre_filt, lowcut=lowcut,
highcut=highcut, corners=corners)
new_sfile = sfile_util.eventtosfile(event=event, userID=str('EQCO'),
evtype=str('L'), outdir=str('.'),
wavefiles=sfile_util.
readwavename(sfile))
shutil.move(new_sfile, 'mag_calc.out')
return event
[docs]def SVD_moments(U, s, V, stachans, event_list, n_SVs=4):
"""
Calculate relative moments using singular-value decomposition.
Convert basis vectors calculated by singular value \
decomposition (see the SVD functions in clustering) into relative \
moments.
For more information see the paper by \
`Rubinstein & Ellsworth (2010).
<http://www.bssaonline.org/content/100/5A/1952.short>`_
:type U: list
:param U: List of the numpy array input basis vectors from the SVD, \
one array for each channel used.
:type s: list
:param s: List of the numpy arrays of singular values, one array for \
each channel.
:type V: list
:param V: List of numpy arrays of output basis vectors from SVD, one \
array per channel.
:type stachans: list
:param stachans: List of station.channel input
:type event_list: list
:param event_list: List of events for which you have data, such that \
event_list[i] corresponds to stachans[i], U[i] etc. and \
event_list[i][j] corresponds to event j in U[i]. These are a series \
of indexes that map the basis vectors to their relative events and \
channels - if you have every channel for every event generating these \
is trivial (see example).
:type n_SVs: int
:param n_SVs: Number of singular values to use, defaults to 4.
:returns: M, array of relative moments
:rtype: numpy.ndarray
:returns: events_out, list of events that relate to M (in order), \
does not include the magnitude information in the events, see note.
:rtype: obspy.core.event.Event
.. note:: M is an array of relative moments, these cannot be directly \
compared to true moments without calibration.
.. rubric:: Example
>>> from eqcorrscan.utils.mag_calc import SVD_moments
>>> from obspy import read
>>> import glob
>>> import os
>>> from eqcorrscan.utils.clustering import SVD
>>> import numpy as np
>>> # Do the set-up
>>> testing_path = 'eqcorrscan/tests/test_data/similar_events'
>>> stream_files = glob.glob(os.path.join(testing_path, '*'))
>>> stream_list = [read(stream_file) for stream_file in stream_files]
>>> event_list = []
>>> for i, stream in enumerate(stream_list):
... st_list = []
... for tr in stream:
... if (tr.stats.station, tr.stats.channel) not in [('WHAT2', 'SH1'), ('WV04', 'SHZ'), ('GCSZ', 'EHZ')]:
... stream.remove(tr)
... continue
... tr.detrend('simple')
... tr.filter('bandpass', freqmin=5.0, freqmax=15.0)
... tr.trim(tr.stats.starttime + 40, tr.stats.endtime - 45)
... st_list.append(i)
... event_list.append(st_list) # doctest: +SKIP
>>> event_list = np.asarray(event_list).T.tolist()
>>> SVectors, SValues, Uvectors, stachans = SVD(stream_list=stream_list) # doctest: +SKIP
['GCSZ.EHZ', 'WV04.SHZ', 'WHAT2.SH1']
>>> M, events_out = SVD_moments(U=Uvectors, s=SValues, V=SVectors,
... stachans=stachans, event_list=event_list) # doctest: +SKIP
"""
import copy
import random
import pickle
# Define maximum number of events, will be the width of K
K_width = max([max(ev_list) for ev_list in event_list])+1
# Sometimes the randomisation generates a singular matrix - rather than
# attempting to regulerize this matrix I propose undertaking the
# randomisation step a further time
for i, stachan in enumerate(stachans):
k = [] # Small kernel matrix for one station - channel
# Copy the relevant vectors so as not to detroy them
U_working = copy.deepcopy(U[i])
V_working = copy.deepcopy(V[i])
s_working = copy.deepcopy(s[i])
ev_list = event_list[i]
if len(ev_list) > len(V_working):
print('V is : '+str(len(V_working)))
f_dump = open('mag_calc_V_working.pkl', 'wb')
pickle.dump(V_working, f_dump)
f_dump.close()
raise IOError('More events than represented in V')
# Set all non-important singular values to zero
s_working[n_SVs:len(s_working)] = 0
s_working = np.diag(s_working)
# Convert to numpy matrices
U_working = np.matrix(U_working)
V_working = np.matrix(V_working)
s_working = np.matrix(s_working)
SVD_weights = U_working[:, 0]
# If all the weights are negative take the abs
if np.all(SVD_weights < 0):
warnings.warn('All weights are negative - flipping them')
SVD_weights = np.abs(SVD_weights)
SVD_weights = np.array(SVD_weights).reshape(-1).tolist()
# Shuffle the SVD_weights prior to pairing - will give one of multiple
# pairwise options - see p1956 of Rubinstein & Ellsworth 2010
# We need to keep the real indexes though, otherwise, if there are
# multiple events with the same weight we will end up with multiple
# -1 values
random_SVD_weights = np.copy(SVD_weights)
# Tack on the indexes
random_SVD_weights = random_SVD_weights.tolist()
random_SVD_weights = [(random_SVD_weights[_i], _i)
for _i in range(len(random_SVD_weights))]
random.shuffle(random_SVD_weights)
# Add the first element to the end so all elements will be paired twice
random_SVD_weights.append(random_SVD_weights[0])
# Take pairs of all the SVD_weights (each weight appears in 2 pairs)
pairs = []
for pair in _pairwise(random_SVD_weights):
pairs.append(pair)
# Deciding values for each place in kernel matrix using the pairs
for pairsIndex in range(len(pairs)):
# We will normalize by the minimum weight
_weights = list(zip(*list(pairs[pairsIndex])))[0]
_indeces = list(zip(*list(pairs[pairsIndex])))[1]
min_weight = min(_weights)
max_weight = max(_weights)
min_index = _indeces[np.argmin(_weights)]
max_index = _indeces[np.argmax(_weights)]
row = []
# Working out values for each row of kernel matrix
for j in range(len(SVD_weights)):
if j == max_index:
result = -1
elif j == min_index:
normalised = max_weight / min_weight
result = float(normalised)
else:
result = 0
row.append(result)
print(row)
# Add each row to the K matrix
k.append(row)
# k is now a square matrix, we need to flesh it out to be K_width
k_filled = np.zeros([len(k), K_width])
for j in range(len(k)):
for l, ev in enumerate(ev_list):
k_filled[j, ev] = k[j][l]
if 'K' not in locals():
K = k_filled
else:
K = np.concatenate([K, k_filled])
# Remove any empty rows
K_nonempty = []
events_out = []
for i in range(0, K_width):
if not np.all(K[:, i] == 0):
K_nonempty.append(K[:, i])
events_out.append(i)
K = np.array(K_nonempty).T
K = K.tolist()
K_width = len(K[0])
# Add an extra row to K, so average moment = 1
K.append(np.ones(K_width) * (1. / K_width))
print("\nCreated Kernel matrix: ")
del row
print('\n'.join([''.join([str(round(float(item), 3)).ljust(6)
for item in row]) for row in K]))
Krounded = np.around(K, decimals=4)
# Create a weighting matrix to put emphasis on the final row.
W = np.matrix(np.identity(len(K)))
# the final element of W = the number of stations*number of events
W[-1, -1] = len(K) - 1
# Make K into a matrix
K = np.matrix(K)
############
# Solve using the weighted least squares equation, K.T is K transpose
Kinv = np.array(np.linalg.inv(K.T*W*K) * K.T * W)
# M are the relative moments of the events
M = Kinv[:, -1]
return M, events_out
[docs]def pick_db(indir, outdir, calpath, startdate, enddate, wavepath=None):
"""
Wrapper to loop through a SEISAN database and make a lot of magnitude \
picks.
:type indir: str
:param indir: Path to the seisan REA directory (not including yyyy/mm)
:type outdir: str
:param outdir: Path to output seisan REA directory (not including yyyy/mm)
:type calpath: str
:param calpath: Path to the directory containing the response files
:type startdate: datetime.datetime
:param startdate: Date to start looking for S-files
:type enddate: datetime.datetime
:param enddate: Date to stop looking for S-files
:type wavepath: str
:param wavepath: Path to the seisan WAV directory (not including yyyy/mm)
"""
import datetime as dt
import glob
import shutil
kdays = ((enddate + dt.timedelta(1)) - startdate).days
for i in range(kdays):
day = startdate + dt.timedelta(i)
print('Working on ' + str(day))
sfiles = glob.glob(indir + '/' + str(day.year) + '/' +
str(day.month).zfill(2) + '/' +
str(day.day).zfill(2) + '-*L.S' + str(day.year) +
str(day.month).zfill(2))
datetimes = [dt.datetime.strptime(sfiles[i].split('/')[-1],
'%d-%H%M-%SL.S%Y%m')
for i in range(len(sfiles))]
sfiles = [sfiles[i] for i in range(len(sfiles))
if datetimes[i] > startdate and
datetimes[i] < enddate]
if not wavepath:
wavedir = "/".join(indir.split('/')[:-2])+'/WAV/' +\
indir.split('/')[-1]+'/'+str(day.year)+'/' +\
str(day.month).zfill(2)
else:
wavedir = wavepath+'/'+str(day.year)+'/' +\
str(day.month).zfill(2)
sfiles.sort()
for sfile in sfiles:
# Make the picks!
print(' Working on Sfile: '+sfile)
event = Amp_pick_sfile(sfile, wavedir, calpath)
del event
# Copy the mag_calc.out file to the correct place
shutil.copyfile('mag_calc.out', outdir+'/'+str(day.year)+'/' +
str(day.month).zfill(2)+'/'+sfile.split('/')[-1])
if __name__ == "__main__":
import doctest
doctest.testmod()