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 hashlib
22import numpy as num
23from os import urandom
24from base64 import urlsafe_b64encode
25from collections import defaultdict
27from pyrocko import util
28from pyrocko.guts import Object, String, Timestamp, Float, Int, Unicode, \
29 Tuple, List, StringChoice, Any
30from pyrocko.model import Content
31from pyrocko.response import FrequencyResponse, MultiplyResponse, \
32 IntegrationResponse, DifferentiationResponse, simplify_responses, \
33 FrequencyResponseCheckpoint
35from .error import ConversionError
38guts_prefix = 'squirrel'
40separator = '\t'
42g_content_kinds = [
43 'undefined',
44 'waveform',
45 'station',
46 'channel',
47 'response',
48 'event',
49 'waveform_promise']
52g_content_kind_ids = (
53 UNDEFINED, WAVEFORM, STATION, CHANNEL, RESPONSE, EVENT,
54 WAVEFORM_PROMISE) = range(len(g_content_kinds))
56g_tmin, g_tmax = util.get_working_system_time_range()[:2]
59try:
60 g_tmin_queries = max(g_tmin, util.str_to_time_fillup('1900-01-01'))
61except Exception:
62 g_tmin_queries = g_tmin
65def to_kind(kind_id):
66 return g_content_kinds[kind_id]
69def to_kinds(kind_ids):
70 return [g_content_kinds[kind_id] for kind_id in kind_ids]
73def to_kind_id(kind):
74 return g_content_kinds.index(kind)
77def to_kind_ids(kinds):
78 return [g_content_kinds.index(kind) for kind in kinds]
81g_kind_mask_all = 0xff
84def to_kind_mask(kinds):
85 if kinds:
86 return sum(1 << kind_id for kind_id in to_kind_ids(kinds))
87 else:
88 return g_kind_mask_all
91def str_or_none(x):
92 if x is None:
93 return None
94 else:
95 return str(x)
98def float_or_none(x):
99 if x is None:
100 return None
101 else:
102 return float(x)
105def int_or_none(x):
106 if x is None:
107 return None
108 else:
109 return int(x)
112def int_or_g_tmin(x):
113 if x is None:
114 return g_tmin
115 else:
116 return int(x)
119def int_or_g_tmax(x):
120 if x is None:
121 return g_tmax
122 else:
123 return int(x)
126def tmin_or_none(x):
127 if x == g_tmin:
128 return None
129 else:
130 return x
133def tmax_or_none(x):
134 if x == g_tmax:
135 return None
136 else:
137 return x
140def time_or_none_to_str(x):
141 if x is None:
142 return '...'.ljust(17)
143 else:
144 return util.time_to_str(x)
147def codes_to_str_abbreviated(codes, indent=' '):
148 codes = ['.'.join(x) for x in codes]
150 if len(codes) > 20:
151 scodes = '\n' + util.ewrap(codes[:10], indent=indent) \
152 + '\n%s[%i more]\n' % (indent, len(codes) - 20) \
153 + util.ewrap(codes[-10:], indent=' ')
154 else:
155 scodes = '\n' + util.ewrap(codes, indent=indent) \
156 if codes else '<none>'
158 return scodes
161g_offset_time_unit_inv = 1000000000
162g_offset_time_unit = 1.0 / g_offset_time_unit_inv
165def tsplit(t):
166 if t is None:
167 return None, 0.0
169 t = util.to_time_float(t)
170 if type(t) is float:
171 t = round(t, 5)
172 else:
173 t = round(t, 9)
175 seconds = num.floor(t)
176 offset = t - seconds
177 return int(seconds), int(round(offset * g_offset_time_unit_inv))
180def tjoin(seconds, offset):
181 if seconds is None:
182 return None
184 return util.to_time_float(seconds) \
185 + util.to_time_float(offset*g_offset_time_unit)
188tscale_min = 1
189tscale_max = 365 * 24 * 3600 # last edge is one above
190tscale_logbase = 20
192tscale_edges = [tscale_min]
193while True:
194 tscale_edges.append(tscale_edges[-1]*tscale_logbase)
195 if tscale_edges[-1] >= tscale_max:
196 break
199tscale_edges = num.array(tscale_edges)
202def tscale_to_kscale(tscale):
204 # 0 <= x < tscale_edges[1]: 0
205 # tscale_edges[1] <= x < tscale_edges[2]: 1
206 # ...
207 # tscale_edges[len(tscale_edges)-1] <= x: len(tscale_edges)
209 return int(num.searchsorted(tscale_edges, tscale))
212class Station(Content):
213 '''
214 A seismic station.
215 '''
217 agency = String.T(default='', help='Agency code (2-5)')
218 network = String.T(default='', help='Deployment/network code (1-8)')
219 station = String.T(default='', help='Station code (1-5)')
220 location = String.T(default='', optional=True, help='Location code (0-2)')
222 tmin = Timestamp.T(optional=True)
223 tmax = Timestamp.T(optional=True)
225 lat = Float.T()
226 lon = Float.T()
227 elevation = Float.T(optional=True)
228 depth = Float.T(optional=True)
230 description = Unicode.T(optional=True)
232 @property
233 def codes(self):
234 return (
235 self.agency, self.network, self.station,
236 self.location if self.location is not None else '*')
238 @property
239 def time_span(self):
240 return (self.tmin, self.tmax)
242 def get_pyrocko_station(self):
243 from pyrocko import model
244 return model.Station(
245 network=self.network,
246 station=self.station,
247 location=self.location if self.location is not None else '*',
248 lat=self.lat,
249 lon=self.lon,
250 elevation=self.elevation,
251 depth=self.depth)
253 def _get_pyrocko_station_args(self):
254 return (
255 '*',
256 self.network,
257 self.station,
258 self.location if self.location is not None else '*',
259 self.lat,
260 self.lon,
261 self.elevation,
262 self.depth)
265class Channel(Content):
266 '''
267 A channel of a seismic station.
268 '''
270 agency = String.T(default='', help='Agency code (2-5)')
271 network = String.T(default='', help='Deployment/network code (1-8)')
272 station = String.T(default='', help='Station code (1-5)')
273 location = String.T(default='', help='Location code (0-2)')
274 channel = String.T(default='', help='Channel code (3)')
275 extra = String.T(default='', help='Extra/custom code')
277 tmin = Timestamp.T(optional=True)
278 tmax = Timestamp.T(optional=True)
280 lat = Float.T()
281 lon = Float.T()
282 elevation = Float.T(optional=True)
283 depth = Float.T(optional=True)
285 dip = Float.T(optional=True)
286 azimuth = Float.T(optional=True)
287 deltat = Float.T(optional=True)
289 @property
290 def codes(self):
291 return (
292 self.agency, self.network, self.station, self.location,
293 self.channel, self.extra)
295 def set_codes(
296 self, agency=None, network=None, station=None, location=None,
297 channel=None, extra=None):
299 if agency is not None:
300 self.agency = agency
301 if network is not None:
302 self.network = network
303 if station is not None:
304 self.station = station
305 if location is not None:
306 self.location = location
307 if channel is not None:
308 self.channel = channel
309 if extra is not None:
310 self.extra = extra
312 @property
313 def time_span(self):
314 return (self.tmin, self.tmax)
316 def get_pyrocko_channel(self):
317 from pyrocko import model
318 return model.Channel(
319 name=self.channel,
320 azimuth=self.azimuth,
321 dip=self.dip)
323 def _get_pyrocko_station_args(self):
324 return (
325 self.channel,
326 self.network,
327 self.station,
328 self.location,
329 self.lat,
330 self.lon,
331 self.elevation,
332 self.depth)
334 def _get_pyrocko_channel_args(self):
335 return (
336 '*',
337 self.channel,
338 self.azimuth,
339 self.dip)
341 def _get_sensor_args(self):
342 return (
343 self.agency,
344 self.network,
345 self.station,
346 self.location,
347 self.channel[:-1] + '?',
348 self.extra,
349 self.lat,
350 self.lon,
351 self.elevation,
352 self.depth,
353 self.deltat,
354 self.tmin,
355 self.tmax)
358class Sensor(Channel):
359 '''
360 Representation of a channel group.
361 '''
363 def grouping(self, channel):
364 return channel._get_sensor_args()
366 @classmethod
367 def from_channels(cls, channels):
368 groups = defaultdict(list)
369 for channel in channels:
370 groups[channel._get_sensor_args()].append(channel)
372 return [cls(
373 agency=args[0],
374 network=args[1],
375 station=args[2],
376 location=args[3],
377 channel=args[4],
378 extra=args[5],
379 lat=args[6],
380 lon=args[7],
381 elevation=args[8],
382 depth=args[9],
383 deltat=args[10],
384 tmin=args[11],
385 tmax=args[12])
386 for args, _ in groups.items()]
389observational_quantities = [
390 'acceleration', 'velocity', 'displacement', 'pressure', 'rotation',
391 'temperature']
394technical_quantities = [
395 'voltage', 'counts']
398class QuantityType(StringChoice):
399 '''
400 Choice of observational or technical quantity.
402 SI units are used for all quantities, where applicable.
403 '''
404 choices = observational_quantities + technical_quantities
407class ResponseStage(Object):
408 '''
409 Representation of a response stage.
411 Components of a seismic recording system are represented as a sequence of
412 response stages, e.g. sensor, pre-amplifier, digitizer, digital
413 downsampling.
414 '''
415 input_quantity = QuantityType.T(optional=True)
416 input_sample_rate = Float.T(optional=True)
417 output_quantity = QuantityType.T(optional=True)
418 output_sample_rate = Float.T(optional=True)
419 elements = List.T(FrequencyResponse.T())
420 log = List.T(Tuple.T(3, String.T()))
422 @property
423 def stage_type(self):
424 if self.input_quantity in observational_quantities \
425 and self.output_quantity in observational_quantities:
426 return 'conversion'
428 if self.input_quantity in observational_quantities \
429 and self.output_quantity == 'voltage':
430 return 'sensor'
432 elif self.input_quantity == 'voltage' \
433 and self.output_quantity == 'voltage':
434 return 'electronics'
436 elif self.input_quantity == 'voltage' \
437 and self.output_quantity == 'counts':
438 return 'digitizer'
440 elif self.input_quantity == 'counts' \
441 and self.output_quantity == 'counts' \
442 and self.input_sample_rate != self.output_sample_rate:
443 return 'decimation'
445 elif self.input_quantity in observational_quantities \
446 and self.output_quantity == 'counts':
447 return 'combination'
449 else:
450 return 'unknown'
452 @property
453 def summary(self):
454 irate = self.input_sample_rate
455 orate = self.output_sample_rate
456 factor = None
457 if irate and orate:
458 factor = irate / orate
459 return 'ResponseStage, ' + (
460 '%s%s => %s%s%s' % (
461 self.input_quantity or '?',
462 ' @ %g Hz' % irate if irate else '',
463 self.output_quantity or '?',
464 ' @ %g Hz' % orate if orate else '',
465 ' :%g' % factor if factor else '')
466 )
468 def get_effective(self):
469 return MultiplyResponse(responses=list(self.elements))
472D = 'displacement'
473V = 'velocity'
474A = 'acceleration'
476g_converters = {
477 (V, D): IntegrationResponse(1),
478 (A, D): IntegrationResponse(2),
479 (D, V): DifferentiationResponse(1),
480 (A, V): IntegrationResponse(1),
481 (D, A): DifferentiationResponse(2),
482 (V, A): DifferentiationResponse(1)}
485def response_converters(input_quantity, output_quantity):
486 if input_quantity is None or input_quantity == output_quantity:
487 return []
489 if output_quantity is None:
490 raise ConversionError('Unspecified target quantity.')
492 try:
493 return [g_converters[input_quantity, output_quantity]]
495 except KeyError:
496 raise ConversionError('No rule to convert from "%s" to "%s".' % (
497 input_quantity, output_quantity))
500class Response(Content):
501 '''
502 The instrument response of a seismic station channel.
503 '''
505 agency = String.T(default='', help='Agency code (2-5)')
506 network = String.T(default='', help='Deployment/network code (1-8)')
507 station = String.T(default='', help='Station code (1-5)')
508 location = String.T(default='', help='Location code (0-2)')
509 channel = String.T(default='', help='Channel code (3)')
510 extra = String.T(default='', help='Extra/custom code')
512 tmin = Timestamp.T(optional=True)
513 tmax = Timestamp.T(optional=True)
515 stages = List.T(ResponseStage.T())
516 checkpoints = List.T(FrequencyResponseCheckpoint.T())
518 deltat = Float.T(optional=True)
519 log = List.T(Tuple.T(3, String.T()))
521 @property
522 def codes(self):
523 return (
524 self.agency, self.network, self.station, self.location,
525 self.channel, self.extra)
527 @property
528 def time_span(self):
529 return (self.tmin, self.tmax)
531 @property
532 def nstages(self):
533 return len(self.stages)
535 @property
536 def input_quantity(self):
537 return self.stages[0].input_quantity if self.stages else None
539 @property
540 def output_quantity(self):
541 return self.stages[-1].input_quantity if self.stages else None
543 @property
544 def output_sample_rate(self):
545 return self.stages[-1].output_sample_rate if self.stages else None
547 @property
548 def stages_summary(self):
549 def grouped(xs):
550 xs = list(xs)
551 g = []
552 for i in range(len(xs)):
553 g.append(xs[i])
554 if i+1 < len(xs) and xs[i+1] != xs[i]:
555 yield g
556 g = []
558 if g:
559 yield g
561 return '+'.join(
562 '%s%s' % (g[0], '(%i)' % len(g) if len(g) > 1 else '')
563 for g in grouped(stage.stage_type for stage in self.stages))
565 @property
566 def summary(self):
567 orate = self.output_sample_rate
568 return Content.summary.fget(self) + ', ' + ', '.join((
569 '%s => %s' % (
570 self.input_quantity or '?', self.output_quantity or '?')
571 + (' @ %g Hz' % orate if orate else ''),
572 self.stages_summary,
573 ))
575 def get_effective(self, input_quantity=None):
576 elements = response_converters(input_quantity, self.input_quantity)
578 elements.extend(
579 stage.get_effective() for stage in self.stages)
581 return MultiplyResponse(responses=simplify_responses(elements))
584class Event(Content):
585 '''
586 A seismic event.
587 '''
589 name = String.T(optional=True)
590 time = Timestamp.T()
591 duration = Float.T(optional=True)
593 lat = Float.T()
594 lon = Float.T()
595 depth = Float.T(optional=True)
597 magnitude = Float.T(optional=True)
599 def get_pyrocko_event(self):
600 from pyrocko import model
601 model.Event(
602 name=self.name,
603 time=self.time,
604 lat=self.lat,
605 lon=self.lon,
606 depth=self.depth,
607 magnitude=self.magnitude,
608 duration=self.duration)
610 @property
611 def time_span(self):
612 return (self.time, self.time)
615def ehash(s):
616 return hashlib.sha1(s.encode('utf8')).hexdigest()
619def random_name(n=8):
620 return urlsafe_b64encode(urandom(n)).rstrip(b'=').decode('ascii')
623class WaveformPromise(Content):
624 '''
625 Information about a waveform potentially downloadable from a remote site.
627 In the Squirrel framework, waveform promises are used to permit download of
628 selected waveforms from a remote site. They are typically generated by
629 calls to
630 :py:meth:`~pyrocko.squirrel.base.Squirrel.update_waveform_promises`.
631 Waveform promises are inserted and indexed in the database similar to
632 normal waveforms. When processing a waveform query, e.g. from
633 :py:meth:`~pyrocko.squirrel.base.Squirrel.get_waveform`, and no local
634 waveform is available for the queried time span, a matching promise can be
635 resolved, i.e. an attempt is made to download the waveform from the remote
636 site. The promise is removed after the download attempt (except when a
637 network error occurs). This prevents Squirrel from making unnecessary
638 queries for waveforms missing at the remote site.
639 '''
641 agency = String.T(default='', help='Agency code (2-5)')
642 network = String.T(default='', help='Deployment/network code (1-8)')
643 station = String.T(default='', help='Station code (1-5)')
644 location = String.T(default='', help='Location code (0-2)')
645 channel = String.T(default='', help='Channel code (3)')
646 extra = String.T(default='', help='Extra/custom code')
648 tmin = Timestamp.T()
649 tmax = Timestamp.T()
651 deltat = Float.T(optional=True)
653 source_hash = String.T()
655 @property
656 def codes(self):
657 return (
658 self.agency, self.network, self.station, self.location,
659 self.channel, self.extra)
661 @property
662 def time_span(self):
663 return (self.tmin, self.tmax)
666class InvalidWaveform(Exception):
667 pass
670class WaveformOrder(Object):
671 '''
672 Waveform request information.
673 '''
675 source_id = String.T()
676 codes = Tuple.T(None, String.T())
677 deltat = Float.T()
678 tmin = Timestamp.T()
679 tmax = Timestamp.T()
680 gaps = List.T(Tuple.T(2, Timestamp.T()))
682 @property
683 def client(self):
684 return self.source_id.split(':')[1]
686 def describe(self, site='?'):
687 return '%s:%s %s [%s - %s]' % (
688 self.client, site, '.'.join(self.codes),
689 util.time_to_str(self.tmin), util.time_to_str(self.tmax))
691 def validate(self, tr):
692 if tr.ydata.size == 0:
693 raise InvalidWaveform(
694 'waveform with zero data samples')
696 if tr.deltat != self.deltat:
697 raise InvalidWaveform(
698 'incorrect sampling interval - waveform: %g s, '
699 'meta-data: %g s' % (
700 tr.deltat, self.deltat))
702 if not num.all(num.isfinite(tr.ydata)):
703 raise InvalidWaveform('waveform has NaN values')
706def order_summary(orders):
707 codes = sorted(set(order.codes[1:-1] for order in orders))
708 if len(codes) >= 2:
709 return '%i orders, %s - %s' % (
710 len(orders),
711 '.'.join(codes[0]),
712 '.'.join(codes[-1]))
714 else:
715 return '%i orders, %s' % (
716 len(orders),
717 '.'.join(codes[0]))
720class Nut(Object):
721 '''
722 Index entry referencing an elementary piece of content.
724 So-called *nuts* are used in Pyrocko's Squirrel framework to hold common
725 meta-information about individual pieces of waveforms, stations, channels,
726 etc. together with the information where it was found or generated.
727 '''
729 file_path = String.T(optional=True)
730 file_format = String.T(optional=True)
731 file_mtime = Timestamp.T(optional=True)
732 file_size = Int.T(optional=True)
734 file_segment = Int.T(optional=True)
735 file_element = Int.T(optional=True)
737 kind_id = Int.T()
738 codes = String.T()
740 tmin_seconds = Timestamp.T()
741 tmin_offset = Int.T(default=0, optional=True)
742 tmax_seconds = Timestamp.T()
743 tmax_offset = Int.T(default=0, optional=True)
745 deltat = Float.T(default=0.0)
747 content = Content.T(optional=True)
749 content_in_db = False
751 def __init__(
752 self,
753 file_path=None,
754 file_format=None,
755 file_mtime=None,
756 file_size=None,
757 file_segment=None,
758 file_element=None,
759 kind_id=0,
760 codes='',
761 tmin_seconds=None,
762 tmin_offset=0,
763 tmax_seconds=None,
764 tmax_offset=0,
765 deltat=None,
766 content=None,
767 tmin=None,
768 tmax=None,
769 values_nocheck=None):
771 if values_nocheck is not None:
772 (self.file_path, self.file_format, self.file_mtime,
773 self.file_size,
774 self.file_segment, self.file_element,
775 self.kind_id, self.codes,
776 self.tmin_seconds, self.tmin_offset,
777 self.tmax_seconds, self.tmax_offset,
778 self.deltat) = values_nocheck
780 self.content = None
781 else:
782 if tmin is not None:
783 tmin_seconds, tmin_offset = tsplit(tmin)
785 if tmax is not None:
786 tmax_seconds, tmax_offset = tsplit(tmax)
788 self.kind_id = int(kind_id)
789 self.codes = str(codes)
790 self.tmin_seconds = int_or_g_tmin(tmin_seconds)
791 self.tmin_offset = int(tmin_offset)
792 self.tmax_seconds = int_or_g_tmax(tmax_seconds)
793 self.tmax_offset = int(tmax_offset)
794 self.deltat = float_or_none(deltat)
795 self.file_path = str_or_none(file_path)
796 self.file_segment = int_or_none(file_segment)
797 self.file_element = int_or_none(file_element)
798 self.file_format = str_or_none(file_format)
799 self.file_mtime = float_or_none(file_mtime)
800 self.file_size = int_or_none(file_size)
801 self.content = content
803 Object.__init__(self, init_props=False)
805 def __eq__(self, other):
806 return (isinstance(other, Nut) and
807 self.equality_values == other.equality_values)
809 def hash(self):
810 return ehash(','.join(str(x) for x in self.key))
812 def __ne__(self, other):
813 return not (self == other)
815 def get_io_backend(self):
816 from . import io
817 return io.get_backend(self.file_format)
819 def file_modified(self):
820 return self.get_io_backend().get_stats(self.file_path) \
821 != (self.file_mtime, self.file_size)
823 @property
824 def dkey(self):
825 return (self.kind_id, self.tmin_seconds, self.tmin_offset, self.codes)
827 @property
828 def key(self):
829 return (
830 self.file_path,
831 self.file_segment,
832 self.file_element,
833 self.file_mtime)
835 @property
836 def equality_values(self):
837 return (
838 self.file_segment, self.file_element,
839 self.kind_id, self.codes,
840 self.tmin_seconds, self.tmin_offset,
841 self.tmax_seconds, self.tmax_offset, self.deltat)
843 @property
844 def tmin(self):
845 return tjoin(self.tmin_seconds, self.tmin_offset)
847 @tmin.setter
848 def tmin(self, t):
849 self.tmin_seconds, self.tmin_offset = tsplit(t)
851 @property
852 def tmax(self):
853 return tjoin(self.tmax_seconds, self.tmax_offset)
855 @tmax.setter
856 def tmax(self, t):
857 self.tmax_seconds, self.tmax_offset = tsplit(t)
859 @property
860 def kscale(self):
861 if self.tmin_seconds is None or self.tmax_seconds is None:
862 return 0
863 return tscale_to_kscale(self.tmax_seconds - self.tmin_seconds)
865 @property
866 def waveform_kwargs(self):
867 agency, network, station, location, channel, extra = \
868 self.codes.split(separator)
870 return dict(
871 agency=agency,
872 network=network,
873 station=station,
874 location=location,
875 channel=channel,
876 extra=extra,
877 tmin=self.tmin,
878 tmax=self.tmax,
879 deltat=self.deltat)
881 @property
882 def waveform_promise_kwargs(self):
883 agency, network, station, location, channel, extra = \
884 self.codes.split(separator)
886 return dict(
887 agency=agency,
888 network=network,
889 station=station,
890 location=location,
891 channel=channel,
892 extra=extra,
893 tmin=self.tmin,
894 tmax=self.tmax,
895 deltat=self.deltat)
897 @property
898 def station_kwargs(self):
899 agency, network, station, location = self.codes.split(separator)
900 return dict(
901 agency=agency,
902 network=network,
903 station=station,
904 location=location if location != '*' else None,
905 tmin=tmin_or_none(self.tmin),
906 tmax=tmax_or_none(self.tmax))
908 @property
909 def channel_kwargs(self):
910 agency, network, station, location, channel, extra \
911 = self.codes.split(separator)
913 return dict(
914 agency=agency,
915 network=network,
916 station=station,
917 location=location,
918 channel=channel,
919 extra=extra,
920 tmin=tmin_or_none(self.tmin),
921 tmax=tmax_or_none(self.tmax),
922 deltat=self.deltat)
924 @property
925 def response_kwargs(self):
926 agency, network, station, location, channel, extra \
927 = self.codes.split(separator)
929 return dict(
930 agency=agency,
931 network=network,
932 station=station,
933 location=location,
934 channel=channel,
935 extra=extra,
936 tmin=tmin_or_none(self.tmin),
937 tmax=tmax_or_none(self.tmax),
938 deltat=self.deltat)
940 @property
941 def event_kwargs(self):
942 return dict(
943 name=self.codes,
944 time=self.tmin,
945 duration=(self.tmax - self.tmin) or None)
947 @property
948 def trace_kwargs(self):
949 agency, network, station, location, channel, extra = \
950 self.codes.split(separator)
952 return dict(
953 network=network,
954 station=station,
955 location=location,
956 channel=channel,
957 extra=extra,
958 tmin=self.tmin,
959 tmax=self.tmax-self.deltat,
960 deltat=self.deltat)
962 @property
963 def dummy_trace(self):
964 return DummyTrace(self)
966 @property
967 def codes_tuple(self):
968 return tuple(self.codes.split(separator))
970 @property
971 def summary(self):
972 if self.tmin == self.tmax:
973 ts = util.time_to_str(self.tmin)
974 else:
975 ts = '%s - %s' % (
976 util.time_to_str(self.tmin),
977 util.time_to_str(self.tmax))
979 return ' '.join((
980 ('%s,' % to_kind(self.kind_id)).ljust(9),
981 ('%s,' % '.'.join(self.codes.split(separator))).ljust(18),
982 ts))
985def make_waveform_nut(
986 agency='', network='', station='', location='', channel='', extra='',
987 **kwargs):
989 codes = separator.join(
990 (agency, network, station, location, channel, extra))
992 return Nut(
993 kind_id=WAVEFORM,
994 codes=codes,
995 **kwargs)
998def make_waveform_promise_nut(
999 agency='', network='', station='', location='', channel='', extra='',
1000 **kwargs):
1002 codes = separator.join(
1003 (agency, network, station, location, channel, extra))
1005 return Nut(
1006 kind_id=WAVEFORM_PROMISE,
1007 codes=codes,
1008 **kwargs)
1011def make_station_nut(
1012 agency='', network='', station='', location='', **kwargs):
1014 codes = separator.join((agency, network, station, location))
1016 return Nut(
1017 kind_id=STATION,
1018 codes=codes,
1019 **kwargs)
1022def make_channel_nut(
1023 agency='', network='', station='', location='', channel='', extra='',
1024 **kwargs):
1026 codes = separator.join(
1027 (agency, network, station, location, channel, extra))
1029 return Nut(
1030 kind_id=CHANNEL,
1031 codes=codes,
1032 **kwargs)
1035def make_response_nut(
1036 agency='', network='', station='', location='', channel='', extra='',
1037 **kwargs):
1039 codes = separator.join(
1040 (agency, network, station, location, channel, extra))
1042 return Nut(
1043 kind_id=RESPONSE,
1044 codes=codes,
1045 **kwargs)
1048def make_event_nut(name='', **kwargs):
1050 codes = name
1052 return Nut(
1053 kind_id=EVENT, codes=codes,
1054 **kwargs)
1057def group_channels(nuts):
1058 by_ansl = {}
1059 for nut in nuts:
1060 if nut.kind_id != CHANNEL:
1061 continue
1063 ansl = nut.codes[:4]
1065 if ansl not in by_ansl:
1066 by_ansl[ansl] = {}
1068 group = by_ansl[ansl]
1070 k = nut.codes[4][:-1], nut.deltat, nut.tmin, nut.tmax
1072 if k not in group:
1073 group[k] = set()
1075 group.add(nut.codes[4])
1077 return by_ansl
1080class DummyTrace(object):
1082 def __init__(self, nut):
1083 self.nut = nut
1084 self._codes = None
1085 self.meta = {}
1087 @property
1088 def tmin(self):
1089 return self.nut.tmin
1091 @property
1092 def tmax(self):
1093 return self.nut.tmax
1095 @property
1096 def deltat(self):
1097 return self.nut.deltat
1099 @property
1100 def codes(self):
1101 if self._codes is None:
1102 self._codes = self.nut.codes_tuple
1104 return self._codes
1106 @property
1107 def nslc_id(self):
1108 return self.codes[1:5]
1110 @property
1111 def agency(self):
1112 return self.codes[0]
1114 @property
1115 def network(self):
1116 return self.codes[1]
1118 @property
1119 def station(self):
1120 return self.codes[2]
1122 @property
1123 def location(self):
1124 return self.codes[3]
1126 @property
1127 def channel(self):
1128 return self.codes[4]
1130 @property
1131 def extra(self):
1132 return self.codes[5]
1134 def overlaps(self, tmin, tmax):
1135 return not (tmax < self.nut.tmin or self.nut.tmax < tmin)
1138def duration_to_str(t):
1139 if t > 24*3600:
1140 return '%gd' % (t / (24.*3600.))
1141 elif t > 3600:
1142 return '%gh' % (t / 3600.)
1143 elif t > 60:
1144 return '%gm' % (t / 60.)
1145 else:
1146 return '%gs' % t
1149class Coverage(Object):
1150 '''
1151 Information about times covered by a waveform or other content type.
1152 '''
1153 kind_id = Int.T()
1154 pattern = String.T()
1155 codes = String.T()
1156 deltat = Float.T(optional=True)
1157 tmin = Timestamp.T(optional=True)
1158 tmax = Timestamp.T(optional=True)
1159 changes = List.T(Tuple.T(2, Any.T()))
1161 @classmethod
1162 def from_values(cls, args):
1163 pattern, codes, deltat, tmin, tmax, changes, kind_id = args
1164 return cls(
1165 kind_id=kind_id,
1166 pattern=pattern,
1167 codes=codes,
1168 deltat=deltat,
1169 tmin=tmin,
1170 tmax=tmax,
1171 changes=changes)
1173 @property
1174 def summary(self):
1175 ts = '%s - %s,' % (
1176 util.time_to_str(self.tmin),
1177 util.time_to_str(self.tmax))
1179 srate = self.sample_rate
1181 return ' '.join((
1182 ('%s,' % to_kind(self.kind_id)).ljust(9),
1183 ('%s,' % '.'.join(self.codes.split(separator))).ljust(18),
1184 ts,
1185 '%10.3g,' % srate if srate else '',
1186 '%4i' % len(self.changes),
1187 '%s' % duration_to_str(self.total)))
1189 @property
1190 def sample_rate(self):
1191 if self.deltat is None:
1192 return None
1193 elif self.deltat == 0.0:
1194 return 0.0
1195 else:
1196 return 1.0 / self.deltat
1198 @property
1199 def labels(self):
1200 srate = self.sample_rate
1201 return (
1202 ('%s' % '.'.join(self.codes.split(separator))),
1203 '%.3g' % srate if srate else '')
1205 @property
1206 def total(self):
1207 total_t = None
1208 for tmin, tmax, _ in self.iter_spans():
1209 total_t = (total_t or 0.0) + (tmax - tmin)
1211 return total_t
1213 def iter_spans(self):
1214 last = None
1215 for (t, count) in self.changes:
1216 if last is not None:
1217 last_t, last_count = last
1218 if last_count > 0:
1219 yield last_t, t, last_count
1221 last = (t, count)
1224__all__ = [
1225 'separator',
1226 'to_kind',
1227 'to_kinds',
1228 'to_kind_id',
1229 'to_kind_ids',
1230 'Station',
1231 'Channel',
1232 'Sensor',
1233 'Response',
1234 'Nut',
1235 'Coverage',
1236 'WaveformPromise',
1237]