1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Data model and content types handled by the Squirrel framework.
9Squirrel uses flat content types to represent waveform, station, channel,
10response, event, and a few other objects. A common subset of the information in
11these objects is indexed in the database, currently: kind, codes, time interval
12and sampling rate. The :py:class:`Nut` objects encapsulate this information
13together with information about where and how to get the associated bulk data.
15Further content types are defined here to handle waveform orders, waveform
16promises, data coverage and sensor information.
17'''
19from __future__ import absolute_import, print_function
21import re
22import fnmatch
23import hashlib
24import numpy as num
25from os import urandom
26from base64 import urlsafe_b64encode
27from collections import defaultdict, namedtuple
29from pyrocko import util
30from pyrocko.guts import Object, SObject, String, Timestamp, Float, Int, \
31 Unicode, Tuple, List, StringChoice, Any
32from pyrocko.model import squirrel_content, Location
33from pyrocko.response import FrequencyResponse, MultiplyResponse, \
34 IntegrationResponse, DifferentiationResponse, simplify_responses, \
35 FrequencyResponseCheckpoint
37from .error import ConversionError, SquirrelError
40guts_prefix = 'squirrel'
43g_codes_pool = {}
46class CodesError(SquirrelError):
47 pass
50class Codes(SObject):
51 pass
54def normalize_nslce(*args, **kwargs):
55 if args and kwargs:
56 raise ValueError('Either *args or **kwargs accepted, not both.')
58 if len(args) == 1:
59 if isinstance(args[0], str):
60 args = tuple(args[0].split('.'))
61 elif isinstance(args[0], tuple):
62 args = args[0]
63 else:
64 raise ValueError('Invalid argument type: %s' % type(args[0]))
66 nargs = len(args)
67 if nargs == 5:
68 t = args
70 elif nargs == 4:
71 t = args + ('',)
73 elif nargs == 0:
74 d = dict(
75 network='',
76 station='',
77 location='',
78 channel='',
79 extra='')
81 d.update(kwargs)
82 t = tuple(kwargs.get(k, '') for k in (
83 'network', 'station', 'location', 'channel', 'extra'))
85 else:
86 raise CodesError(
87 'Does not match NSLC or NSLCE codes pattern: %s' % '.'.join(args))
89 if '.'.join(t).count('.') != 4:
90 raise CodesError(
91 'Codes may not contain a ".": "%s", "%s", "%s", "%s", "%s"' % t)
93 return t
96CodesNSLCEBase = namedtuple(
97 'CodesNSLCEBase', [
98 'network', 'station', 'location', 'channel', 'extra'])
101class CodesNSLCE(CodesNSLCEBase, Codes):
102 '''
103 Codes denominating a seismic channel (NSLC or NSLCE).
105 FDSN/SEED style NET.STA.LOC.CHA is accepted or NET.STA.LOC.CHA.EXTRA, where
106 the EXTRA part in the latter form can be used to identify a custom
107 processing applied to a channel.
108 '''
110 __slots__ = ()
111 __hash__ = CodesNSLCEBase.__hash__
113 as_dict = CodesNSLCEBase._asdict
115 def __new__(cls, *args, safe_str=None, **kwargs):
116 nargs = len(args)
117 if nargs == 1 and isinstance(args[0], CodesNSLCE):
118 return args[0]
119 elif nargs == 1 and isinstance(args[0], CodesNSL):
120 t = (args[0].tuple) + ('*', '*')
121 elif nargs == 1 and isinstance(args[0], CodesX):
122 t = ('*', '*', '*', '*', '*')
123 elif safe_str is not None:
124 t = safe_str.split('.')
125 else:
126 t = normalize_nslce(*args, **kwargs)
128 x = CodesNSLCEBase.__new__(cls, *t)
129 return g_codes_pool.setdefault(x, x)
131 def __init__(self, *args, **kwargs):
132 Codes.__init__(self)
134 def __str__(self):
135 if self.extra == '':
136 return '.'.join(self[:-1])
137 else:
138 return '.'.join(self)
140 def __eq__(self, other):
141 if not isinstance(other, CodesNSLCE):
142 other = CodesNSLCE(other)
144 return CodesNSLCEBase.__eq__(self, other)
146 @property
147 def safe_str(self):
148 return '.'.join(self)
150 @property
151 def nslce(self):
152 return self[:4]
154 @property
155 def nslc(self):
156 return self[:4]
158 @property
159 def nsl(self):
160 return self[:3]
162 @property
163 def ns(self):
164 return self[:2]
166 def as_tuple(self):
167 return tuple(self)
169 def replace(self, **kwargs):
170 x = CodesNSLCEBase._replace(self, **kwargs)
171 return g_codes_pool.setdefault(x, x)
174def normalize_nsl(*args, **kwargs):
175 if args and kwargs:
176 raise ValueError('Either *args or **kwargs accepted, not both.')
178 if len(args) == 1:
179 if isinstance(args[0], str):
180 args = tuple(args[0].split('.'))
181 elif isinstance(args[0], tuple):
182 args = args[0]
183 else:
184 raise ValueError('Invalid argument type: %s' % type(args[0]))
186 nargs = len(args)
187 if nargs == 3:
188 t = args
190 elif nargs == 0:
191 d = dict(
192 network='',
193 station='',
194 location='')
196 d.update(kwargs)
197 t = tuple(kwargs.get(k, '') for k in (
198 'network', 'station', 'location'))
200 else:
201 raise CodesError(
202 'Does not match NSL codes pattern: %s' % '.'.join(args))
204 if '.'.join(t).count('.') != 2:
205 raise CodesError(
206 'Codes may not contain a ".": "%s", "%s", "%s"' % t)
208 return t
211CodesNSLBase = namedtuple(
212 'CodesNSLBase', [
213 'network', 'station', 'location'])
216class CodesNSL(CodesNSLBase, Codes):
217 '''
218 Codes denominating a seismic station (NSL).
220 NET.STA.LOC is accepted, slightly different from SEED/StationXML, where
221 LOC is part of the channel. By setting location='*' is possible to get
222 compatible behaviour in most cases.
223 '''
225 __slots__ = ()
226 __hash__ = CodesNSLBase.__hash__
228 as_dict = CodesNSLBase._asdict
230 def __new__(cls, *args, safe_str=None, **kwargs):
231 nargs = len(args)
232 if nargs == 1 and isinstance(args[0], CodesNSL):
233 return args[0]
234 elif nargs == 1 and isinstance(args[0], CodesNSLCE):
235 t = args[0].nsl
236 elif nargs == 1 and isinstance(args[0], CodesX):
237 t = ('*', '*', '*')
238 elif safe_str is not None:
239 t = safe_str.split('.')
240 else:
241 t = normalize_nsl(*args, **kwargs)
243 x = CodesNSLBase.__new__(cls, *t)
244 return g_codes_pool.setdefault(x, x)
246 def __init__(self, *args, **kwargs):
247 Codes.__init__(self)
249 def __str__(self):
250 return '.'.join(self)
252 def __eq__(self, other):
253 if not isinstance(other, CodesNSL):
254 other = CodesNSL(other)
256 return CodesNSLBase.__eq__(self, other)
258 @property
259 def safe_str(self):
260 return '.'.join(self)
262 @property
263 def ns(self):
264 return self[:2]
266 @property
267 def nsl(self):
268 return self[:3]
270 def as_tuple(self):
271 return tuple(self)
273 def replace(self, **kwargs):
274 x = CodesNSLBase._replace(self, **kwargs)
275 return g_codes_pool.setdefault(x, x)
278CodesXBase = namedtuple(
279 'CodesXBase', [
280 'name'])
283class CodesX(CodesXBase, Codes):
284 '''
285 General purpose codes for anything other than channels or stations.
286 '''
288 __slots__ = ()
289 __hash__ = CodesXBase.__hash__
290 __eq__ = CodesXBase.__eq__
292 as_dict = CodesXBase._asdict
294 def __new__(cls, name='', safe_str=None):
295 if isinstance(name, CodesX):
296 return name
297 elif isinstance(name, (CodesNSLCE, CodesNSL)):
298 name = '*'
299 elif safe_str is not None:
300 name = safe_str
301 else:
302 if '.' in name:
303 raise CodesError('Code may not contain a ".": %s' % name)
305 x = CodesXBase.__new__(cls, name)
306 return g_codes_pool.setdefault(x, x)
308 def __init__(self, *args, **kwargs):
309 Codes.__init__(self)
311 def __str__(self):
312 return '.'.join(self)
314 @property
315 def safe_str(self):
316 return '.'.join(self)
318 @property
319 def ns(self):
320 return self[:2]
322 def as_tuple(self):
323 return tuple(self)
325 def replace(self, **kwargs):
326 x = CodesXBase._replace(self, **kwargs)
327 return g_codes_pool.setdefault(x, x)
330g_codes_patterns = {}
333def match_codes(pattern, codes):
334 spattern = pattern.safe_str
335 scodes = codes.safe_str
336 if spattern not in g_codes_patterns:
337 rpattern = re.compile(fnmatch.translate(spattern), re.I)
338 g_codes_patterns[spattern] = rpattern
340 rpattern = g_codes_patterns[spattern]
341 return bool(rpattern.match(scodes))
344g_content_kinds = [
345 'undefined',
346 'waveform',
347 'station',
348 'channel',
349 'response',
350 'event',
351 'waveform_promise']
354g_codes_classes = [
355 CodesX,
356 CodesNSLCE,
357 CodesNSL,
358 CodesNSLCE,
359 CodesNSLCE,
360 CodesX,
361 CodesNSLCE]
364def to_codes_simple(kind_id, codes_safe_str):
365 return g_codes_classes[kind_id](safe_str=codes_safe_str)
368def to_codes(kind_id, obj):
369 return g_codes_classes[kind_id](obj)
372g_content_kind_ids = (
373 UNDEFINED, WAVEFORM, STATION, CHANNEL, RESPONSE, EVENT,
374 WAVEFORM_PROMISE) = range(len(g_content_kinds))
377g_tmin, g_tmax = util.get_working_system_time_range()[:2]
380try:
381 g_tmin_queries = max(g_tmin, util.str_to_time_fillup('1900-01-01'))
382except Exception:
383 g_tmin_queries = g_tmin
386def to_kind(kind_id):
387 return g_content_kinds[kind_id]
390def to_kinds(kind_ids):
391 return [g_content_kinds[kind_id] for kind_id in kind_ids]
394def to_kind_id(kind):
395 return g_content_kinds.index(kind)
398def to_kind_ids(kinds):
399 return [g_content_kinds.index(kind) for kind in kinds]
402g_kind_mask_all = 0xff
405def to_kind_mask(kinds):
406 if kinds:
407 return sum(1 << kind_id for kind_id in to_kind_ids(kinds))
408 else:
409 return g_kind_mask_all
412def str_or_none(x):
413 if x is None:
414 return None
415 else:
416 return str(x)
419def float_or_none(x):
420 if x is None:
421 return None
422 else:
423 return float(x)
426def int_or_none(x):
427 if x is None:
428 return None
429 else:
430 return int(x)
433def int_or_g_tmin(x):
434 if x is None:
435 return g_tmin
436 else:
437 return int(x)
440def int_or_g_tmax(x):
441 if x is None:
442 return g_tmax
443 else:
444 return int(x)
447def tmin_or_none(x):
448 if x == g_tmin:
449 return None
450 else:
451 return x
454def tmax_or_none(x):
455 if x == g_tmax:
456 return None
457 else:
458 return x
461def time_or_none_to_str(x):
462 if x is None:
463 return '...'.ljust(17)
464 else:
465 return util.time_to_str(x)
468def codes_to_str_abbreviated(codes, indent=' '):
469 codes = [str(x) for x in codes]
471 if len(codes) > 20:
472 scodes = '\n' + util.ewrap(codes[:10], indent=indent) \
473 + '\n%s[%i more]\n' % (indent, len(codes) - 20) \
474 + util.ewrap(codes[-10:], indent=' ')
475 else:
476 scodes = '\n' + util.ewrap(codes, indent=indent) \
477 if codes else '<none>'
479 return scodes
482g_offset_time_unit_inv = 1000000000
483g_offset_time_unit = 1.0 / g_offset_time_unit_inv
486def tsplit(t):
487 if t is None:
488 return None, 0.0
490 t = util.to_time_float(t)
491 if type(t) is float:
492 t = round(t, 5)
493 else:
494 t = round(t, 9)
496 seconds = num.floor(t)
497 offset = t - seconds
498 return int(seconds), int(round(offset * g_offset_time_unit_inv))
501def tjoin(seconds, offset):
502 if seconds is None:
503 return None
505 return util.to_time_float(seconds) \
506 + util.to_time_float(offset*g_offset_time_unit)
509tscale_min = 1
510tscale_max = 365 * 24 * 3600 # last edge is one above
511tscale_logbase = 20
513tscale_edges = [tscale_min]
514while True:
515 tscale_edges.append(tscale_edges[-1]*tscale_logbase)
516 if tscale_edges[-1] >= tscale_max:
517 break
520tscale_edges = num.array(tscale_edges)
523def tscale_to_kscale(tscale):
525 # 0 <= x < tscale_edges[1]: 0
526 # tscale_edges[1] <= x < tscale_edges[2]: 1
527 # ...
528 # tscale_edges[len(tscale_edges)-1] <= x: len(tscale_edges)
530 return int(num.searchsorted(tscale_edges, tscale))
533@squirrel_content
534class Station(Location):
535 '''
536 A seismic station.
537 '''
539 codes = CodesNSL.T()
541 tmin = Timestamp.T(optional=True)
542 tmax = Timestamp.T(optional=True)
544 description = Unicode.T(optional=True)
546 def __init__(self, **kwargs):
547 kwargs['codes'] = CodesNSL(kwargs['codes'])
548 Location.__init__(self, **kwargs)
550 @property
551 def time_span(self):
552 return (self.tmin, self.tmax)
554 def get_pyrocko_station(self):
555 from pyrocko import model
556 return model.Station(*self._get_pyrocko_station_args())
558 def _get_pyrocko_station_args(self):
559 return (
560 self.codes.network,
561 self.codes.station,
562 self.codes.location,
563 self.lat,
564 self.lon,
565 self.elevation,
566 self.depth,
567 self.north_shift,
568 self.east_shift)
571class Sensor(Location):
572 '''
573 Representation of a channel group.
574 '''
576 codes = CodesNSLCE.T()
578 tmin = Timestamp.T(optional=True)
579 tmax = Timestamp.T(optional=True)
581 deltat = Float.T(optional=True)
583 @property
584 def time_span(self):
585 return (self.tmin, self.tmax)
587 def __init__(self, **kwargs):
588 kwargs['codes'] = CodesNSLCE(kwargs['codes'])
589 Location.__init__(self, **kwargs)
591 def _get_sensor_args(self):
592 def getattr_rep(k):
593 if k == 'codes':
594 return self.codes.replace(self.codes[:-1])
595 else:
596 return getattr(self, k)
598 return [getattr_rep(k) for k in self.T.propnames]
600 @classmethod
601 def from_channels(cls, channels):
602 groups = defaultdict(list)
603 for channel in channels:
604 groups[channel._get_sensor_args()].append(channel)
606 return [
607 cls(**dict((k, v) for (k, v) in zip(cls.T.propnames, args)))
608 for args, _ in groups.items()]
610 def _get_pyrocko_station_args(self):
611 return (
612 self.codes.network,
613 self.codes.station,
614 self.codes.location,
615 self.lat,
616 self.lon,
617 self.elevation,
618 self.depth,
619 self.north_shift,
620 self.east_shift)
623@squirrel_content
624class Channel(Sensor):
625 '''
626 A channel of a seismic station.
627 '''
629 dip = Float.T(optional=True)
630 azimuth = Float.T(optional=True)
632 @classmethod
633 def from_channels(cls, channels):
634 raise NotImplementedError()
636 def get_pyrocko_channel(self):
637 from pyrocko import model
638 return model.Channel(*self._get_pyrocko_channel_args())
640 def _get_pyrocko_channel_args(self):
641 return (
642 self.codes.channel,
643 self.azimuth,
644 self.dip)
647observational_quantities = [
648 'acceleration', 'velocity', 'displacement', 'pressure', 'rotation',
649 'temperature']
652technical_quantities = [
653 'voltage', 'counts']
656class QuantityType(StringChoice):
657 '''
658 Choice of observational or technical quantity.
660 SI units are used for all quantities, where applicable.
661 '''
662 choices = observational_quantities + technical_quantities
665class ResponseStage(Object):
666 '''
667 Representation of a response stage.
669 Components of a seismic recording system are represented as a sequence of
670 response stages, e.g. sensor, pre-amplifier, digitizer, digital
671 downsampling.
672 '''
673 input_quantity = QuantityType.T(optional=True)
674 input_sample_rate = Float.T(optional=True)
675 output_quantity = QuantityType.T(optional=True)
676 output_sample_rate = Float.T(optional=True)
677 elements = List.T(FrequencyResponse.T())
678 log = List.T(Tuple.T(3, String.T()))
680 @property
681 def stage_type(self):
682 if self.input_quantity in observational_quantities \
683 and self.output_quantity in observational_quantities:
684 return 'conversion'
686 if self.input_quantity in observational_quantities \
687 and self.output_quantity == 'voltage':
688 return 'sensor'
690 elif self.input_quantity == 'voltage' \
691 and self.output_quantity == 'voltage':
692 return 'electronics'
694 elif self.input_quantity == 'voltage' \
695 and self.output_quantity == 'counts':
696 return 'digitizer'
698 elif self.input_quantity == 'counts' \
699 and self.output_quantity == 'counts' \
700 and self.input_sample_rate != self.output_sample_rate:
701 return 'decimation'
703 elif self.input_quantity in observational_quantities \
704 and self.output_quantity == 'counts':
705 return 'combination'
707 else:
708 return 'unknown'
710 @property
711 def summary(self):
712 irate = self.input_sample_rate
713 orate = self.output_sample_rate
714 factor = None
715 if irate and orate:
716 factor = irate / orate
717 return 'ResponseStage, ' + (
718 '%s%s => %s%s%s' % (
719 self.input_quantity or '?',
720 ' @ %g Hz' % irate if irate else '',
721 self.output_quantity or '?',
722 ' @ %g Hz' % orate if orate else '',
723 ' :%g' % factor if factor else '')
724 )
726 def get_effective(self):
727 return MultiplyResponse(responses=list(self.elements))
730D = 'displacement'
731V = 'velocity'
732A = 'acceleration'
734g_converters = {
735 (V, D): IntegrationResponse(1),
736 (A, D): IntegrationResponse(2),
737 (D, V): DifferentiationResponse(1),
738 (A, V): IntegrationResponse(1),
739 (D, A): DifferentiationResponse(2),
740 (V, A): DifferentiationResponse(1)}
743def response_converters(input_quantity, output_quantity):
744 if input_quantity is None or input_quantity == output_quantity:
745 return []
747 if output_quantity is None:
748 raise ConversionError('Unspecified target quantity.')
750 try:
751 return [g_converters[input_quantity, output_quantity]]
753 except KeyError:
754 raise ConversionError('No rule to convert from "%s" to "%s".' % (
755 input_quantity, output_quantity))
758@squirrel_content
759class Response(Object):
760 '''
761 The instrument response of a seismic station channel.
762 '''
764 codes = CodesNSLCE.T()
765 tmin = Timestamp.T(optional=True)
766 tmax = Timestamp.T(optional=True)
768 stages = List.T(ResponseStage.T())
769 checkpoints = List.T(FrequencyResponseCheckpoint.T())
771 deltat = Float.T(optional=True)
772 log = List.T(Tuple.T(3, String.T()))
774 def __init__(self, **kwargs):
775 kwargs['codes'] = CodesNSLCE(kwargs['codes'])
776 Object.__init__(self, **kwargs)
778 @property
779 def time_span(self):
780 return (self.tmin, self.tmax)
782 @property
783 def nstages(self):
784 return len(self.stages)
786 @property
787 def input_quantity(self):
788 return self.stages[0].input_quantity if self.stages else None
790 @property
791 def output_quantity(self):
792 return self.stages[-1].input_quantity if self.stages else None
794 @property
795 def output_sample_rate(self):
796 return self.stages[-1].output_sample_rate if self.stages else None
798 @property
799 def stages_summary(self):
800 def grouped(xs):
801 xs = list(xs)
802 g = []
803 for i in range(len(xs)):
804 g.append(xs[i])
805 if i+1 < len(xs) and xs[i+1] != xs[i]:
806 yield g
807 g = []
809 if g:
810 yield g
812 return '+'.join(
813 '%s%s' % (g[0], '(%i)' % len(g) if len(g) > 1 else '')
814 for g in grouped(stage.stage_type for stage in self.stages))
816 @property
817 def summary(self):
818 orate = self.output_sample_rate
819 return '%s %-16s %s' % (
820 self.__class__.__name__, self.str_codes, self.str_time_span) \
821 + ', ' + ', '.join((
822 '%s => %s' % (
823 self.input_quantity or '?', self.output_quantity or '?')
824 + (' @ %g Hz' % orate if orate else ''),
825 self.stages_summary,
826 ))
828 def get_effective(self, input_quantity=None):
829 elements = response_converters(input_quantity, self.input_quantity)
831 elements.extend(
832 stage.get_effective() for stage in self.stages)
834 return MultiplyResponse(responses=simplify_responses(elements))
837@squirrel_content
838class Event(Object):
839 '''
840 A seismic event.
841 '''
843 name = String.T(optional=True)
844 time = Timestamp.T()
845 duration = Float.T(optional=True)
847 lat = Float.T()
848 lon = Float.T()
849 depth = Float.T(optional=True)
851 magnitude = Float.T(optional=True)
853 def get_pyrocko_event(self):
854 from pyrocko import model
855 model.Event(
856 name=self.name,
857 time=self.time,
858 lat=self.lat,
859 lon=self.lon,
860 depth=self.depth,
861 magnitude=self.magnitude,
862 duration=self.duration)
864 @property
865 def time_span(self):
866 return (self.time, self.time)
869def ehash(s):
870 return hashlib.sha1(s.encode('utf8')).hexdigest()
873def random_name(n=8):
874 return urlsafe_b64encode(urandom(n)).rstrip(b'=').decode('ascii')
877@squirrel_content
878class WaveformPromise(Object):
879 '''
880 Information about a waveform potentially downloadable from a remote site.
882 In the Squirrel framework, waveform promises are used to permit download of
883 selected waveforms from a remote site. They are typically generated by
884 calls to
885 :py:meth:`~pyrocko.squirrel.base.Squirrel.update_waveform_promises`.
886 Waveform promises are inserted and indexed in the database similar to
887 normal waveforms. When processing a waveform query, e.g. from
888 :py:meth:`~pyrocko.squirrel.base.Squirrel.get_waveform`, and no local
889 waveform is available for the queried time span, a matching promise can be
890 resolved, i.e. an attempt is made to download the waveform from the remote
891 site. The promise is removed after the download attempt (except when a
892 network error occurs). This prevents Squirrel from making unnecessary
893 queries for waveforms missing at the remote site.
894 '''
896 codes = CodesNSLCE.T()
897 tmin = Timestamp.T()
898 tmax = Timestamp.T()
900 deltat = Float.T(optional=True)
902 source_hash = String.T()
904 def __init__(self, **kwargs):
905 kwargs['codes'] = CodesNSLCE(kwargs['codes'])
906 Object.__init__(self, **kwargs)
908 @property
909 def time_span(self):
910 return (self.tmin, self.tmax)
913class InvalidWaveform(Exception):
914 pass
917class WaveformOrder(Object):
918 '''
919 Waveform request information.
920 '''
922 source_id = String.T()
923 codes = CodesNSLCE.T()
924 deltat = Float.T()
925 tmin = Timestamp.T()
926 tmax = Timestamp.T()
927 gaps = List.T(Tuple.T(2, Timestamp.T()))
929 @property
930 def client(self):
931 return self.source_id.split(':')[1]
933 def describe(self, site='?'):
934 return '%s:%s %s [%s - %s]' % (
935 self.client, site, str(self.codes),
936 util.time_to_str(self.tmin), util.time_to_str(self.tmax))
938 def validate(self, tr):
939 if tr.ydata.size == 0:
940 raise InvalidWaveform(
941 'waveform with zero data samples')
943 if tr.deltat != self.deltat:
944 raise InvalidWaveform(
945 'incorrect sampling interval - waveform: %g s, '
946 'meta-data: %g s' % (
947 tr.deltat, self.deltat))
949 if not num.all(num.isfinite(tr.ydata)):
950 raise InvalidWaveform('waveform has NaN values')
953def order_summary(orders):
954 codes_list = sorted(set(order.codes for order in orders))
955 if len(codes_list) > 3:
956 return '%i order%s: %s - %s' % (
957 len(orders),
958 util.plural_s(orders),
959 str(codes_list[0]),
960 str(codes_list[1]))
962 else:
963 return '%i order%s: %s' % (
964 len(orders),
965 util.plural_s(orders),
966 ', '.join(str(codes) for codes in codes_list))
969class Nut(Object):
970 '''
971 Index entry referencing an elementary piece of content.
973 So-called *nuts* are used in Pyrocko's Squirrel framework to hold common
974 meta-information about individual pieces of waveforms, stations, channels,
975 etc. together with the information where it was found or generated.
976 '''
978 file_path = String.T(optional=True)
979 file_format = String.T(optional=True)
980 file_mtime = Timestamp.T(optional=True)
981 file_size = Int.T(optional=True)
983 file_segment = Int.T(optional=True)
984 file_element = Int.T(optional=True)
986 kind_id = Int.T()
987 codes = Codes.T()
989 tmin_seconds = Int.T(default=0)
990 tmin_offset = Int.T(default=0, optional=True)
991 tmax_seconds = Int.T(default=0)
992 tmax_offset = Int.T(default=0, optional=True)
994 deltat = Float.T(default=0.0)
996 content = Any.T(optional=True)
998 content_in_db = False
1000 def __init__(
1001 self,
1002 file_path=None,
1003 file_format=None,
1004 file_mtime=None,
1005 file_size=None,
1006 file_segment=None,
1007 file_element=None,
1008 kind_id=0,
1009 codes=CodesX(''),
1010 tmin_seconds=None,
1011 tmin_offset=0,
1012 tmax_seconds=None,
1013 tmax_offset=0,
1014 deltat=None,
1015 content=None,
1016 tmin=None,
1017 tmax=None,
1018 values_nocheck=None):
1020 if values_nocheck is not None:
1021 (self.file_path, self.file_format, self.file_mtime,
1022 self.file_size,
1023 self.file_segment, self.file_element,
1024 self.kind_id, codes_safe_str,
1025 self.tmin_seconds, self.tmin_offset,
1026 self.tmax_seconds, self.tmax_offset,
1027 self.deltat) = values_nocheck
1029 self.codes = to_codes_simple(self.kind_id, codes_safe_str)
1030 self.content = None
1031 else:
1032 if tmin is not None:
1033 tmin_seconds, tmin_offset = tsplit(tmin)
1035 if tmax is not None:
1036 tmax_seconds, tmax_offset = tsplit(tmax)
1038 self.kind_id = int(kind_id)
1039 self.codes = codes
1040 self.tmin_seconds = int_or_g_tmin(tmin_seconds)
1041 self.tmin_offset = int(tmin_offset)
1042 self.tmax_seconds = int_or_g_tmax(tmax_seconds)
1043 self.tmax_offset = int(tmax_offset)
1044 self.deltat = float_or_none(deltat)
1045 self.file_path = str_or_none(file_path)
1046 self.file_segment = int_or_none(file_segment)
1047 self.file_element = int_or_none(file_element)
1048 self.file_format = str_or_none(file_format)
1049 self.file_mtime = float_or_none(file_mtime)
1050 self.file_size = int_or_none(file_size)
1051 self.content = content
1053 Object.__init__(self, init_props=False)
1055 def __eq__(self, other):
1056 return (isinstance(other, Nut) and
1057 self.equality_values == other.equality_values)
1059 def hash(self):
1060 return ehash(','.join(str(x) for x in self.key))
1062 def __ne__(self, other):
1063 return not (self == other)
1065 def get_io_backend(self):
1066 from . import io
1067 return io.get_backend(self.file_format)
1069 def file_modified(self):
1070 return self.get_io_backend().get_stats(self.file_path) \
1071 != (self.file_mtime, self.file_size)
1073 @property
1074 def dkey(self):
1075 return (self.kind_id, self.tmin_seconds, self.tmin_offset, self.codes)
1077 @property
1078 def key(self):
1079 return (
1080 self.file_path,
1081 self.file_segment,
1082 self.file_element,
1083 self.file_mtime)
1085 @property
1086 def equality_values(self):
1087 return (
1088 self.file_segment, self.file_element,
1089 self.kind_id, self.codes,
1090 self.tmin_seconds, self.tmin_offset,
1091 self.tmax_seconds, self.tmax_offset, self.deltat)
1093 @property
1094 def tmin(self):
1095 return tjoin(self.tmin_seconds, self.tmin_offset)
1097 @tmin.setter
1098 def tmin(self, t):
1099 self.tmin_seconds, self.tmin_offset = tsplit(t)
1101 @property
1102 def tmax(self):
1103 return tjoin(self.tmax_seconds, self.tmax_offset)
1105 @tmax.setter
1106 def tmax(self, t):
1107 self.tmax_seconds, self.tmax_offset = tsplit(t)
1109 @property
1110 def kscale(self):
1111 if self.tmin_seconds is None or self.tmax_seconds is None:
1112 return 0
1113 return tscale_to_kscale(self.tmax_seconds - self.tmin_seconds)
1115 @property
1116 def waveform_kwargs(self):
1117 network, station, location, channel, extra = self.codes
1119 return dict(
1120 network=network,
1121 station=station,
1122 location=location,
1123 channel=channel,
1124 extra=extra,
1125 tmin=self.tmin,
1126 tmax=self.tmax,
1127 deltat=self.deltat)
1129 @property
1130 def waveform_promise_kwargs(self):
1131 return dict(
1132 codes=self.codes,
1133 tmin=self.tmin,
1134 tmax=self.tmax,
1135 deltat=self.deltat)
1137 @property
1138 def station_kwargs(self):
1139 network, station, location = self.codes
1140 return dict(
1141 codes=self.codes,
1142 tmin=tmin_or_none(self.tmin),
1143 tmax=tmax_or_none(self.tmax))
1145 @property
1146 def channel_kwargs(self):
1147 network, station, location, channel, extra = self.codes
1148 return dict(
1149 codes=self.codes,
1150 tmin=tmin_or_none(self.tmin),
1151 tmax=tmax_or_none(self.tmax),
1152 deltat=self.deltat)
1154 @property
1155 def response_kwargs(self):
1156 return dict(
1157 codes=self.codes,
1158 tmin=tmin_or_none(self.tmin),
1159 tmax=tmax_or_none(self.tmax),
1160 deltat=self.deltat)
1162 @property
1163 def event_kwargs(self):
1164 return dict(
1165 name=self.codes,
1166 time=self.tmin,
1167 duration=(self.tmax - self.tmin) or None)
1169 @property
1170 def trace_kwargs(self):
1171 network, station, location, channel, extra = self.codes
1173 return dict(
1174 network=network,
1175 station=station,
1176 location=location,
1177 channel=channel,
1178 extra=extra,
1179 tmin=self.tmin,
1180 tmax=self.tmax-self.deltat,
1181 deltat=self.deltat)
1183 @property
1184 def dummy_trace(self):
1185 return DummyTrace(self)
1187 @property
1188 def summary(self):
1189 if self.tmin == self.tmax:
1190 ts = util.time_to_str(self.tmin)
1191 else:
1192 ts = '%s - %s' % (
1193 util.time_to_str(self.tmin),
1194 util.time_to_str(self.tmax))
1196 return ' '.join((
1197 ('%s,' % to_kind(self.kind_id)).ljust(9),
1198 ('%s,' % str(self.codes)).ljust(18),
1199 ts))
1202def make_waveform_nut(**kwargs):
1203 return Nut(kind_id=WAVEFORM, **kwargs)
1206def make_waveform_promise_nut(**kwargs):
1207 return Nut(kind_id=WAVEFORM_PROMISE, **kwargs)
1210def make_station_nut(**kwargs):
1211 return Nut(kind_id=STATION, **kwargs)
1214def make_channel_nut(**kwargs):
1215 return Nut(kind_id=CHANNEL, **kwargs)
1218def make_response_nut(**kwargs):
1219 return Nut(kind_id=RESPONSE, **kwargs)
1222def make_event_nut(**kwargs):
1223 return Nut(kind_id=EVENT, **kwargs)
1226def group_channels(nuts):
1227 by_ansl = {}
1228 for nut in nuts:
1229 if nut.kind_id != CHANNEL:
1230 continue
1232 ansl = nut.codes[:4]
1234 if ansl not in by_ansl:
1235 by_ansl[ansl] = {}
1237 group = by_ansl[ansl]
1239 k = nut.codes[4][:-1], nut.deltat, nut.tmin, nut.tmax
1241 if k not in group:
1242 group[k] = set()
1244 group.add(nut.codes[4])
1246 return by_ansl
1249class DummyTrace(object):
1251 def __init__(self, nut):
1252 self.nut = nut
1253 self.codes = nut.codes
1254 self.meta = {}
1256 @property
1257 def tmin(self):
1258 return self.nut.tmin
1260 @property
1261 def tmax(self):
1262 return self.nut.tmax
1264 @property
1265 def deltat(self):
1266 return self.nut.deltat
1268 @property
1269 def nslc_id(self):
1270 return self.codes.nslc
1272 @property
1273 def network(self):
1274 return self.codes.network
1276 @property
1277 def station(self):
1278 return self.codes.station
1280 @property
1281 def location(self):
1282 return self.codes.location
1284 @property
1285 def channel(self):
1286 return self.codes.channel
1288 @property
1289 def extra(self):
1290 return self.codes.extra
1292 def overlaps(self, tmin, tmax):
1293 return not (tmax < self.nut.tmin or self.nut.tmax < tmin)
1296def duration_to_str(t):
1297 if t > 24*3600:
1298 return '%gd' % (t / (24.*3600.))
1299 elif t > 3600:
1300 return '%gh' % (t / 3600.)
1301 elif t > 60:
1302 return '%gm' % (t / 60.)
1303 else:
1304 return '%gs' % t
1307class Coverage(Object):
1308 '''
1309 Information about times covered by a waveform or other time series data.
1310 '''
1311 kind_id = Int.T(
1312 help='Content type.')
1313 pattern = Codes.T(
1314 help='The codes pattern in the request, which caused this entry to '
1315 'match.')
1316 codes = Codes.T(
1317 help='NSLCE or NSL codes identifier of the time series.')
1318 deltat = Float.T(
1319 help='Sampling interval [s]',
1320 optional=True)
1321 tmin = Timestamp.T(
1322 help='Global start time of time series.',
1323 optional=True)
1324 tmax = Timestamp.T(
1325 help='Global end time of time series.',
1326 optional=True)
1327 changes = List.T(
1328 Tuple.T(2, Any.T()),
1329 help='List of change points, with entries of the form '
1330 '``(time, count)``, where a ``count`` of zero indicates start of '
1331 'a gap, a value of 1 start of normal data coverage and a higher '
1332 'value duplicate or redundant data coverage.')
1334 @classmethod
1335 def from_values(cls, args):
1336 pattern, codes, deltat, tmin, tmax, changes, kind_id = args
1337 return cls(
1338 kind_id=kind_id,
1339 pattern=pattern,
1340 codes=codes,
1341 deltat=deltat,
1342 tmin=tmin,
1343 tmax=tmax,
1344 changes=changes)
1346 @property
1347 def summary(self):
1348 ts = '%s - %s,' % (
1349 util.time_to_str(self.tmin),
1350 util.time_to_str(self.tmax))
1352 srate = self.sample_rate
1354 return ' '.join((
1355 ('%s,' % to_kind(self.kind_id)).ljust(9),
1356 ('%s,' % str(self.codes)).ljust(18),
1357 ts,
1358 '%10.3g,' % srate if srate else '',
1359 '%4i' % len(self.changes),
1360 '%s' % duration_to_str(self.total)))
1362 @property
1363 def sample_rate(self):
1364 if self.deltat is None:
1365 return None
1366 elif self.deltat == 0.0:
1367 return 0.0
1368 else:
1369 return 1.0 / self.deltat
1371 @property
1372 def labels(self):
1373 srate = self.sample_rate
1374 return (
1375 ('%s' % str(self.codes)),
1376 '%.3g' % srate if srate else '')
1378 @property
1379 def total(self):
1380 total_t = None
1381 for tmin, tmax, _ in self.iter_spans():
1382 total_t = (total_t or 0.0) + (tmax - tmin)
1384 return total_t
1386 def iter_spans(self):
1387 last = None
1388 for (t, count) in self.changes:
1389 if last is not None:
1390 last_t, last_count = last
1391 if last_count > 0:
1392 yield last_t, t, last_count
1394 last = (t, count)
1397__all__ = [
1398 'to_kind',
1399 'to_kinds',
1400 'to_kind_id',
1401 'to_kind_ids',
1402 'CodesError',
1403 'Codes',
1404 'CodesNSLCE',
1405 'CodesNSL',
1406 'CodesX',
1407 'Station',
1408 'Channel',
1409 'Sensor',
1410 'Response',
1411 'Nut',
1412 'Coverage',
1413 'WaveformPromise',
1414]