Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/squirrel/model.py: 70%
952 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
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'''
19import re
20import fnmatch
21import math
22import hashlib
23from os import urandom
24from base64 import urlsafe_b64encode
25from collections import defaultdict, namedtuple
27import numpy as num
29from pyrocko import util
30from pyrocko.guts import Object, SObject, String, Timestamp, Float, Int, \
31 Unicode, Tuple, List, StringChoice, Any, Dict, Duration, clone
32from pyrocko.model import squirrel_content, Location
33from pyrocko.response import FrequencyResponse, MultiplyResponse, \
34 IntegrationResponse, DifferentiationResponse, simplify_responses, \
35 FrequencyResponseCheckpoint, Gain
37from .error import ConversionError, SquirrelError
39d2r = num.pi / 180.
40r2d = 1.0 / d2r
43guts_prefix = 'squirrel'
46def mkvec(x, y, z):
47 return num.array([x, y, z], dtype=float)
50def are_orthogonal(vecs, eps=0.01):
51 return all(abs(
52 num.dot(vecs[i], vecs[j]) < eps
53 for (i, j) in [(0, 1), (1, 2), (2, 0)]))
56g_codes_pool = {}
59class CodesError(SquirrelError):
60 pass
63class Codes(SObject):
64 def __new__(cls, string):
65 return to_codes_guess(string)
68def normalize_nslce(*args, **kwargs):
69 if args and kwargs:
70 raise ValueError('Either *args or **kwargs accepted, not both.')
72 if len(args) == 1:
73 if isinstance(args[0], str):
74 args = tuple(args[0].split('.'))
75 elif isinstance(args[0], tuple):
76 args = args[0]
77 else:
78 raise ValueError('Invalid argument type: %s' % type(args[0]))
80 nargs = len(args)
81 if nargs == 5:
82 t = args
84 elif nargs == 4:
85 t = args + ('',)
87 elif nargs == 0:
88 d = dict(
89 network='',
90 station='',
91 location='',
92 channel='',
93 extra='')
95 d.update(kwargs)
96 t = tuple(kwargs.get(k, '') for k in (
97 'network', 'station', 'location', 'channel', 'extra'))
99 else:
100 raise CodesError(
101 'Does not match NSLC or NSLCE codes pattern: %s' % '.'.join(args))
103 if '.'.join(t).count('.') != 4:
104 raise CodesError(
105 'Codes may not contain a ".": "%s", "%s", "%s", "%s", "%s"' % t)
107 return t
110CodesNSLCEBase = namedtuple(
111 'CodesNSLCEBase', [
112 'network', 'station', 'location', 'channel', 'extra'])
115class CodesNSLCE(CodesNSLCEBase, Codes):
116 '''
117 Codes denominating a seismic channel (NSLC or NSLCE).
119 FDSN/SEED style NET.STA.LOC.CHA is accepted or NET.STA.LOC.CHA.EXTRA, where
120 the EXTRA part in the latter form can be used to identify a custom
121 processing applied to a channel.
122 '''
124 __slots__ = ()
125 __hash__ = CodesNSLCEBase.__hash__
127 as_dict = CodesNSLCEBase._asdict
129 def __new__(cls, *args, safe_str=None, **kwargs):
130 nargs = len(args)
131 if nargs == 1 and isinstance(args[0], CodesNSLCE):
132 return args[0]
133 elif nargs == 1 and isinstance(args[0], CodesNSL):
134 t = (args[0].nsl) + ('*', '*')
135 elif nargs == 1 and isinstance(args[0], CodesX):
136 t = ('*', '*', '*', '*', '*')
137 elif safe_str is not None:
138 t = safe_str.split('.')
139 else:
140 t = normalize_nslce(*args, **kwargs)
142 x = CodesNSLCEBase.__new__(cls, *t)
143 return g_codes_pool.setdefault(x, x)
145 def __init__(self, *args, **kwargs):
146 Codes.__init__(self)
148 def __str__(self):
149 if self.extra == '':
150 return '.'.join(self[:-1])
151 else:
152 return '.'.join(self)
154 def __eq__(self, other):
155 if not isinstance(other, CodesNSLCE):
156 other = CodesNSLCE(other)
158 return CodesNSLCEBase.__eq__(self, other)
160 def matches(self, pattern):
161 if not isinstance(pattern, CodesNSLCE):
162 pattern = CodesNSLCE(pattern)
164 return match_codes(pattern, self)
166 @property
167 def safe_str(self):
168 return '.'.join(self)
170 @property
171 def nslce(self):
172 return self[:5]
174 @property
175 def nslc(self):
176 return self[:4]
178 @property
179 def nsl(self):
180 return self[:3]
182 @property
183 def ns(self):
184 return self[:2]
186 @property
187 def codes_nsl(self):
188 return CodesNSL(self)
190 @property
191 def codes_nsl_star(self):
192 return CodesNSL(self.network, self.station, '*')
194 def as_tuple(self):
195 return tuple(self)
197 def replace(self, **kwargs):
198 x = CodesNSLCEBase._replace(self, **kwargs)
199 return g_codes_pool.setdefault(x, x)
202def normalize_nsl(*args, **kwargs):
203 if args and kwargs:
204 raise ValueError('Either *args or **kwargs accepted, not both.')
206 if len(args) == 1:
207 if isinstance(args[0], str):
208 args = tuple(args[0].split('.'))
209 elif isinstance(args[0], tuple):
210 args = args[0]
211 else:
212 raise ValueError('Invalid argument type: %s' % type(args[0]))
214 nargs = len(args)
215 if nargs == 3:
216 t = args
218 elif nargs == 0:
219 d = dict(
220 network='',
221 station='',
222 location='')
224 d.update(kwargs)
225 t = tuple(kwargs.get(k, '') for k in (
226 'network', 'station', 'location'))
228 else:
229 raise CodesError(
230 'Does not match NSL codes pattern: %s' % '.'.join(args))
232 if '.'.join(t).count('.') != 2:
233 raise CodesError(
234 'Codes may not contain a ".": "%s", "%s", "%s"' % t)
236 return t
239CodesNSLBase = namedtuple(
240 'CodesNSLBase', [
241 'network', 'station', 'location'])
244class CodesNSL(CodesNSLBase, Codes):
245 '''
246 Codes denominating a seismic station (NSL).
248 NET.STA.LOC is accepted, slightly different from SEED/StationXML, where
249 LOC is part of the channel. By setting location='*' is possible to get
250 compatible behaviour in most cases.
251 '''
253 __slots__ = ()
254 __hash__ = CodesNSLBase.__hash__
256 as_dict = CodesNSLBase._asdict
258 def __new__(cls, *args, safe_str=None, **kwargs):
259 nargs = len(args)
260 if nargs == 1 and isinstance(args[0], CodesNSL):
261 return args[0]
262 elif nargs == 1 and isinstance(args[0], CodesNSLCE):
263 t = args[0].nsl
264 elif nargs == 1 and isinstance(args[0], CodesX):
265 t = ('*', '*', '*')
266 elif safe_str is not None:
267 t = safe_str.split('.')
268 else:
269 t = normalize_nsl(*args, **kwargs)
271 x = CodesNSLBase.__new__(cls, *t)
272 return g_codes_pool.setdefault(x, x)
274 def __init__(self, *args, **kwargs):
275 Codes.__init__(self)
277 def __str__(self):
278 return '.'.join(self)
280 def __eq__(self, other):
281 if not isinstance(other, CodesNSL):
282 other = CodesNSL(other)
284 return CodesNSLBase.__eq__(self, other)
286 def matches(self, pattern):
287 if not isinstance(pattern, CodesNSL):
288 pattern = CodesNSL(pattern)
290 return match_codes(pattern, self)
292 @property
293 def safe_str(self):
294 return '.'.join(self)
296 @property
297 def ns(self):
298 return self[:2]
300 @property
301 def nsl(self):
302 return self[:3]
304 def as_tuple(self):
305 return tuple(self)
307 def replace(self, **kwargs):
308 x = CodesNSLBase._replace(self, **kwargs)
309 return g_codes_pool.setdefault(x, x)
312CodesXBase = namedtuple(
313 'CodesXBase', [
314 'name'])
317class CodesX(CodesXBase, Codes):
318 '''
319 General purpose codes for anything other than channels or stations.
320 '''
322 __slots__ = ()
323 __hash__ = CodesXBase.__hash__
324 __eq__ = CodesXBase.__eq__
326 as_dict = CodesXBase._asdict
328 def __new__(cls, name='', safe_str=None):
329 if isinstance(name, CodesX):
330 return name
331 elif isinstance(name, (CodesNSLCE, CodesNSL)):
332 name = '*'
333 elif safe_str is not None:
334 name = safe_str
335 else:
336 if '.' in name:
337 raise CodesError('Code may not contain a ".": %s' % name)
339 x = CodesXBase.__new__(cls, name)
340 return g_codes_pool.setdefault(x, x)
342 def __init__(self, *args, **kwargs):
343 Codes.__init__(self)
345 def __str__(self):
346 return '.'.join(self)
348 @property
349 def safe_str(self):
350 return '.'.join(self)
352 @property
353 def ns(self):
354 return self[:2]
356 def as_tuple(self):
357 return tuple(self)
359 def replace(self, **kwargs):
360 x = CodesXBase._replace(self, **kwargs)
361 return g_codes_pool.setdefault(x, x)
364g_codes_patterns = {}
367def _is_exact(pat):
368 return not ('*' in pat or '?' in pat or ']' in pat or '[' in pat)
371def classify_patterns(patterns):
372 pats_exact = []
373 pats_nonexact = []
374 for pat in patterns:
375 spat = pat.safe_str
376 (pats_exact if _is_exact(spat) else pats_nonexact).append(spat)
378 return pats_exact, pats_nonexact
381def get_regex_pattern(spattern):
382 if spattern not in g_codes_patterns:
383 rpattern = re.compile(fnmatch.translate(spattern), re.I)
384 g_codes_patterns[spattern] = rpattern
386 return g_codes_patterns[spattern]
389def match_codes(pattern, codes):
390 spattern = pattern.safe_str
391 scodes = codes.safe_str
392 rpattern = get_regex_pattern(spattern)
393 return bool(rpattern.match(scodes))
396class CodesMatcher:
397 def __init__(self, patterns):
398 self._pats_exact, self._pats_nonexact = classify_patterns(patterns)
399 self._pats_exact = set(self._pats_exact)
400 self._pats_nonexact = [
401 get_regex_pattern(spat) for spat in self._pats_nonexact]
403 def match(self, codes):
404 scodes = codes.safe_str
405 if scodes in self._pats_exact:
406 return True
408 return any(rpat.match(scodes) for rpat in self._pats_nonexact)
411def match_codes_any(patterns, codes):
413 pats_exact, pats_nonexact = classify_patterns(patterns)
415 if codes.safe_str in pats_exact:
416 return True
418 return any(match_codes(pattern, codes) for pattern in patterns)
421g_content_kinds = [
422 'undefined',
423 'waveform',
424 'station',
425 'channel',
426 'response',
427 'event',
428 'waveform_promise',
429 'empty']
432g_codes_classes = [
433 CodesX,
434 CodesNSLCE,
435 CodesNSL,
436 CodesNSLCE,
437 CodesNSLCE,
438 CodesX,
439 CodesNSLCE,
440 CodesX]
442g_codes_classes_ndot = {
443 0: CodesX,
444 2: CodesNSL,
445 3: CodesNSLCE,
446 4: CodesNSLCE}
449def to_codes_simple(kind_id, codes_safe_str):
450 return g_codes_classes[kind_id](safe_str=codes_safe_str)
453def to_codes(kind_id, obj):
454 return g_codes_classes[kind_id](obj)
457def to_codes_guess(s):
458 try:
459 return g_codes_classes_ndot[s.count('.')](s)
460 except KeyError:
461 raise CodesError('Cannot guess codes type: %s' % s)
464# derived list class to enable detection of already preprocessed codes patterns
465class codes_patterns_list(list):
466 pass
469def codes_patterns_for_kind(kind_id, codes):
470 if isinstance(codes, codes_patterns_list):
471 return codes
473 if isinstance(codes, list):
474 lcodes = codes_patterns_list()
475 for sc in codes:
476 lcodes.extend(codes_patterns_for_kind(kind_id, sc))
478 return lcodes
480 codes = to_codes(kind_id, codes)
482 lcodes = codes_patterns_list()
483 lcodes.append(codes)
484 if kind_id == STATION:
485 lcodes.append(codes.replace(location='[*]'))
487 return lcodes
490g_content_kind_ids = (
491 UNDEFINED, WAVEFORM, STATION, CHANNEL, RESPONSE, EVENT,
492 WAVEFORM_PROMISE, EMPTY) = range(len(g_content_kinds))
495g_tmin, g_tmax = util.get_working_system_time_range()[:2]
498try:
499 g_tmin_queries = max(g_tmin, util.str_to_time_fillup('1900-01-01'))
500except Exception:
501 g_tmin_queries = g_tmin
504def to_kind(kind_id):
505 return g_content_kinds[kind_id]
508def to_kinds(kind_ids):
509 return [g_content_kinds[kind_id] for kind_id in kind_ids]
512def to_kind_id(kind):
513 return g_content_kinds.index(kind)
516def to_kind_ids(kinds):
517 return [g_content_kinds.index(kind) for kind in kinds]
520g_kind_mask_all = 0xff
523def to_kind_mask(kinds):
524 if kinds:
525 return sum(1 << kind_id for kind_id in to_kind_ids(kinds))
526 else:
527 return g_kind_mask_all
530def str_or_none(x):
531 if x is None:
532 return None
533 else:
534 return str(x)
537def float_or_none(x):
538 if x is None:
539 return None
540 else:
541 return float(x)
544def int_or_none(x):
545 if x is None:
546 return None
547 else:
548 return int(x)
551def int_or_g_tmin(x):
552 if x is None:
553 return g_tmin
554 else:
555 return int(x)
558def int_or_g_tmax(x):
559 if x is None:
560 return g_tmax
561 else:
562 return int(x)
565def tmin_or_none(x):
566 if x == g_tmin:
567 return None
568 else:
569 return x
572def tmax_or_none(x):
573 if x == g_tmax:
574 return None
575 else:
576 return x
579def time_or_none_to_str(x):
580 if x is None:
581 return '...'.ljust(17)
582 else:
583 return util.time_to_str(x)
586def codes_to_str_abbreviated(codes, indent=' '):
587 codes = [str(x) for x in codes]
589 if len(codes) > 20:
590 scodes = '\n' + util.ewrap(codes[:10], indent=indent) \
591 + '\n%s[%i more]\n' % (indent, len(codes) - 20) \
592 + util.ewrap(codes[-10:], indent=' ')
593 else:
594 scodes = '\n' + util.ewrap(codes, indent=indent) \
595 if codes else '<none>'
597 return scodes
600g_offset_time_unit_inv = 1000000000
601g_offset_time_unit = 1.0 / g_offset_time_unit_inv
604def tsplit(t):
605 if t is None:
606 return None, 0
608 t = util.to_time_float(t)
609 if type(t) is float:
610 t = round(t, 5)
611 else:
612 t = round(t, 9)
614 seconds = num.floor(t)
615 offset = t - seconds
616 return int(seconds), int(round(offset * g_offset_time_unit_inv))
619def tjoin(seconds, offset):
620 if seconds is None:
621 return None
623 return util.to_time_float(seconds) \
624 + util.to_time_float(offset*g_offset_time_unit)
627tscale_min = 1
628tscale_max = 365 * 24 * 3600 # last edge is one above
629tscale_logbase = 20
631tscale_edges = [tscale_min]
632while True:
633 tscale_edges.append(tscale_edges[-1]*tscale_logbase)
634 if tscale_edges[-1] >= tscale_max:
635 break
638tscale_edges = num.array(tscale_edges)
641def tscale_to_kscale(tscale):
643 # 0 <= x < tscale_edges[1]: 0
644 # tscale_edges[1] <= x < tscale_edges[2]: 1
645 # ...
646 # tscale_edges[len(tscale_edges)-1] <= x: len(tscale_edges)
648 return int(num.searchsorted(tscale_edges, tscale))
651@squirrel_content
652class Station(Location):
653 '''
654 A seismic station.
655 '''
657 codes = CodesNSL.T()
659 tmin = Timestamp.T(optional=True)
660 tmax = Timestamp.T(optional=True)
662 description = Unicode.T(optional=True)
664 def __init__(self, **kwargs):
665 kwargs['codes'] = CodesNSL(kwargs['codes'])
666 Location.__init__(self, **kwargs)
668 @property
669 def time_span(self):
670 return (self.tmin, self.tmax)
672 def get_pyrocko_station(self):
673 '''
674 Get station as a classic Pyrocko station object.
676 :returns:
677 Converted station object.
678 :rtype:
679 :py:class:`pyrocko.model.station.Station`
680 '''
681 from pyrocko import model
682 return model.Station(*self._get_pyrocko_station_args())
684 def _get_pyrocko_station_args(self):
685 return (
686 self.codes.network,
687 self.codes.station,
688 self.codes.location,
689 self.lat,
690 self.lon,
691 self.elevation,
692 self.depth,
693 self.north_shift,
694 self.east_shift)
697@squirrel_content
698class ChannelBase(Location):
699 codes = CodesNSLCE.T()
701 tmin = Timestamp.T(optional=True)
702 tmax = Timestamp.T(optional=True)
704 deltat = Float.T(optional=True)
706 def __init__(self, **kwargs):
707 kwargs['codes'] = CodesNSLCE(kwargs['codes'])
708 Location.__init__(self, **kwargs)
710 @property
711 def time_span(self):
712 return (self.tmin, self.tmax)
714 def _get_sensor_codes(self):
715 return self.codes.replace(
716 channel=self.codes.channel[:-1] + '?')
718 def _get_sensor_args(self):
719 def getattr_rep(k):
720 if k == 'codes':
721 return self._get_sensor_codes()
722 else:
723 return getattr(self, k)
725 return tuple(getattr_rep(k) for k in ChannelBase.T.propnames)
727 def _get_channel_args(self, component):
728 def getattr_rep(k):
729 if k == 'codes':
730 return self.codes.replace(
731 channel=self.codes.channel[:-1] + component)
732 else:
733 return getattr(self, k)
735 return tuple(getattr_rep(k) for k in ChannelBase.T.propnames)
737 def _get_pyrocko_station_args(self):
738 return (
739 self.codes.network,
740 self.codes.station,
741 self.codes.location,
742 self.lat,
743 self.lon,
744 self.elevation,
745 self.depth,
746 self.north_shift,
747 self.east_shift)
750class Channel(ChannelBase):
751 '''
752 A channel of a seismic station.
753 '''
755 dip = Float.T(optional=True)
756 azimuth = Float.T(optional=True)
758 def get_pyrocko_channel(self):
759 from pyrocko import model
760 return model.Channel(*self._get_pyrocko_channel_args())
762 def _get_pyrocko_channel_args(self):
763 return (
764 self.codes.channel,
765 self.azimuth,
766 self.dip)
768 @property
769 def orientation_enz(self):
770 if None in (self.azimuth, self.dip):
771 return None
773 n = math.cos(self.azimuth*d2r)*math.cos(self.dip*d2r)
774 e = math.sin(self.azimuth*d2r)*math.cos(self.dip*d2r)
775 d = math.sin(self.dip*d2r)
776 return mkvec(e, n, -d)
779def cut_intervals(channels):
780 channels = list(channels)
781 times = set()
782 for channel in channels:
783 if channel.tmin is not None:
784 times.add(channel.tmin)
785 if channel.tmax is not None:
786 times.add(channel.tmax)
788 times = sorted(times)
790 if any(channel.tmin is None for channel in channels):
791 times[0:0] = [None]
793 if any(channel.tmax is None for channel in channels):
794 times.append(None)
796 if len(times) <= 2:
797 return channels
799 channels_out = []
800 for channel in channels:
801 for i in range(len(times)-1):
802 tmin = times[i]
803 tmax = times[i+1]
804 if ((channel.tmin is None or (
805 tmin is not None and channel.tmin <= tmin))
806 and (channel.tmax is None or (
807 tmax is not None and tmax <= channel.tmax))):
809 channel_out = clone(channel)
810 channel_out.tmin = tmin
811 channel_out.tmax = tmax
812 channels_out.append(channel_out)
814 return channels_out
817class Sensor(ChannelBase):
818 '''
819 Representation of a channel group.
820 '''
822 channels = List.T(Channel.T())
824 @classmethod
825 def from_channels(cls, channels):
826 groups = defaultdict(list)
827 for channel in channels:
828 groups[channel._get_sensor_codes()].append(channel)
830 channels_cut = []
831 for group in groups.values():
832 channels_cut.extend(cut_intervals(group))
834 groups = defaultdict(list)
835 for channel in channels_cut:
836 groups[channel._get_sensor_args()].append(channel)
838 return [
839 cls(channels=channels,
840 **dict(zip(ChannelBase.T.propnames, args)))
841 for args, channels in groups.items()]
843 def channel_vectors(self):
844 return num.vstack(
845 [channel.orientation_enz for channel in self.channels])
847 def projected_channels(self, system, component_names):
848 return [
849 Channel(
850 azimuth=math.atan2(e, n) * r2d,
851 dip=-math.asin(z) * r2d,
852 **dict(zip(
853 ChannelBase.T.propnames,
854 self._get_channel_args(comp))))
855 for comp, (e, n, z) in zip(component_names, system)]
857 def matrix_to(self, system, epsilon=1e-7):
858 m = num.dot(system, self.channel_vectors().T)
859 m[num.abs(m) < epsilon] = 0.0
860 return m
862 def projection_to(self, system, component_names):
863 return (
864 self.matrix_to(system),
865 self.channels,
866 self.projected_channels(system, component_names))
868 def projection_to_enz(self):
869 return self.projection_to(num.identity(3), 'ENZ')
871 def projection_to_trz(self, source, azimuth=None):
872 if azimuth is not None:
873 assert source is None
874 else:
875 azimuth = source.azibazi_to(self)[1] + 180.
877 return self.projection_to(num.array([
878 [math.cos(azimuth*d2r), -math.sin(azimuth*d2r), 0.],
879 [math.sin(azimuth*d2r), math.cos(azimuth*d2r), 0.],
880 [0., 0., 1.]], dtype=float), 'TRZ')
882 def project_to_enz(self, traces):
883 from pyrocko import trace
885 matrix, in_channels, out_channels = self.projection_to_enz()
887 return trace.project(traces, matrix, in_channels, out_channels)
889 def project_to_trz(self, source, traces, azimuth=None):
890 from pyrocko import trace
892 matrix, in_channels, out_channels = self.projection_to_trz(
893 source, azimuth=azimuth)
895 return trace.project(traces, matrix, in_channels, out_channels)
898observational_quantities = [
899 'acceleration', 'velocity', 'displacement', 'pressure',
900 'rotation_displacement', 'rotation_velocity', 'rotation_acceleration',
901 'temperature']
904technical_quantities = [
905 'voltage', 'counts']
908class QuantityType(StringChoice):
909 '''
910 Choice of observational or technical quantity.
912 SI units are used for all quantities, where applicable.
913 '''
914 choices = observational_quantities + technical_quantities
917class ResponseStage(Object):
918 '''
919 Representation of a response stage.
921 Components of a seismic recording system are represented as a sequence of
922 response stages, e.g. sensor, pre-amplifier, digitizer, digital
923 downsampling.
924 '''
925 input_quantity = QuantityType.T(optional=True)
926 input_sample_rate = Float.T(optional=True)
927 output_quantity = QuantityType.T(optional=True)
928 output_sample_rate = Float.T(optional=True)
929 elements = List.T(FrequencyResponse.T())
930 log = List.T(Tuple.T(3, String.T()))
932 @property
933 def stage_type(self):
934 if self.input_quantity in observational_quantities \
935 and self.output_quantity in observational_quantities:
936 return 'conversion'
938 if self.input_quantity in observational_quantities \
939 and self.output_quantity == 'voltage':
940 return 'sensor'
942 elif self.input_quantity == 'voltage' \
943 and self.output_quantity == 'voltage':
944 return 'electronics'
946 elif self.input_quantity == 'voltage' \
947 and self.output_quantity == 'counts':
948 return 'digitizer'
950 elif self.decimation_factor is not None \
951 and (self.input_quantity is None or self.input_quantity == 'counts') \
952 and (self.output_quantity is None or self.output_quantity == 'counts'): # noqa
953 return 'decimation'
955 elif self.input_quantity in observational_quantities \
956 and self.output_quantity == 'counts':
957 return 'combination'
959 else:
960 return 'unknown'
962 @property
963 def decimation_factor(self):
964 irate = self.input_sample_rate
965 orate = self.output_sample_rate
966 if irate is not None and orate is not None \
967 and irate > orate and irate / orate > 1.0:
969 return irate / orate
970 else:
971 return None
973 @property
974 def summary_quantities(self):
975 return '%s -> %s' % (
976 self.input_quantity or '?',
977 self.output_quantity or '?')
979 @property
980 def summary_rates(self):
981 irate = self.input_sample_rate
982 orate = self.output_sample_rate
983 factor = self.decimation_factor
985 if irate and orate is None:
986 return '%g Hz' % irate
988 elif orate and irate is None:
989 return '%g Hz' % orate
991 elif irate and orate and irate == orate:
992 return '%g Hz' % irate
994 elif any(x for x in (irate, orate, factor)):
995 return '%s -> %s Hz (%s)' % (
996 '%g' % irate if irate else '?',
997 '%g' % orate if orate else '?',
998 ':%g' % factor if factor else '?')
999 else:
1000 return ''
1002 @property
1003 def summary_elements(self):
1004 xs = [x.summary for x in self.elements]
1005 return '%s' % ('*'.join(x for x in xs if x != 'one') or 'one')
1007 @property
1008 def summary_log(self):
1009 return ''.join(sorted(set(x[0][0].upper() for x in self.log)))
1011 @property
1012 def summary_entries(self):
1013 return (
1014 self.__class__.__name__,
1015 self.stage_type,
1016 self.summary_log,
1017 self.summary_quantities,
1018 self.summary_rates,
1019 self.summary_elements)
1021 @property
1022 def summary(self):
1023 return util.fmt_summary(self.summary_entries, (10, 15, 3, 30, 30, 0))
1025 def get_effective(self):
1026 return MultiplyResponse(responses=list(self.elements))
1029D = 'displacement'
1030V = 'velocity'
1031A = 'acceleration'
1033g_converters = {
1034 (V, D): IntegrationResponse(1),
1035 (A, D): IntegrationResponse(2),
1036 (D, V): DifferentiationResponse(1),
1037 (A, V): IntegrationResponse(1),
1038 (D, A): DifferentiationResponse(2),
1039 (V, A): DifferentiationResponse(1)}
1042def response_converters(input_quantity, output_quantity):
1043 if input_quantity is None or input_quantity == output_quantity:
1044 return []
1046 if output_quantity is None:
1047 raise ConversionError('Unspecified target quantity.')
1049 try:
1050 return [g_converters[input_quantity, output_quantity]]
1052 except KeyError:
1053 raise ConversionError('No rule to convert from "%s" to "%s".' % (
1054 input_quantity, output_quantity))
1057@squirrel_content
1058class Response(Object):
1059 '''
1060 The instrument response of a seismic station channel.
1061 '''
1063 codes = CodesNSLCE.T()
1064 tmin = Timestamp.T(optional=True)
1065 tmax = Timestamp.T(optional=True)
1067 stages = List.T(ResponseStage.T())
1068 checkpoints = List.T(FrequencyResponseCheckpoint.T())
1070 deltat = Float.T(optional=True)
1071 log = List.T(Tuple.T(3, String.T()))
1073 def __init__(self, **kwargs):
1074 kwargs['codes'] = CodesNSLCE(kwargs['codes'])
1075 Object.__init__(self, **kwargs)
1076 self._effective_responses_cache = {}
1078 @property
1079 def time_span(self):
1080 return (self.tmin, self.tmax)
1082 @property
1083 def nstages(self):
1084 return len(self.stages)
1086 @property
1087 def input_quantity(self):
1088 return self.stages[0].input_quantity if self.stages else None
1090 @property
1091 def output_quantity(self):
1092 return self.stages[-1].output_quantity if self.stages else None
1094 @property
1095 def output_sample_rate(self):
1096 return self.stages[-1].output_sample_rate if self.stages else None
1098 @property
1099 def summary_stages(self):
1100 def grouped(xs):
1101 xs = list(xs)
1102 g = []
1103 for i in range(len(xs)):
1104 g.append(xs[i])
1105 if i+1 < len(xs) and xs[i+1] != xs[i]:
1106 yield g
1107 g = []
1109 if g:
1110 yield g
1112 return '+'.join(
1113 '%s%s' % (g[0], '(%i)' % len(g) if len(g) > 1 else '')
1114 for g in grouped(stage.stage_type for stage in self.stages))
1116 @property
1117 def summary_quantities(self):
1118 orate = self.output_sample_rate
1119 return '%s -> %s%s' % (
1120 self.input_quantity or '?',
1121 self.output_quantity or '?',
1122 ' @ %g Hz' % orate if orate else '')
1124 @property
1125 def summary_log(self):
1126 return ''.join(sorted(set(x[0][0].upper() for x in self.log)))
1128 @property
1129 def summary_entries(self):
1130 return (
1131 self.__class__.__name__,
1132 str(self.codes),
1133 self.str_time_span,
1134 self.summary_log,
1135 self.summary_quantities,
1136 self.summary_stages)
1138 @property
1139 def summary(self):
1140 return util.fmt_summary(self.summary_entries, (10, 20, 55, 3, 35, 0))
1142 def get_effective(
1143 self,
1144 input_quantity=None,
1145 stages=(None, None),
1146 mode='complete',
1147 gain_frequency=None):
1149 assert mode in ('complete', 'sensor')
1150 assert not (mode == 'sensor' and gain_frequency is None)
1152 cache_key = (input_quantity, stages)
1153 if cache_key in self._effective_responses_cache:
1154 return self._effective_responses_cache[cache_key]
1156 try:
1157 elements = response_converters(input_quantity, self.input_quantity)
1158 except ConversionError as e:
1159 raise ConversionError(str(e) + ' (%s)' % self.summary)
1161 for istage, stage in enumerate(self.stages):
1162 if (stages[0] is None or stages[0] <= istage) \
1163 and (stages[1] is None or istage < stages[1]):
1165 is_sensor = istage == 0 or stage.stage_type == 'sensor'
1167 if mode == 'complete' or (mode == 'sensor' and is_sensor):
1168 elements.append(stage.get_effective())
1169 else:
1170 resp = stage.get_effective()
1171 gain = Gain(constant=resp.evaluate1(gain_frequency))
1172 # maybe check derivative if response is flat enough?
1173 # or check frequency band?
1174 elements.append(gain)
1176 if input_quantity is None \
1177 or input_quantity == self.input_quantity:
1178 checkpoints = self.checkpoints
1179 else:
1180 checkpoints = []
1182 resp = MultiplyResponse(
1183 responses=simplify_responses(elements),
1184 checkpoints=checkpoints)
1186 self._effective_responses_cache[cache_key] = resp
1187 return resp
1190@squirrel_content
1191class Event(Object):
1192 '''
1193 A seismic event.
1194 '''
1196 name = String.T(optional=True)
1197 time = Timestamp.T()
1198 duration = Float.T(optional=True)
1200 lat = Float.T()
1201 lon = Float.T()
1202 depth = Float.T(optional=True)
1204 magnitude = Float.T(optional=True)
1206 def get_pyrocko_event(self):
1207 from pyrocko import model
1208 model.Event(
1209 name=self.name,
1210 time=self.time,
1211 lat=self.lat,
1212 lon=self.lon,
1213 depth=self.depth,
1214 magnitude=self.magnitude,
1215 duration=self.duration)
1217 @property
1218 def time_span(self):
1219 return (self.time, self.time)
1222def ehash(s):
1223 return hashlib.sha1(s.encode('utf8')).hexdigest()
1226def random_name(n=8):
1227 return urlsafe_b64encode(urandom(n)).rstrip(b'=').decode('ascii')
1230@squirrel_content
1231class WaveformPromise(Object):
1232 '''
1233 Information about a waveform potentially downloadable from a remote site.
1235 In the Squirrel framework, waveform promises are used to permit download of
1236 selected waveforms from a remote site. They are typically generated by
1237 calls to
1238 :py:meth:`~pyrocko.squirrel.base.Squirrel.update_waveform_promises`.
1239 Waveform promises are inserted and indexed in the database similar to
1240 normal waveforms. When processing a waveform query, e.g. from
1241 :py:meth:`~pyrocko.squirrel.base.Squirrel.get_waveforms`, and no local
1242 waveform is available for the queried time span, a matching promise can be
1243 resolved, i.e. an attempt is made to download the waveform from the remote
1244 site. The promise is removed after the download attempt (except when a
1245 network error occurs). This prevents Squirrel from making unnecessary
1246 queries for waveforms missing at the remote site.
1247 '''
1249 codes = CodesNSLCE.T()
1250 tmin = Timestamp.T()
1251 tmax = Timestamp.T()
1253 deltat = Float.T(optional=True)
1255 source_hash = String.T()
1257 def __init__(self, **kwargs):
1258 kwargs['codes'] = CodesNSLCE(kwargs['codes'])
1259 Object.__init__(self, **kwargs)
1261 @property
1262 def time_span(self):
1263 return (self.tmin, self.tmax)
1266class InvalidWaveform(Exception):
1267 pass
1270class WaveformOrder(Object):
1271 '''
1272 Waveform request information.
1273 '''
1275 source_id = String.T()
1276 codes = CodesNSLCE.T()
1277 deltat = Float.T()
1278 tmin = Timestamp.T()
1279 tmax = Timestamp.T()
1280 gaps = List.T(Tuple.T(2, Timestamp.T()))
1281 time_created = Timestamp.T()
1282 anxious = Duration.T(default=600.)
1284 @property
1285 def client(self):
1286 return self.source_id.split(':')[1]
1288 def key1(self):
1289 return (self.source_id, self.codes, self.tmin, self.tmax)
1291 def key2(self):
1292 return (self.codes, self.tmin, self.tmax)
1294 def describe(self, site='?'):
1295 return '%s:%s %s [%s - %s]' % (
1296 self.client, site, str(self.codes),
1297 util.time_to_str(self.tmin), util.time_to_str(self.tmax))
1299 def validate(self, tr):
1300 if tr.ydata.size == 0:
1301 raise InvalidWaveform(
1302 'waveform with zero data samples')
1304 if abs(tr.deltat - self.deltat) > (tr.deltat + self.deltat)*1e-5:
1305 raise InvalidWaveform(
1306 'incorrect sampling interval - waveform: %g s, '
1307 'meta-data: %g s' % (
1308 tr.deltat, self.deltat))
1310 if not num.all(num.isfinite(tr.ydata)):
1311 raise InvalidWaveform('waveform has NaN values')
1313 def estimate_nsamples(self):
1314 return int(round((self.tmax - self.tmin) / self.deltat))+1
1316 def is_near_real_time(self):
1317 return self.tmax > self.time_created - self.anxious
1320def order_summary(orders):
1321 codes_list = sorted(set(order.codes for order in orders))
1322 if len(codes_list) > 3:
1323 return '%i order%s: %s - %s' % (
1324 len(orders),
1325 util.plural_s(orders),
1326 str(codes_list[0]),
1327 str(codes_list[1]))
1329 else:
1330 return '%i order%s: %s' % (
1331 len(orders),
1332 util.plural_s(orders),
1333 ', '.join(str(codes) for codes in codes_list))
1336class Nut(Object):
1337 '''
1338 Index entry referencing an elementary piece of content.
1340 So-called *nuts* are used in Pyrocko's Squirrel framework to hold common
1341 meta-information about individual pieces of waveforms, stations, channels,
1342 etc. together with the information where it was found or generated.
1343 '''
1345 file_path = String.T(optional=True)
1346 file_format = String.T(optional=True)
1347 file_mtime = Timestamp.T(optional=True)
1348 file_size = Int.T(optional=True)
1350 file_segment = Int.T(optional=True)
1351 file_element = Int.T(optional=True)
1353 kind_id = Int.T()
1354 codes = Codes.T()
1356 tmin_seconds = Int.T(default=0)
1357 tmin_offset = Int.T(default=0, optional=True)
1358 tmax_seconds = Int.T(default=0)
1359 tmax_offset = Int.T(default=0, optional=True)
1361 deltat = Float.T(default=0.0)
1363 content = Any.T(optional=True)
1364 raw_content = Dict.T(String.T(), Any.T())
1366 content_in_db = False
1368 def __init__(
1369 self,
1370 file_path=None,
1371 file_format=None,
1372 file_mtime=None,
1373 file_size=None,
1374 file_segment=None,
1375 file_element=None,
1376 kind_id=0,
1377 codes=CodesX(''),
1378 tmin_seconds=None,
1379 tmin_offset=0,
1380 tmax_seconds=None,
1381 tmax_offset=0,
1382 deltat=None,
1383 content=None,
1384 raw_content=None,
1385 tmin=None,
1386 tmax=None,
1387 values_nocheck=None):
1389 if values_nocheck is not None:
1390 (self.file_path, self.file_format, self.file_mtime,
1391 self.file_size,
1392 self.file_segment, self.file_element,
1393 self.kind_id, codes_safe_str,
1394 self.tmin_seconds, self.tmin_offset,
1395 self.tmax_seconds, self.tmax_offset,
1396 self.deltat) = values_nocheck
1398 self.codes = to_codes_simple(self.kind_id, codes_safe_str)
1399 self.deltat = self.deltat or None
1400 self.raw_content = {}
1401 self.content = None
1402 else:
1403 if tmin is not None:
1404 tmin_seconds, tmin_offset = tsplit(tmin)
1406 if tmax is not None:
1407 tmax_seconds, tmax_offset = tsplit(tmax)
1409 self.kind_id = int(kind_id)
1410 self.codes = codes
1411 self.tmin_seconds = int_or_g_tmin(tmin_seconds)
1412 self.tmin_offset = int(tmin_offset)
1413 self.tmax_seconds = int_or_g_tmax(tmax_seconds)
1414 self.tmax_offset = int(tmax_offset)
1415 self.deltat = float_or_none(deltat)
1416 self.file_path = str_or_none(file_path)
1417 self.file_segment = int_or_none(file_segment)
1418 self.file_element = int_or_none(file_element)
1419 self.file_format = str_or_none(file_format)
1420 self.file_mtime = float_or_none(file_mtime)
1421 self.file_size = int_or_none(file_size)
1422 self.content = content
1423 if raw_content is None:
1424 self.raw_content = {}
1425 else:
1426 self.raw_content = raw_content
1428 Object.__init__(self, init_props=False)
1430 def __eq__(self, other):
1431 return (isinstance(other, Nut) and
1432 self.equality_values == other.equality_values)
1434 def hash(self):
1435 return ehash(','.join(str(x) for x in self.key))
1437 def __ne__(self, other):
1438 return not (self == other)
1440 def get_io_backend(self):
1441 from . import io
1442 return io.get_backend(self.file_format)
1444 def file_modified(self):
1445 return self.get_io_backend().get_stats(self.file_path) \
1446 != (self.file_mtime, self.file_size)
1448 @property
1449 def dkey(self):
1450 return (self.kind_id, self.tmin_seconds, self.tmin_offset, self.codes)
1452 @property
1453 def key(self):
1454 return (
1455 self.file_path,
1456 self.file_segment,
1457 self.file_element,
1458 self.file_mtime)
1460 @property
1461 def equality_values(self):
1462 return (
1463 self.file_segment, self.file_element,
1464 self.kind_id, self.codes,
1465 self.tmin_seconds, self.tmin_offset,
1466 self.tmax_seconds, self.tmax_offset, self.deltat)
1468 def diff(self, other):
1469 names = [
1470 'file_segment', 'file_element', 'kind_id', 'codes',
1471 'tmin_seconds', 'tmin_offset', 'tmax_seconds', 'tmax_offset',
1472 'deltat']
1474 d = []
1475 for name, a, b in zip(
1476 names, self.equality_values, other.equality_values):
1478 if a != b:
1479 d.append((name, a, b))
1481 return d
1483 @property
1484 def tmin(self):
1485 return tjoin(self.tmin_seconds, self.tmin_offset)
1487 @tmin.setter
1488 def tmin(self, t):
1489 self.tmin_seconds, self.tmin_offset = tsplit(t)
1491 @property
1492 def tmax(self):
1493 return tjoin(self.tmax_seconds, self.tmax_offset)
1495 @tmax.setter
1496 def tmax(self, t):
1497 self.tmax_seconds, self.tmax_offset = tsplit(t)
1499 @property
1500 def kscale(self):
1501 if self.tmin_seconds is None or self.tmax_seconds is None:
1502 return 0
1503 return tscale_to_kscale(self.tmax_seconds - self.tmin_seconds)
1505 @property
1506 def waveform_kwargs(self):
1507 network, station, location, channel, extra = self.codes
1509 return dict(
1510 network=network,
1511 station=station,
1512 location=location,
1513 channel=channel,
1514 extra=extra,
1515 tmin=self.tmin,
1516 tmax=self.tmax,
1517 deltat=self.deltat)
1519 @property
1520 def waveform_promise_kwargs(self):
1521 return dict(
1522 codes=self.codes,
1523 tmin=self.tmin,
1524 tmax=self.tmax,
1525 deltat=self.deltat)
1527 @property
1528 def station_kwargs(self):
1529 network, station, location = self.codes
1530 return dict(
1531 codes=self.codes,
1532 tmin=tmin_or_none(self.tmin),
1533 tmax=tmax_or_none(self.tmax))
1535 @property
1536 def channel_kwargs(self):
1537 network, station, location, channel, extra = self.codes
1538 return dict(
1539 codes=self.codes,
1540 tmin=tmin_or_none(self.tmin),
1541 tmax=tmax_or_none(self.tmax),
1542 deltat=self.deltat)
1544 @property
1545 def response_kwargs(self):
1546 return dict(
1547 codes=self.codes,
1548 tmin=tmin_or_none(self.tmin),
1549 tmax=tmax_or_none(self.tmax),
1550 deltat=self.deltat)
1552 @property
1553 def event_kwargs(self):
1554 return dict(
1555 name=self.codes,
1556 time=self.tmin,
1557 duration=(self.tmax - self.tmin) or None)
1559 @property
1560 def trace_kwargs(self):
1561 network, station, location, channel, extra = self.codes
1563 return dict(
1564 network=network,
1565 station=station,
1566 location=location,
1567 channel=channel,
1568 extra=extra,
1569 tmin=self.tmin,
1570 tmax=self.tmax-self.deltat,
1571 deltat=self.deltat)
1573 @property
1574 def dummy_trace(self):
1575 return DummyTrace(self)
1577 @property
1578 def summary_entries(self):
1579 if self.tmin == self.tmax:
1580 ts = util.time_to_str(self.tmin)
1581 else:
1582 ts = '%s - %s' % (
1583 util.time_to_str(self.tmin),
1584 util.time_to_str(self.tmax))
1586 return (
1587 self.__class__.__name__,
1588 to_kind(self.kind_id),
1589 str(self.codes),
1590 ts)
1592 @property
1593 def summary(self):
1594 return util.fmt_summary(self.summary_entries, (10, 16, 20, 0))
1597def make_waveform_nut(**kwargs):
1598 return Nut(kind_id=WAVEFORM, **kwargs)
1601def make_waveform_promise_nut(**kwargs):
1602 return Nut(kind_id=WAVEFORM_PROMISE, **kwargs)
1605def make_station_nut(**kwargs):
1606 return Nut(kind_id=STATION, **kwargs)
1609def make_channel_nut(**kwargs):
1610 return Nut(kind_id=CHANNEL, **kwargs)
1613def make_response_nut(**kwargs):
1614 return Nut(kind_id=RESPONSE, **kwargs)
1617def make_event_nut(**kwargs):
1618 return Nut(kind_id=EVENT, **kwargs)
1621def group_channels(nuts):
1622 by_ansl = {}
1623 for nut in nuts:
1624 if nut.kind_id != CHANNEL:
1625 continue
1627 ansl = nut.codes[:4]
1629 if ansl not in by_ansl:
1630 by_ansl[ansl] = {}
1632 group = by_ansl[ansl]
1634 k = nut.codes[4][:-1], nut.deltat, nut.tmin, nut.tmax
1636 if k not in group:
1637 group[k] = set()
1639 group.add(nut.codes[4])
1641 return by_ansl
1644class DummyTrace(object):
1646 def __init__(self, nut):
1647 self.nut = nut
1648 self.codes = nut.codes
1649 self.meta = {}
1651 @property
1652 def tmin(self):
1653 return self.nut.tmin
1655 @property
1656 def tmax(self):
1657 return self.nut.tmax
1659 @property
1660 def deltat(self):
1661 return self.nut.deltat or 0.0
1663 @property
1664 def nslc_id(self):
1665 return self.codes.nslc
1667 @property
1668 def network(self):
1669 return self.codes.network
1671 @property
1672 def station(self):
1673 return self.codes.station
1675 @property
1676 def location(self):
1677 return self.codes.location
1679 @property
1680 def channel(self):
1681 return self.codes.channel
1683 @property
1684 def extra(self):
1685 return self.codes.extra
1687 def overlaps(self, tmin, tmax):
1688 return not (tmax < self.nut.tmin or self.nut.tmax < tmin)
1691def duration_to_str(t):
1692 if t > 24*3600:
1693 return '%gd' % (t / (24.*3600.))
1694 elif t > 3600:
1695 return '%gh' % (t / 3600.)
1696 elif t > 60:
1697 return '%gm' % (t / 60.)
1698 else:
1699 return '%gs' % t
1702class Coverage(Object):
1703 '''
1704 Information about times covered by a waveform or other time series data.
1705 '''
1706 kind_id = Int.T(
1707 help='Content type.')
1708 pattern = Codes.T(
1709 help='The codes pattern in the request, which caused this entry to '
1710 'match.')
1711 codes = Codes.T(
1712 help='NSLCE or NSL codes identifier of the time series.')
1713 deltat = Float.T(
1714 help='Sampling interval [s]',
1715 optional=True)
1716 tmin = Timestamp.T(
1717 help='Global start time of time series.',
1718 optional=True)
1719 tmax = Timestamp.T(
1720 help='Global end time of time series.',
1721 optional=True)
1722 changes = List.T(
1723 Tuple.T(2, Any.T()),
1724 help='List of change points, with entries of the form '
1725 '``(time, count)``, where a ``count`` of zero indicates start of '
1726 'a gap, a value of 1 start of normal data coverage and a higher '
1727 'value duplicate or redundant data coverage.')
1729 @classmethod
1730 def from_values(cls, args):
1731 pattern, codes, deltat, tmin, tmax, changes, kind_id = args
1732 return cls(
1733 kind_id=kind_id,
1734 pattern=pattern,
1735 codes=codes,
1736 deltat=deltat,
1737 tmin=tmin,
1738 tmax=tmax,
1739 changes=changes)
1741 @property
1742 def summary_entries(self):
1743 ts = '%s - %s' % (
1744 util.time_to_str(self.tmin),
1745 util.time_to_str(self.tmax))
1747 srate = self.sample_rate
1749 total = self.total
1751 return (
1752 self.__class__.__name__,
1753 to_kind(self.kind_id),
1754 str(self.codes),
1755 ts,
1756 '%10.3g' % srate if srate else '',
1757 '%i' % len(self.changes),
1758 '%s' % duration_to_str(total) if total else 'none')
1760 @property
1761 def summary(self):
1762 return util.fmt_summary(
1763 self.summary_entries,
1764 (10, 16, 20, 55, 10, 4, 0))
1766 @property
1767 def sample_rate(self):
1768 if self.deltat is None:
1769 return None
1770 elif self.deltat == 0.0:
1771 return 0.0
1772 else:
1773 return 1.0 / self.deltat
1775 @property
1776 def labels(self):
1777 srate = self.sample_rate
1778 return (
1779 ('%s' % str(self.codes)),
1780 '%.4g' % srate if srate else '')
1782 @property
1783 def total(self):
1784 total_t = None
1785 for tmin, tmax, _ in self.iter_spans():
1786 total_t = (total_t or 0.0) + (tmax - tmin)
1788 return total_t
1790 def iter_spans(self):
1791 last = None
1792 for (t, count) in self.changes:
1793 if last is not None:
1794 last_t, last_count = last
1795 if last_count > 0:
1796 yield last_t, t, last_count
1798 last = (t, count)
1800 def iter_uncovered_by(self, other):
1801 a = self
1802 b = other
1803 ia = ib = -1
1804 ca = cb = 0
1805 last = None
1806 while not (ib + 1 == len(b.changes) and ia + 1 == len(a.changes)):
1807 if ib + 1 == len(b.changes):
1808 ia += 1
1809 t, ca = a.changes[ia]
1810 elif ia + 1 == len(a.changes):
1811 ib += 1
1812 t, cb = b.changes[ib]
1813 elif a.changes[ia+1][0] < b.changes[ib+1][0]:
1814 ia += 1
1815 t, ca = a.changes[ia]
1816 else:
1817 ib += 1
1818 t, cb = b.changes[ib]
1820 if last is not None:
1821 tl, cal, cbl = last
1822 if tl < t and cal > 0 and cbl == 0:
1823 yield tl, t, ia, ib
1825 last = (t, ca, cb)
1827 def iter_uncovered_by_combined(self, other):
1828 ib_last = None
1829 group = None
1830 for tmin, tmax, _, ib in self.iter_uncovered_by(other):
1831 if ib_last is None or ib != ib_last:
1832 if group:
1833 yield (group[0][0], group[-1][1])
1835 group = []
1837 group.append((tmin, tmax))
1838 ib_last = ib
1840 if group:
1841 yield (group[0][0], group[-1][1])
1844__all__ = [
1845 'UNDEFINED',
1846 'WAVEFORM',
1847 'STATION',
1848 'CHANNEL',
1849 'RESPONSE',
1850 'EVENT',
1851 'WAVEFORM_PROMISE',
1852 'EMPTY',
1853 'to_codes',
1854 'to_codes_guess',
1855 'to_codes_simple',
1856 'to_kind',
1857 'to_kinds',
1858 'to_kind_id',
1859 'to_kind_ids',
1860 'match_codes',
1861 'codes_patterns_for_kind',
1862 'CodesMatcher',
1863 'CodesError',
1864 'Codes',
1865 'CodesNSLCE',
1866 'CodesNSL',
1867 'CodesX',
1868 'Station',
1869 'Channel',
1870 'Sensor',
1871 'Response',
1872 'Nut',
1873 'Coverage',
1874 'WaveformPromise',
1875]