# https://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------
# import copy
import logging
import operator
import calendar
import numpy as num
from pyrocko.guts import \
Object, StringChoice, String, List, Float
from pyrocko import table, model # , automap
from pyrocko.client import fdsn, catalog
from pyrocko.gui.qt_compat import qw, qc
from pyrocko.plot import beachball
# from pyrocko.himesh import HiMesh
# from pyrocko.gui.vtk_util import TrimeshPipe
from .table import TableElement, TableState
logger = logging.getLogger('pyrocko.gui.sparrow.elements.catalog')
guts_prefix = 'sparrow'
attribute_names = [
'time', 'lat', 'lon', 'northing', 'easting', 'depth', 'magnitude']
attribute_dtypes = [
'f16', 'f8', 'f8', 'f8', 'f8', 'f8', 'f8']
name_to_icol = dict(
(name, icol) for (icol, name) in enumerate(attribute_names))
# event_dtype = num.dtype(list(zip(attribute_names, attribute_dtypes)))
t_time = num.float64
def binned_statistic(values, ibins, function):
order = num.argsort(ibins)
values_sorted = values[order]
ibins_sorted = ibins[order]
parts = num.concatenate((
[0],
num.where(num.diff(ibins_sorted) != 0)[0] + 1,
[ibins.size]))
results = []
ibins_result = []
for ilow, ihigh in zip(parts[:-1], parts[1:]):
values_part = values_sorted[ilow:ihigh]
results.append(function(values_part))
ibins_result.append(ibins_sorted[ilow])
return num.array(ibins_result, dtype=num.int64), num.array(results)
def load_text(
filepath,
column_names=('time', 'lat', 'lon', 'depth', 'magnitude'),
time_format='seconds'):
with open(filepath, 'r') as f:
if column_names == 'from_header':
line = f.readline()
column_names = line.split()
name_to_icol_in = dict(
(name, icol) for (icol, name) in enumerate(column_names)
if name in attribute_names)
data_in = num.loadtxt(filepath, skiprows=1)
nevents = data_in.shape[0]
c5 = num.zeros((nevents, 5))
c5[:, 0] = data_in[:, name_to_icol_in['lat']]
c5[:, 1] = data_in[:, name_to_icol_in['lon']]
c5[:, 2] = 0.0
c5[:, 3] = 0.0
c5[:, 4] = data_in[:, name_to_icol_in['depth']] * 1000.
tab = table.Table()
loc_rec = table.LocationRecipe()
tab.add_recipe(loc_rec)
tab.add_col(loc_rec.c5_header, c5)
for k, unit in [
('time', 's'),
('magnitude', None)]:
values = data_in[:, name_to_icol_in[k]]
if k == 'time' and time_format == 'year':
values = decimal_year_to_time(values)
tab.add_col(table.Header(k, unit), values)
return tab
def decimal_year_to_time(year):
iyear_start = num.floor(year).astype(num.int64)
iyear_end = iyear_start + 1
iyear_min = num.min(iyear_start)
iyear_max = num.max(iyear_end)
iyear_to_time = num.zeros(iyear_max - iyear_min + 1, dtype=t_time)
for iyear in range(iyear_min, iyear_max+1):
iyear_to_time[iyear-iyear_min] = calendar.timegm(
(iyear, 1, 1, 0, 0, 0))
tyear_start = iyear_to_time[iyear_start - iyear_min]
tyear_end = iyear_to_time[iyear_end - iyear_min]
t = tyear_start + (year - iyear_start) * (tyear_end - tyear_start)
return t
def oa_to_array(objects, attribute):
return num.fromiter(
map(operator.attrgetter(attribute), objects),
num.float64,
len(objects))
def eventtags_to_array(events, tab):
ks = set()
event_tags = []
for ev in events:
tags = ev.tags_as_dict()
ks.update(tags.keys())
event_tags.append(tags)
for k in sorted(ks):
column = [tags.get(k, None) for tags in event_tags]
if all(isinstance(v, int) for v in column):
dtype = int
elif all(isinstance(v, float) for v in column):
dtype = float
else:
dtype = str
column = [v or '' for v in column]
arr = num.array(column, dtype=dtype)
tab.add_col(table.Header('tag_%s' % k), arr)
def eventextras_to_array(events, tab):
n_extras = num.array([len(ev.extras) for ev in events])
evts_all_extras = num.arange(len(events))[n_extras != 0]
if evts_all_extras.shape[0] == 0:
return tab
ev0 = events[evts_all_extras[0]]
if num.unique(n_extras).shape[0] > 1:
msg = 'Not all events have equal number of extras.'
if num.unique(n_extras).shape[0] == 2 and num.min(n_extras) == 0:
logger.warning(msg + ' Zero length lists are filled with NaNs.')
else:
raise IndexError(
msg + ' Several non-zero shapes detected. Please check.')
for key, val in ev0.extras.items():
dtype = str
values = num.array(['' for x in range(n_extras.shape[0])], dtype=dtype)
if type(val) is float:
dtype, values = num.float64, num.ones_like(n_extras) * num.nan
elif type(val) is int:
dtype = num.int64
values = num.ones_like(n_extras, dtype=num.int64) * num.nan
header = 'extra_%s' % key
values[evts_all_extras] = num.array([
events[iev].extras[key] for iev in evts_all_extras], dtype=dtype)
tab.add_col(table.Header(header), values)
def events_to_table(events):
c5 = num.zeros((len(events), 5))
m6 = None
for i, ev in enumerate(events):
c5[i, :] = (
ev.lat, ev.lon, ev.north_shift, ev.east_shift, ev.depth)
if ev.moment_tensor:
if m6 is None:
m6 = num.zeros((len(events), 6))
m6[:, 0:3] = 1.0
m6[i, :] = beachball.deco_part(ev.moment_tensor, 'deviatoric').m6()
tab = table.Table()
ev_rec = table.EventRecipe()
tab.add_recipe(ev_rec)
tab.add_col(ev_rec.get_header('c5'), c5)
for k in ['time', 'magnitude']:
tab.add_col(ev_rec.get_header(k), oa_to_array(events, k))
if events:
eventtags_to_array(events, tab)
eventextras_to_array(events, tab)
if m6 is not None:
mt_rec = table.MomentTensorRecipe()
tab.add_recipe(mt_rec)
tab.add_col(mt_rec.m6_header, m6)
return tab
[docs]
class LoadingChoice(StringChoice):
choices = [choice.upper() for choice in [
'file',
'fdsn']]
[docs]
class FDSNSiteChoice(StringChoice):
choices = [key.upper() for key in fdsn.g_site_abbr.keys()]
[docs]
class CatalogSelection(Object):
[docs]
def get_events(self):
'''
Get events of the selection.
Returns ``None`` if the selection is not based on events (e.g. if it
has been loaded from a plain text table).
'''
raise NotImplementedError()
def get_table(self):
return events_to_table(self.get_events())
[docs]
class MemoryCatalogSelection(CatalogSelection):
def __init__(self, events=None):
if events is None:
events = []
self._events = events
def get_events(self):
return self._events
[docs]
class FileCatalogSelection(CatalogSelection):
paths = List.T(String.T())
def _is_text_table(self):
return any(
path.split('.')[-1].lower() in ['dat', 'csv']
for path in self.paths)
def get_events(self):
from pyrocko.io import quakeml
if self._is_text_table():
return None
events = []
for path in self.paths:
fn_ext = path.split('.')[-1].lower()
if fn_ext in ['xml', 'qml', 'quakeml']:
qml = quakeml.QuakeML.load_xml(filename=path)
events.extend(qml.get_pyrocko_events())
else:
events.extend(model.load_events(path))
return events
def get_table(self):
if self._is_text_table():
assert len(self.paths) == 1
return load_text(
self.paths[0], column_names='from_header', time_format='year')
return events_to_table(self.get_events())
g_catalogs = {
'Geofon': catalog.Geofon(),
'USGS/NEIC US': catalog.USGS('us'),
'Global-CMT': catalog.GlobalCMT(),
'Saxony (Uni-Leipzig)': catalog.Saxony()
}
g_fdsn_has_events = ['ISC', 'SCEDC', 'NCEDC', 'IRIS', 'GEONET']
g_sites = sorted(g_catalogs.keys())
g_sites.extend(g_fdsn_has_events)
[docs]
class OnlineCatalogSelection(CatalogSelection):
site = String.T()
tmin = Float.T()
tmax = Float.T()
magnitude_min = Float.T(optional=True)
def get_events(self):
logger.info('Getting events from "%s" catalog.' % self.site)
cat = g_catalogs.get(self.site, None)
try:
if cat:
kwargs = {}
if self.magnitude_min is not None:
kwargs['magmin'] = self.magnitude_min
events = cat.get_events(
time_range=(self.tmin, self.tmax), **kwargs)
else:
kwargs = {}
if self.magnitude_min is not None:
kwargs['minmagnitude'] = self.magnitude_min
request = fdsn.event(
starttime=self.tmin,
endtime=self.tmax,
site=self.site, **kwargs)
from pyrocko.io import quakeml
qml = quakeml.QuakeML.load_xml(request)
events = qml.get_pyrocko_events()
logger.info('Got %i event%s from "%s" catalog.' % (
len(events), '' if len(events) == 1 else 's', self.site))
except Exception as e:
logger.error(
'Getting events from "%s" catalog failed: %s' % (
self.site, str(e)))
events = []
return events
_m6_indices = [(0, 0), (1, 1), (2, 2), (0, 1), (0, 2), (1, 2)]
def m6s_to_m33s(m6s):
m33s = num.zeros((m6s.shape[0], 3, 3))
for i, (j, k) in enumerate(_m6_indices):
m33s[:, j, k] = m6s[:, i]
m33s[:, k, j] = m6s[:, i]
return m33s
def m33s_to_m6s(m33s):
return num.stack([
m33s[:, j, k]
for (j, k) in _m6_indices],
axis=1)
def skew_matrices(axes):
k = num.zeros((axes.shape[0], 3, 3))
k[:, 0, 1] = -axes[:, 2]
k[:, 0, 2] = axes[:, 1]
k[:, 1, 0] = axes[:, 2]
k[:, 1, 2] = -axes[:, 0]
k[:, 2, 0] = -axes[:, 1]
k[:, 2, 1] = axes[:, 0]
return k
[docs]
class MomentTensorInterpolator(object):
'''
Interpolate between pairs of moment tensors.
The eigenvalues (normalized to unit norm) are interpolated linearly, the
scalar moment logarithmically and the orientation of the principal axes
along the shortest rotation connecting the two eigensystems (taking into
account the sign ambiguity of the eigenvectors).
'''
def __init__(self, m6s_a, m6s_b):
evals_a, rots_a = num.linalg.eigh(m6s_to_m33s(m6s_a))
evals_b, rots_b = num.linalg.eigh(m6s_to_m33s(m6s_b))
# make proper rotations
rots_a[:, :, 2] *= num.sign(num.linalg.det(rots_a))[:, num.newaxis]
rots_b[:, :, 2] *= num.sign(num.linalg.det(rots_b))[:, num.newaxis]
# choose the eigenvector signs giving the smallest rotation
best_trace = num.full(m6s_a.shape[0], -num.inf)
best_rots = num.zeros_like(rots_a)
for flips in [(1, 1, 1), (1, -1, -1), (-1, 1, -1), (-1, -1, 1)]:
rots = num.einsum(
'nij,nkj->nik',
rots_b * num.array(flips)[num.newaxis, num.newaxis, :],
rots_a)
trace = num.einsum('nii->n', rots)
better = trace > best_trace
best_trace[better] = trace[better]
best_rots[better] = rots[better]
angles = num.arccos(num.clip((best_trace - 1.0) * 0.5, -1.0, 1.0))
axes = num.stack([
best_rots[:, 2, 1] - best_rots[:, 1, 2],
best_rots[:, 0, 2] - best_rots[:, 2, 0],
best_rots[:, 1, 0] - best_rots[:, 0, 1]], axis=1)
axes_norm = num.sqrt(num.sum(axes**2, axis=1))
axes_norm[axes_norm == 0.0] = 1.0
axes /= axes_norm[:, num.newaxis]
moments_a = num.sqrt(num.sum(evals_a**2, axis=1))
moments_b = num.sqrt(num.sum(evals_b**2, axis=1))
moments_a[moments_a == 0.0] = 1.0
moments_b[moments_b == 0.0] = 1.0
self._evals_a = evals_a / moments_a[:, num.newaxis]
self._evals_b = evals_b / moments_b[:, num.newaxis]
self._log_moments_a = num.log(moments_a)
self._log_moments_b = num.log(moments_b)
self._rots_a = rots_a
self._angles = angles
self._skew = skew_matrices(axes)
def __call__(self, x):
evals = (1.0 - x) * self._evals_a + x * self._evals_b
evals_norm = num.sqrt(num.sum(evals**2, axis=1))
evals_norm[evals_norm == 0.0] = 1.0
evals *= (num.exp(
(1.0 - x) * self._log_moments_a
+ x * self._log_moments_b) / evals_norm)[:, num.newaxis]
angles = x * self._angles
k = self._skew
rots = num.eye(3)[num.newaxis, :, :] \
+ num.sin(angles)[:, num.newaxis, num.newaxis] * k \
+ (1.0 - num.cos(angles))[:, num.newaxis, num.newaxis] \
* num.einsum('nij,njk->nik', k, k)
rots = num.einsum('nij,njk->nik', rots, self._rots_a)
m33s = num.einsum('nij,nj,nkj->nik', rots, evals, rots)
return m33s_to_m6s(m33s)
def events_to_m6s(events):
m6s = num.zeros((len(events), 6))
has_mt = num.zeros(len(events), dtype=bool)
for i, ev in enumerate(events):
if ev.moment_tensor:
m6s[i, :] = beachball.deco_part(
ev.moment_tensor, 'deviatoric').m6()
has_mt[i] = True
return m6s, has_mt
def events_to_locations(events):
lats = oa_to_array(events, 'effective_lat')
lons = oa_to_array(events, 'effective_lon')
depths = oa_to_array(events, 'depth')
return lats, lons, depths
[docs]
class CatalogInterpolator(object):
'''
Interpolate between two solutions for the same events.
Events in the secondary catalog are associated with events in the primary
catalog by event name. Events without counterpart are left unchanged.
'''
def __init__(self, events_a, events_b):
name_to_event_b = {}
for ev in events_b:
if ev.name:
if ev.name in name_to_event_b:
logger.warning(
'Duplicate event name in secondary catalog: %s'
% ev.name)
else:
name_to_event_b[ev.name] = ev
events_b_matched = []
nmatched = 0
for ev in events_a:
ev_b = name_to_event_b.get(ev.name, None) if ev.name else None
if ev_b is None:
events_b_matched.append(ev)
else:
events_b_matched.append(ev_b)
nmatched += 1
logger.info(
'Matched %i of %i event%s with secondary catalog.' % (
nmatched, len(events_a), '' if len(events_a) == 1 else 's'))
events_b = events_b_matched
self._lats_a, self._lons_a, self._depths_a = \
events_to_locations(events_a)
lats_b, lons_b, depths_b = events_to_locations(events_b)
self._dlats = lats_b - self._lats_a
self._dlons = (lons_b - self._lons_a + 180.) % 360. - 180.
self._ddepths = depths_b - self._depths_a
self._times_a = oa_to_array(events_a, 'time')
self._dtimes = oa_to_array(events_b, 'time') - self._times_a
self._magnitudes_a = num.array(
[ev.magnitude for ev in events_a], dtype=float)
magnitudes_b = num.array(
[ev.magnitude for ev in events_b], dtype=float)
magnitudes_b[num.isnan(magnitudes_b)] = \
self._magnitudes_a[num.isnan(magnitudes_b)]
self._magnitudes_a[num.isnan(self._magnitudes_a)] = \
magnitudes_b[num.isnan(self._magnitudes_a)]
self._dmagnitudes = magnitudes_b - self._magnitudes_a
m6s_a, has_mt_a = events_to_m6s(events_a)
m6s_b, has_mt_b = events_to_m6s(events_b)
if num.any(has_mt_a) or num.any(has_mt_b):
m6s_a[~has_mt_a] = m6s_b[~has_mt_a]
m6s_b[~has_mt_b] = m6s_a[~has_mt_b]
has_mt = has_mt_a | has_mt_b
m6s_a[~has_mt, 0:3] = 1.0
m6s_b[~has_mt, 0:3] = 1.0
self._m6s_a = m6s_a
self._m6_interpolator = MomentTensorInterpolator(m6s_a, m6s_b)
else:
self._m6s_a = None
self._m6_interpolator = None
tab_a = events_to_table(events_a)
self._extra_cols = [
(name, tab_a.get_col(name))
for name in tab_a.get_col_names(sub_headers=False)
if name.startswith('tag_') or name.startswith('extra_')]
def get_table(self, x):
nevents = self._times_a.size
c5 = num.zeros((nevents, 5))
c5[:, 0] = self._lats_a + x * self._dlats
c5[:, 1] = self._lons_a + x * self._dlons
c5[:, 4] = self._depths_a + x * self._ddepths
tab = table.Table()
ev_rec = table.EventRecipe()
tab.add_recipe(ev_rec)
tab.add_col(ev_rec.get_header('c5'), c5)
tab.add_col(
ev_rec.get_header('time'), self._times_a + x * self._dtimes)
tab.add_col(
ev_rec.get_header('magnitude'),
self._magnitudes_a + x * self._dmagnitudes)
for name, values in self._extra_cols:
tab.add_col(table.Header(name), values)
if self._m6_interpolator is not None:
mt_rec = table.MomentTensorRecipe()
tab.add_recipe(mt_rec)
if x == 0.0:
m6s = self._m6s_a
else:
m6s = self._m6_interpolator(x)
tab.add_col(mt_rec.m6_header, m6s)
return tab
[docs]
class CatalogState(TableState):
selection = CatalogSelection.T(optional=True)
secondary_selection = CatalogSelection.T(optional=True)
interpolation = Float.T(default=0.0)
size_parameter = String.T(default='magnitude', optional=True)
color_parameter = String.T(default='depth', optional=True)
@classmethod
def get_name(self):
return 'Catalog'
def create(self):
element = CatalogElement()
return element
class CatalogElement(TableElement):
def __init__(self, *args, **kwargs):
TableElement.__init__(self, *args, **kwargs)
self._selection_view = None
self._secondary_selection_view = None
self._interpolation_view = None
self._events = None
self._interpolator = None
self._interpolation_slider = None
# self._himesh = HiMesh(order=6)
# cpt_data = [
# (0.0, 0.0, 0.0, 0.0),
# (1.0, 0.9, 0.9, 0.2)]
#
# self.cpt_mesh = automap.CPT(
# levels=[
# automap.CPTLevel(
# vmin=a[0],
# vmax=b[0],
# color_min=[255*x for x in a[1:]],
# color_max=[255*x for x in b[1:]])
# for (a, b) in zip(cpt_data[:-1], cpt_data[1:])])
def get_name(self):
return 'Catalog'
def get_size_parameter_extra_entries(self):
return ['magnitude']
def get_color_parameter_extra_entries(self):
return ['depth']
def bind_state(self, state):
TableElement.bind_state(self, state)
self.talkie_connect(
state,
['selection', 'secondary_selection', 'interpolation'],
self.update)
def update(self, *args):
state = self._state
# ifaces = None
selection_changed = self._selection_view is not state.selection
if selection_changed:
if state.selection is not None:
self._events = state.selection.get_events()
else:
self._events = None
self._selection_view = state.selection
# ifaces = self._himesh.points_to_faces(self._table.get_col('xyz'))
if selection_changed \
or self._secondary_selection_view \
is not state.secondary_selection:
self._interpolator = None
if state.secondary_selection is not None:
if self._events is None:
logger.warning(
'Interpolation between catalogs requires event '
'based primary catalog.')
else:
events_b = state.secondary_selection.get_events()
if events_b is None:
logger.warning(
'Interpolation between catalogs requires event '
'based secondary catalog.')
else:
self._interpolator = CatalogInterpolator(
self._events, events_b)
self._secondary_selection_view = state.secondary_selection
if state.selection is not None:
if self._interpolator is not None:
self.set_table(
self._interpolator.get_table(state.interpolation))
elif self._events is not None:
self.set_table(events_to_table(self._events))
else:
self.set_table(state.selection.get_table())
self._interpolation_view = state.interpolation
self._update_interpolation_controls()
elif self._interpolator is not None \
and self._interpolation_view != state.interpolation:
self.set_table(
self._interpolator.get_table(state.interpolation),
update_controls=False)
self._interpolation_view = state.interpolation
TableElement.update(self, *args)
# if ifaces is not None:
# ifaces_x, sizes = binned_statistic(
# ifaces, ifaces, lambda part: part.shape[0])
#
# vertices = self._himesh.get_vertices()
# # vertices *= 0.95
# faces = self._himesh.get_faces()
#
# values = num.zeros(faces.shape[0])
# values[ifaces_x] = num.log(1+sizes)
#
# self._mesh = TrimeshPipe(vertices, faces, values=values)
# cpt = copy.deepcopy(self.cpt_mesh)
# cpt.scale(num.min(values), num.max(values))
# self._mesh.set_cpt(cpt)
# self._mesh.set_opacity(0.5)
# self._parent.add_actor(self._mesh.actor)
def _update_interpolation_controls(self):
if self._interpolation_slider is not None:
self._interpolation_slider.setEnabled(
self._interpolator is not None)
def open_file_dialog(self, target='selection'):
caption = 'Select one or more files to open'
fns, _ = qw.QFileDialog.getOpenFileNames(
self._parent, caption)
if fns:
setattr(self._state, target, FileCatalogSelection(
paths=[str(fn) for fn in fns]))
def clear_secondary_selection(self):
self._state.secondary_selection = None
def open_catalog_load_dialog(self, target='selection'):
dialog = qw.QDialog(self._parent)
dialog.setWindowTitle('Get events from online catalog.')
layout = qw.QHBoxLayout(dialog)
layout.addWidget(qw.QLabel('Site'))
cb = qw.QComboBox()
for i, s in enumerate(g_sites):
cb.insertItem(i, s)
layout.addWidget(cb)
pb = qw.QPushButton('Cancel')
pb.clicked.connect(dialog.reject)
layout.addWidget(pb)
pb = qw.QPushButton('Ok')
pb.clicked.connect(dialog.accept)
layout.addWidget(pb)
dialog.exec_()
site = str(cb.currentText())
vstate = self._parent.state
if dialog.result() == qw.QDialog.Accepted:
setattr(self._state, target, OnlineCatalogSelection(
site=site,
tmin=vstate.tmin,
tmax=vstate.tmax))
def _get_table_widgets_start(self):
return 3 # used as y arg in addWidget calls
def _get_controls(self):
if not self._controls:
from ..state import state_bind_slider
frame = TableElement._get_controls(self) # sets self._controls
layout = frame.layout()
for iy, (label, target) in enumerate([
('Load from', 'selection'),
('Secondary', 'secondary_selection')]):
layout.addWidget(qw.QLabel(label), iy, 0)
pb = qw.QPushButton('File')
layout.addWidget(pb, iy, 1)
pb.clicked.connect(
lambda _, target=target: self.open_file_dialog(target))
pb = qw.QPushButton('Online Catalog')
layout.addWidget(pb, iy, 2)
pb.clicked.connect(
lambda _, target=target: self.open_catalog_load_dialog(
target))
layout.addWidget(qw.QLabel('Interpolation'), 2, 0)
slider = qw.QSlider(qc.Qt.Horizontal)
slider.setSizePolicy(
qw.QSizePolicy(
qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
slider.setMinimum(0)
slider.setMaximum(1000)
layout.addWidget(slider, 2, 1)
state_bind_slider(
self, self._state, 'interpolation', slider, factor=0.001)
pb = qw.QPushButton('Clear Secondary')
layout.addWidget(pb, 2, 2)
pb.clicked.connect(self.clear_secondary_selection)
self._interpolation_slider = slider
self._update_interpolation_controls()
return self._controls
__all__ = [
'CatalogSelection',
'FileCatalogSelection',
'MemoryCatalogSelection',
'CatalogElement',
'CatalogState',
]