Source code for pyrocko.squirrel.storage

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

'''
Waveform data storage utilities.
'''

import math
import os
from collections import defaultdict

from pyrocko import guts, util, trace, io, carpet
from pyrocko.io import rug
from pyrocko.plot import nice_time_tick_inc_approx_secs

from . import error

guts_prefix = 'squirrel'

nsamples_block = 100000
nsamples_segment = 1024**2


def _translate_path_template(s):
    map_name = {
        'year': '%(wmin_year)s',
        'month': '%(wmin_month)s',
        'day': '%(wmin_day)s',
        'jday': '%(wmin_jday)s',
        'hour': '%(wmin_hour)s',
        'minute': '%(wmin_minute)s',
        'second': '%(wmin_second)s',
        'net': '%(network_dsafe)s',
        'sta': '%(station_dsafe)s',
        'loc': '%(location_dsafe)s',
        'cha': '%(channel_dsafe)s',
        'ext': '%(extra_dsafe)s'}

    return os.path.join(*[
        '.'.join(map_name[x] for x in entry.split('.'))
        for entry in s.split('/')])


[docs] def time_to_template_vars(prefix, t): ''' Get dict of path-template substitution variables for a time instant. :param prefix: Prefix to prepend to each generated variable name (e.g. ``'wmin'`` or ``'wmax'``). :type prefix: str :param t: Time instant to be converted. :type t: :py:func:`~pyrocko.util.get_time_float` :returns: Dict with keys ``'<prefix>_year'``, ``'<prefix>_month'``, ``'<prefix>_day'``, ``'<prefix>_jday'``, ``'<prefix>_hour'``, ``'<prefix>_minute'``, ``'<prefix>_second'`` and (for backwards compatibility) ``'<prefix>'``. :rtype: dict ''' d = dict(zip( [prefix + '_' + var for var in [ 'year', 'month', 'day', 'jday', 'hour', 'minute', 'second']], util.time_to_str(t, '%Y.%m.%d.%j.%H.%M.%S').split('.'))) # needed for backwards compatibility d[prefix] = util.time_to_str(t, '%Y-%m-%d_%H-%M-%S') return d
[docs] def iter_windows(tmin, tmax, tinc, tinc_nonuniform): ''' Iterate over time windows covering a given time span. :param tmin: Start of the time span to be covered. :type tmin: :py:func:`~pyrocko.util.get_time_float` :param tmax: End of the time span to be covered. :type tmax: :py:func:`~pyrocko.util.get_time_float` :param tinc: Window length [s], or ``None`` to yield a single window covering ``[tmin, tmax]``. :type tinc: float :param tinc_nonuniform: If given, ``tinc`` is ignored and windows follow non-uniform calendar boundaries instead: ``'month'`` or ``'year'``. :type tinc_nonuniform: str :yields: ``(wmin, wmax)`` tuples. :raises SquirrelError: If ``tinc_nonuniform`` is set to an unsupported value. ''' if tinc is None: yield tmin, tmax elif tinc_nonuniform is None: tmin = math.floor(tmin / tinc) * tinc t = tmin while t < tmax: yield t, t + tinc t += tinc elif tinc_nonuniform == 'month': yield from util.iter_months(tmin, tmax) elif tinc_nonuniform == 'year': yield from util.iter_years(tmin, tmax) else: raise error.SquirrelError( 'Available non-uniform time intervals: month, year. ' 'Invalid choice: %s' % tinc_nonuniform)
[docs] class StorageSchemeLayout(guts.Object): ''' Specific directory layout within a storage scheme. ''' name = guts.String.T( help='Name of the layout, informational.') time_increment = guts.Float.T( optional=True, help='Time window length stored in each file[s]. Exact or ' 'approximate, depending on ' ':py:gattr:`time_increment_nonuniform`.') time_increment_nonuniform = guts.String.T( optional=True, help='Identifier for non-uniform time windows. E.g. ``\'month\'`` or ' '``\'year\'``.') path_template = guts.String.T( help='Template for file paths.')
[docs] def get_additional(self, wmin, wmax): ''' Get path-template substitution variables for a time window. :param wmin: Start of the window. :type wmin: :py:func:`~pyrocko.util.get_time_float` :param wmax: End of the window. :type wmax: :py:func:`~pyrocko.util.get_time_float` :returns: Combined ``'wmin_*'`` and ``'wmax_*'`` variables, see :py:func:`time_to_template_vars`. :rtype: dict ''' d = {} d.update(time_to_template_vars('wmin', wmin)) d.update(time_to_template_vars('wmax', wmax)) return d
[docs] @classmethod def describe_header(self): ''' Get column header line matching :py:meth:`describe`'s output, for display purposes. :rtype: str ''' return '%8s %11s %6s %10s %12s %10s %6s %4s %8s %6s' % ( 'rate', 'deltat', 'tblock', '', 'tsegment', '', 'layout', 'fseg', 'fsize', 'levels')
[docs] def describe(self, deltat): ''' Get one-line summary of file sizing for a given sampling interval, for display purposes (see :py:meth:`describe_header` for the matching column header). :param deltat: Sampling interval [s] to describe file sizing for. :type deltat: float :rtype: str ''' rate = 1.0 / deltat tblock = nice_time_tick_inc_approx_secs(deltat * nsamples_block) tsegment = deltat * nsamples_segment segments_per_file = self.time_increment / tsegment \ if self.time_increment is not None else 0.0, bytesize = (self.time_increment or 0.0) / deltat * 4 return '%8.1f %11.5f %6.0f %10s %12.0f %10s %6s %4.0f %8s %6i' % ( rate, deltat, tblock, guts.str_duration(tblock), tsegment, guts.str_duration(tsegment), self.name, segments_per_file, util.human_bytesize(bytesize), len(self.path_template.split('/')))
[docs] class StorageScheme(guts.Object): ''' Storage scheme for waveform archive data. ''' name = guts.String.T( help='Storage scheme name.') layouts = guts.List.T( StorageSchemeLayout.T(), help='Directory layouts supported by the scheme.') min_segments_per_file = guts.Float.T( default=1.0, help='Target minimum number of segments to be stored in each file.') format = guts.String.T( default='mseed', help='File format of waveform data files.') description = guts.String.T( default='', help='Description of the storage scheme.') mseed_record_length = guts.IntChoice.T( optional=True, choices=list(io.mseed.VALID_RECORD_LENGTHS), help='MiniSEED record length [bytes] to use when saving. ' 'Default: 4096.') mseed_steim = guts.IntChoice.T( optional=True, choices=[1, 2], help='MiniSEED STEIM compression variant to use when saving, ' '``1`` or ``2``. Default: 2.')
[docs] def post_init(self): ''' Guts post-initialization hook, setting up the (unset) base path. ''' self._base_path = None
[docs] def set_base_path(self, base_path): ''' Set base directory prepended to layout path templates. :param base_path: Base directory path, or ``None`` to use path templates as-is. :type base_path: str ''' self._base_path = base_path
[docs] def select_layout(self, deltat): ''' Select the most appropriate directory layout for a given sampling interval. :param deltat: Sampling interval [s] to select a layout for. :type deltat: float :returns: The first of :py:attr:`layouts` whose :py:attr:`~StorageSchemeLayout.time_increment` is large enough to hold :py:attr:`min_segments_per_file` segments at the given sampling interval (or has no time increment restriction), falling back to the last layout. :rtype: :py:class:`StorageSchemeLayout` ''' tsegment = deltat * nsamples_segment twant = tsegment * self.min_segments_per_file for layout in self.layouts: if layout.time_increment is None or layout.time_increment > twant: return layout return layout
def save( self, items, **kwargs): if isinstance(items, (trace.Trace, carpet.Carpet)): items = [items] if self.format == 'rug': if not all(isinstance(item, carpet.Carpet) for item in items): raise error.SquirrelError( 'Items to be stored with StorageScheme.save must be ' 'of type `Carpet` when "rug" format is selected.') return self.save_carpets( items, **kwargs) else: traces = [] for item in items: if isinstance(item, carpet.Carpet): traces.extend(item.get_traces()) else: assert isinstance(item, trace.Trace) traces.append(item) return self.save_traces( traces, **kwargs) def save_traces( self, traces, tmin=None, tmax=None, overwrite=False, check_append_hook=None, check_append_merge=False): if isinstance(traces, trace.Trace): traces = [traces] save_kwargs = dict( append=self.format in io.g_formats_supporting_append, check_append=True, format=self.format, overwrite=overwrite, check_append_hook=check_append_hook, check_append_merge=check_append_merge) if self.format == 'mseed': save_kwargs.update( record_length=self.mseed_record_length or 4096, steim=self.mseed_steim or 2) by_deltat = defaultdict(list) for tr in traces: by_deltat[tr.deltat].append(tr) file_names = set() for deltat, traces_group in by_deltat.items(): layout = self.select_layout(deltat) traces_group.sort(key=lambda tr: tr.full_id) traces_group = trace.deoverlap(traces_group) if tmin is None: tmin = min(tr.tmin for tr in traces_group) if tmax is None: tmax = max(tr.tmax for tr in traces_group) + deltat for wmin, wmax in iter_windows( tmin, tmax, layout.time_increment, layout.time_increment_nonuniform): traces_window = [] for tr in traces_group: try: traces_window.append( tr.chop(wmin, wmax, inplace=False)) except trace.NoData: pass additional = layout.get_additional(wmin, wmax) file_names.update(io.save( traces_window, layout.path_template if self._base_path is None else os.path.join(self._base_path, layout.path_template), additional=additional, **save_kwargs)) return sorted(file_names) def save_carpets( self, carpets, tmin=None, tmax=None, overwrite=False, check_append_hook=None, check_append_merge=False): if isinstance(carpets, carpet.Carpet): carpets = [carpets] by_deltat = defaultdict(list) for cp in carpets: by_deltat[cp.deltat].append(cp) save_kwargs = dict( append=True, check_append=True, overwrite=overwrite, check_append_merge=check_append_merge, check_append_hook=check_append_hook) file_names = set() for deltat, carpet_group in by_deltat.items(): layout = self.select_layout(deltat) carpet_group.sort(key=lambda cp: cp.codes) carpet_group = carpet.deoverlap(carpet_group) tmin_group = tmin if tmin is not None else \ min(cp.tmin for cp in carpet_group) tmax_group = tmax if tmax is not None else \ max(cp.tmax for cp in carpet_group) + deltat for wmin, wmax in iter_windows( tmin_group, tmax_group, layout.time_increment, layout.time_increment_nonuniform): carpet_window = [] for cp in carpet_group: try: carpet_window.append(cp.chop(wmin, wmax)) except trace.NoData: pass additional = layout.get_additional(wmin, wmax) file_names.update(rug.save( carpet_window, layout.path_template if self._base_path is None else os.path.join(self._base_path, layout.path_template), additional=additional, **save_kwargs)) return sorted(file_names)
_g_schemes_list = [] _g_schemes_list.append(StorageScheme( name='default', description='Dynamic storage scheme with balanced file sizes of ' '10 - 400 MB and a balanced directory hierarchy of 4-6 levels', layouts=[ StorageSchemeLayout( name=name, time_increment=time_increment, time_increment_nonuniform=time_increment_nonuniform, path_template=_translate_path_template(path_template)) for (name, time_increment, time_increment_nonuniform, path_template) in [ ('second', 1.0, None, 'net/sta/loc.cha/year/month/day/hour/net.sta.loc.cha.ext.year.month.day.hour.minute.second'), # noqa ('minute', 60.0, None,'net/sta/loc.cha/year/month/day/net.sta.loc.cha.ext.year.month.day.hour.minute'), # noqa ('hour', 3600.0, None,'net/sta/loc.cha/year/month/net.sta.loc.cha.ext.year.month.day.hour'), # noqa ('day', 86400.0, None,'net/sta/loc.cha/year/net.sta.loc.cha.ext.year.month.day'), # noqa ('month', 2628000.0, 'month','net/sta/loc.cha/year/net.sta.loc.cha.ext.year.month'), # noqa ('year', 31536000.0, 'year', 'net/sta/loc.cha/net.sta.loc.cha.ext.year')]], # noqa min_segments_per_file=1.5)) _g_schemes_list.extend(StorageScheme( name='rug-store-%i' % factor, description='Storage scheme used to store carpets with typically %i ' 'components as rug files.', format='rug', layouts=[ StorageSchemeLayout( name=name, time_increment=time_increment, time_increment_nonuniform=time_increment_nonuniform, path_template=_translate_path_template(path_template)) for (name, time_increment, time_increment_nonuniform, path_template) in [ ('second', 1.0, None, 'net/sta/loc.cha/year/month/day/hour/net.sta.loc.cha.ext.year.month.day.hour.minute.second'), # noqa ('minute', 60.0, None,'net/sta/loc.cha/year/month/day/net.sta.loc.cha.ext.year.month.day.hour.minute'), # noqa ('hour', 3600.0, None,'net/sta/loc.cha/year/month/net.sta.loc.cha.ext.year.month.day.hour'), # noqa ('day', 86400.0, None,'net/sta/loc.cha/year/net.sta.loc.cha.ext.year.month.day'), # noqa ('month', 2628000.0, 'month','net/sta/loc.cha/year/net.sta.loc.cha.ext.year.month'), # noqa ('year', 31536000.0, 'year', 'net/sta/loc.cha/net.sta.loc.cha.ext.year')]], # noqa min_segments_per_file=1.5/factor) for factor in [10, 100, 1000]) _g_schemes_list.append(StorageScheme( name='sds', description='Directory scheme conforming to SeisComP Data Structure (SDS) ' 'archive format (https://www.seiscomp.de/seiscomp3/doc' '/applications/slarchive/SDS.html). The scheme has a fixed ' 'layout with day files.', layouts=[ StorageSchemeLayout( name='sds', time_increment=24*3600., path_template=os.path.join( '%(wmin_year)s', '%(network_safe)s', '%(station_safe)s', '%(channel_safe)s.D', '%(network_safe)s.%(station)s.%(location)s.%(channel)s.D' '.%(wmin_year)s.%(wmin_jday)s'))])) g_schemes = dict((scheme.name, scheme) for scheme in _g_schemes_list)
[docs] def get_storage_scheme(name='default'): ''' Get a fresh copy of a named built-in storage scheme. :param name: Name of the storage scheme, see :py:class:`StorageSchemeChoice`. :type name: str :rtype: :py:class:`StorageScheme` ''' return guts.clone(g_schemes[name])
[docs] class StorageSchemeChoice(guts.StringChoice): ''' Name of a supported storage scheme. ''' choices = list(g_schemes.keys())
__all__ = [ 'get_storage_scheme', 'StorageScheme', 'StorageSchemeLayout', 'StorageSchemeChoice' ]