Source code for pyrocko.squirrel.client.catalog

# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------

'''
Squirrel online earthquake catalog client.
'''

import os.path as op
import logging
import time
try:
    import cPickle as pickle
except ImportError:
    import pickle

from pyrocko import util, progress
from pyrocko.guts import String, Dict, Duration, dump_all

from .base import Source
from ..model import ehash, g_tmin, g_tmax
from ..lock import LockDir
from pyrocko import has_paths

guts_prefix = 'squirrel'

logger = logging.getLogger('psq.client.catalog')






[docs] class NoSuchCatalog(Exception): ''' Raised when an unknown catalog name is requested, see :py:func:`get_catalog`. '''
[docs] def get_catalog(name): ''' Get online catalog client by name. :param name: Catalog name, one of ``'geofon'``, ``'gcmt'``, ``'isc'`` or ``'usgs'``. :type name: str :raises NoSuchCatalog: If ``name`` is not one of the known catalogs. ''' if name == 'geofon': from pyrocko.client.geofon import Geofon return Geofon() elif name == 'gcmt': from pyrocko.client.globalcmt import GlobalCMT return GlobalCMT() elif name == 'gcmt_ndk': from pyrocko.client.globalcmt import GlobalCMTNDK return GlobalCMTNDK() elif name == 'isc': from pyrocko.client.isc import ISC return ISC() elif name == 'usgs': from pyrocko.client.usgs import USGS return USGS() else: raise NoSuchCatalog(name)
[docs] class CatalogSource(Source): ''' Squirrel data-source to transparently access online earthquake catalogs. The catalog source maintains and synchronizes a partial copy of the online catalog, e.g. of all events above a certain magnitude. The time span for which the local copy of the catalog should be up-to date is maintained automatically be Squirrel. Data is loaded and updated in chunks as needed in a just-in-time fashion. Data validity can optionally expire after a given period of time and new data can be treated to be preliminary. In both cases information will be refreshed as needed. ''' catalog = String.T( help='Catalog name.') query_args = Dict.T( String.T(), String.T(), optional=True, help='Common arguments, which are appended to all queries, e.g. to ' 'constrain location, depth or magnitude ranges.') expires = Duration.T( optional=True, help='Expiration time [s]. Information older than this will be ' 'refreshed, i.e. queried again.') anxious = Duration.T( optional=True, help='Anxiety period [s]. Information will be treated as preliminary ' 'if it was younger than this at the time of its retrieval. ' 'Preliminary information is refreshed on each query relevant ' 'to it.') cache_path = has_paths.Path.T( optional=True, help='Directory path where the partial local copy of the catalog is ' "kept. By default the Squirrel environment's cache directory is " 'used.') def __init__(self, catalog, query_args=None, **kwargs): ''' Create catalog data source. Arguments are as for the Guts fields above. ''' Source.__init__(self, catalog=catalog, query_args=query_args, **kwargs) self._hash = self.make_hash() self._nevents_query_hint = 1000 self._nevents_chunk_hint = 5000 self._tquery = 3600.*24. self._tquery_limits = (3600., 3600.*24.*365.) self._cache_path = None
[docs] def get_cache_path(self): ''' Get path to this source's on-disk cache directory. :rtype: str ''' return self._cache_path
[docs] def describe(self): ''' Get short identifying description of the source. :rtype: str ''' return 'catalog:%s:%s' % (self.catalog, self.get_hash())
[docs] def setup(self, squirrel, check=True, upgrade=False): ''' Set up cache directory and add already-downloaded local events to the squirrel. See :py:meth:`~pyrocko.squirrel.client.base.Source.setup` for the parameters. ''' self._force_query_age_max = self.anxious self._catalog = get_catalog(self.catalog) self._cache_path = op.join( self.expand_path(self.cache_path) if self.cache_path else squirrel._cache_path, 'catalog', self.get_hash()) util.ensuredir(self._cache_path) with LockDir(self._cache_path): self._load_chain() self._add_events_to_squirrel(squirrel)
[docs] def make_hash(self): ''' Compute hash identifying this source's configuration. Used to distinguish between differently-configured :py:class:`CatalogSource` instances pointing at the same :py:attr:`catalog` when choosing on-disk cache locations. :rtype: str ''' s = self.catalog if self.query_args is not None: s += ','.join( '%s:%s' % (k, self.query_args[k]) for k in sorted(self.query_args.keys())) else: s += 'noqueryargs' return ehash(s)
[docs] def get_hash(self): ''' Get hash identifying this source's configuration (see :py:meth:`make_hash`). :rtype: str ''' return self._hash
[docs] def update_event_inventory(self, squirrel, constraint=None): ''' Query and cache events for a given constraint, refreshing any locally mirrored time spans found outdated (see :py:attr:`expires`/:py:attr:`anxious`). :param squirrel: Squirrel instance the source is attached to. :type squirrel: :py:class:`~pyrocko.squirrel.base.Squirrel` :param constraint: Selection of times to be brought up to date. Required; if its :py:attr:`~pyrocko.squirrel.client.base.Constraint.tmin`/ ``tmax`` are unset, the current data holdings' time span is used instead. :type constraint: :py:class:`~pyrocko.squirrel.client.base.Constraint` ''' with LockDir(self._cache_path): self._load_chain() assert constraint is not None if constraint is not None: tmin, tmax = constraint.tmin, constraint.tmax tmin_sq, tmax_sq = squirrel.get_time_span(dummy_limits=False) if tmin is None: tmin = tmin_sq if tmax is None: tmax = tmax_sq if tmin == g_tmin: tmin = None if tmax == g_tmax: tmax = None if tmin is None or tmax is None: logger.warning( 'Cannot query catalog source "%s" without time ' 'constraint. Could not determine appropriate time ' 'constraint from current data holdings (no data?).' % self.catalog) return if tmin >= tmax: tmax = tmin tnow = time.time() modified = False if tmax > tnow: tmax = tnow chain = [] this_tmin = tmin for link in self._chain: if this_tmin < link.tmin and tmax > this_tmin: chain.append(Link(this_tmin, min(tmax, link.tmin), tnow)) modified = True chain.append(link) this_tmin = link.tmax if this_tmin < tmax: chain.append(Link(this_tmin, tmax, tnow)) modified = True if modified: self._chain = chain chain = [] remove = [] for link in self._chain: if tmin < link.tmax and link.tmin < tmax \ and self._outdated(link, tnow): if link.content_id: remove.append( self._get_events_file_path(link.content_id)) tmin_query = max(link.tmin, tmin) tmax_query = min(link.tmax, tmax) if link.tmin < tmin_query: chain.append(Link(link.tmin, tmin_query, tnow)) if tmin_query < tmax_query: for link in self._iquery(tmin_query, tmax_query, tnow): chain.append(link) if tmax_query < link.tmax: chain.append(Link(tmax_query, link.tmax, tnow)) modified = True else: chain.append(link) if modified: self._chain = chain self._dump_chain() squirrel.remove(remove) self._add_events_to_squirrel(squirrel)
def _add_events_to_squirrel(self, squirrel): add = [] for link in self._chain: if link.content_id: add.append(self._get_events_file_path(link.content_id)) squirrel.add(add, kinds=['event'], format='yaml') def _iquery(self, tmin, tmax, tmodified): nwant = self._nevents_query_hint tlim = self._tquery_limits t = tmin tpack_min = tmin events = [] with progress.task( 'Querying %s' % self.catalog, 100, logger=logger) as task: while t < tmax: tmin_query = t tmax_query = min(t + self._tquery, tmax) events_new = self._query(tmin_query, tmax_query) nevents_new = len(events_new) events.extend(events_new) while len(events) > int(self._nevents_chunk_hint * 1.5): tpack_max = events[self._nevents_chunk_hint].time yield self._pack( events[:self._nevents_chunk_hint], tpack_min, tpack_max, tmodified) tpack_min = tpack_max events[:self._nevents_chunk_hint] = [] t += self._tquery if tmax_query != tmax: if nevents_new < 5: self._tquery *= 10.0 elif not (nwant // 2 < nevents_new < nwant * 2): self._tquery /= float(nevents_new) / float(nwant) self._tquery = max(tlim[0], min(self._tquery, tlim[1])) task.update(int(round(100. * (t-tmin)/(tmax-tmin)))) if self._force_query_age_max is not None: tsplit = tmodified - self._force_query_age_max if tpack_min < tsplit < tmax: events_older = [] events_newer = [] for ev in events: if ev.time < tsplit: events_older.append(ev) else: events_newer.append(ev) yield self._pack( events_older, tpack_min, tsplit, tmodified) yield self._pack( events_newer, tsplit, tmax, tmodified) return yield self._pack(events, tpack_min, tmax, tmodified) def _pack(self, events, tmin, tmax, tmodified): if events: content_id = ehash( self.get_hash() + ' %r %r %r' % (tmin, tmax, tmodified)) path = self._get_events_file_path(content_id) dump_all(events, filename=path) else: content_id = None return Link(tmin, tmax, tmodified, len(events), content_id)
[docs] def query_args_typed(self): ''' Get :py:attr:`query_args` with known numeric keys (magnitude, latitude, longitude and depth ranges) converted from ``str`` to ``float``. :rtype: dict ''' if self.query_args is None: return {} else: type_map = { 'magmin': float, 'magmax': float, 'latmin': float, 'latmax': float, 'lonmin': float, 'lonmax': float, 'depthmin': float, 'depthmax': float} return dict( (k, type_map.get(k, str)(v)) for (k, v) in self.query_args.items())
def _query(self, tmin, tmax): logger.info('Querying catalog "%s" for time span %s - %s.' % ( self.catalog, util.tts(tmin), util.tts(tmax))) return self._catalog.get_events( (tmin, tmax), **self.query_args_typed()) def _outdated(self, link, tnow): if link.nevents == -1: return True if self._force_query_age_max \ and link.tmax + self._force_query_age_max > link.tmodified: return True if self.expires is not None \ and link.tmodified < tnow - self.expires: return True return False def _get_events_file_path(self, fhash): return op.join(self._cache_path, fhash + '.pf') def _get_chain_file_path(self): return op.join(self._cache_path, 'chain.pickle') def _load_chain(self): path = self._get_chain_file_path() if op.exists(path): with open(path, 'rb') as f: self._chain = pickle.load(f) else: self._chain = [] def _dump_chain(self): with open(self._get_chain_file_path(), 'wb') as f: pickle.dump(self._chain, f, protocol=2)
__all__ = [ 'CatalogSource' ]