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

1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6''' 

7Data model and content types handled by the Squirrel framework. 

8 

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. 

14 

15Further content types are defined here to handle waveform orders, waveform 

16promises, data coverage and sensor information. 

17''' 

18 

19import re 

20import fnmatch 

21import math 

22import hashlib 

23from os import urandom 

24from base64 import urlsafe_b64encode 

25from collections import defaultdict, namedtuple 

26 

27import numpy as num 

28 

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 

36 

37from .error import ConversionError, SquirrelError 

38 

39d2r = num.pi / 180. 

40r2d = 1.0 / d2r 

41 

42 

43guts_prefix = 'squirrel' 

44 

45 

46def mkvec(x, y, z): 

47 return num.array([x, y, z], dtype=float) 

48 

49 

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)])) 

54 

55 

56g_codes_pool = {} 

57 

58 

59class CodesError(SquirrelError): 

60 pass 

61 

62 

63class Codes(SObject): 

64 def __new__(cls, string): 

65 return to_codes_guess(string) 

66 

67 

68def normalize_nslce(*args, **kwargs): 

69 if args and kwargs: 

70 raise ValueError('Either *args or **kwargs accepted, not both.') 

71 

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])) 

79 

80 nargs = len(args) 

81 if nargs == 5: 

82 t = args 

83 

84 elif nargs == 4: 

85 t = args + ('',) 

86 

87 elif nargs == 0: 

88 d = dict( 

89 network='', 

90 station='', 

91 location='', 

92 channel='', 

93 extra='') 

94 

95 d.update(kwargs) 

96 t = tuple(kwargs.get(k, '') for k in ( 

97 'network', 'station', 'location', 'channel', 'extra')) 

98 

99 else: 

100 raise CodesError( 

101 'Does not match NSLC or NSLCE codes pattern: %s' % '.'.join(args)) 

102 

103 if '.'.join(t).count('.') != 4: 

104 raise CodesError( 

105 'Codes may not contain a ".": "%s", "%s", "%s", "%s", "%s"' % t) 

106 

107 return t 

108 

109 

110CodesNSLCEBase = namedtuple( 

111 'CodesNSLCEBase', [ 

112 'network', 'station', 'location', 'channel', 'extra']) 

113 

114 

115class CodesNSLCE(CodesNSLCEBase, Codes): 

116 ''' 

117 Codes denominating a seismic channel (NSLC or NSLCE). 

118 

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 ''' 

123 

124 __slots__ = () 

125 __hash__ = CodesNSLCEBase.__hash__ 

126 

127 as_dict = CodesNSLCEBase._asdict 

128 

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) 

141 

142 x = CodesNSLCEBase.__new__(cls, *t) 

143 return g_codes_pool.setdefault(x, x) 

144 

145 def __init__(self, *args, **kwargs): 

146 Codes.__init__(self) 

147 

148 def __str__(self): 

149 if self.extra == '': 

150 return '.'.join(self[:-1]) 

151 else: 

152 return '.'.join(self) 

153 

154 def __eq__(self, other): 

155 if not isinstance(other, CodesNSLCE): 

156 other = CodesNSLCE(other) 

157 

158 return CodesNSLCEBase.__eq__(self, other) 

159 

160 def matches(self, pattern): 

161 if not isinstance(pattern, CodesNSLCE): 

162 pattern = CodesNSLCE(pattern) 

163 

164 return match_codes(pattern, self) 

165 

166 @property 

167 def safe_str(self): 

168 return '.'.join(self) 

169 

170 @property 

171 def nslce(self): 

172 return self[:5] 

173 

174 @property 

175 def nslc(self): 

176 return self[:4] 

177 

178 @property 

179 def nsl(self): 

180 return self[:3] 

181 

182 @property 

183 def ns(self): 

184 return self[:2] 

185 

186 @property 

187 def codes_nsl(self): 

188 return CodesNSL(self) 

189 

190 @property 

191 def codes_nsl_star(self): 

192 return CodesNSL(self.network, self.station, '*') 

193 

194 def as_tuple(self): 

195 return tuple(self) 

196 

197 def replace(self, **kwargs): 

198 x = CodesNSLCEBase._replace(self, **kwargs) 

199 return g_codes_pool.setdefault(x, x) 

200 

201 

202def normalize_nsl(*args, **kwargs): 

203 if args and kwargs: 

204 raise ValueError('Either *args or **kwargs accepted, not both.') 

205 

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])) 

213 

214 nargs = len(args) 

215 if nargs == 3: 

216 t = args 

217 

218 elif nargs == 0: 

219 d = dict( 

220 network='', 

221 station='', 

222 location='') 

223 

224 d.update(kwargs) 

225 t = tuple(kwargs.get(k, '') for k in ( 

226 'network', 'station', 'location')) 

227 

228 else: 

229 raise CodesError( 

230 'Does not match NSL codes pattern: %s' % '.'.join(args)) 

231 

232 if '.'.join(t).count('.') != 2: 

233 raise CodesError( 

234 'Codes may not contain a ".": "%s", "%s", "%s"' % t) 

235 

236 return t 

237 

238 

239CodesNSLBase = namedtuple( 

240 'CodesNSLBase', [ 

241 'network', 'station', 'location']) 

242 

243 

244class CodesNSL(CodesNSLBase, Codes): 

245 ''' 

246 Codes denominating a seismic station (NSL). 

247 

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 ''' 

252 

253 __slots__ = () 

254 __hash__ = CodesNSLBase.__hash__ 

255 

256 as_dict = CodesNSLBase._asdict 

257 

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) 

270 

271 x = CodesNSLBase.__new__(cls, *t) 

272 return g_codes_pool.setdefault(x, x) 

273 

274 def __init__(self, *args, **kwargs): 

275 Codes.__init__(self) 

276 

277 def __str__(self): 

278 return '.'.join(self) 

279 

280 def __eq__(self, other): 

281 if not isinstance(other, CodesNSL): 

282 other = CodesNSL(other) 

283 

284 return CodesNSLBase.__eq__(self, other) 

285 

286 def matches(self, pattern): 

287 if not isinstance(pattern, CodesNSL): 

288 pattern = CodesNSL(pattern) 

289 

290 return match_codes(pattern, self) 

291 

292 @property 

293 def safe_str(self): 

294 return '.'.join(self) 

295 

296 @property 

297 def ns(self): 

298 return self[:2] 

299 

300 @property 

301 def nsl(self): 

302 return self[:3] 

303 

304 def as_tuple(self): 

305 return tuple(self) 

306 

307 def replace(self, **kwargs): 

308 x = CodesNSLBase._replace(self, **kwargs) 

309 return g_codes_pool.setdefault(x, x) 

310 

311 

312CodesXBase = namedtuple( 

313 'CodesXBase', [ 

314 'name']) 

315 

316 

317class CodesX(CodesXBase, Codes): 

318 ''' 

319 General purpose codes for anything other than channels or stations. 

320 ''' 

321 

322 __slots__ = () 

323 __hash__ = CodesXBase.__hash__ 

324 __eq__ = CodesXBase.__eq__ 

325 

326 as_dict = CodesXBase._asdict 

327 

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) 

338 

339 x = CodesXBase.__new__(cls, name) 

340 return g_codes_pool.setdefault(x, x) 

341 

342 def __init__(self, *args, **kwargs): 

343 Codes.__init__(self) 

344 

345 def __str__(self): 

346 return '.'.join(self) 

347 

348 @property 

349 def safe_str(self): 

350 return '.'.join(self) 

351 

352 @property 

353 def ns(self): 

354 return self[:2] 

355 

356 def as_tuple(self): 

357 return tuple(self) 

358 

359 def replace(self, **kwargs): 

360 x = CodesXBase._replace(self, **kwargs) 

361 return g_codes_pool.setdefault(x, x) 

362 

363 

364g_codes_patterns = {} 

365 

366 

367def _is_exact(pat): 

368 return not ('*' in pat or '?' in pat or ']' in pat or '[' in pat) 

369 

370 

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) 

377 

378 return pats_exact, pats_nonexact 

379 

380 

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 

385 

386 return g_codes_patterns[spattern] 

387 

388 

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)) 

394 

395 

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] 

402 

403 def match(self, codes): 

404 scodes = codes.safe_str 

405 if scodes in self._pats_exact: 

406 return True 

407 

408 return any(rpat.match(scodes) for rpat in self._pats_nonexact) 

409 

410 

411def match_codes_any(patterns, codes): 

412 

413 pats_exact, pats_nonexact = classify_patterns(patterns) 

414 

415 if codes.safe_str in pats_exact: 

416 return True 

417 

418 return any(match_codes(pattern, codes) for pattern in patterns) 

419 

420 

421g_content_kinds = [ 

422 'undefined', 

423 'waveform', 

424 'station', 

425 'channel', 

426 'response', 

427 'event', 

428 'waveform_promise', 

429 'empty'] 

430 

431 

432g_codes_classes = [ 

433 CodesX, 

434 CodesNSLCE, 

435 CodesNSL, 

436 CodesNSLCE, 

437 CodesNSLCE, 

438 CodesX, 

439 CodesNSLCE, 

440 CodesX] 

441 

442g_codes_classes_ndot = { 

443 0: CodesX, 

444 2: CodesNSL, 

445 3: CodesNSLCE, 

446 4: CodesNSLCE} 

447 

448 

449def to_codes_simple(kind_id, codes_safe_str): 

450 return g_codes_classes[kind_id](safe_str=codes_safe_str) 

451 

452 

453def to_codes(kind_id, obj): 

454 return g_codes_classes[kind_id](obj) 

455 

456 

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) 

462 

463 

464# derived list class to enable detection of already preprocessed codes patterns 

465class codes_patterns_list(list): 

466 pass 

467 

468 

469def codes_patterns_for_kind(kind_id, codes): 

470 if isinstance(codes, codes_patterns_list): 

471 return codes 

472 

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)) 

477 

478 return lcodes 

479 

480 codes = to_codes(kind_id, codes) 

481 

482 lcodes = codes_patterns_list() 

483 lcodes.append(codes) 

484 if kind_id == STATION: 

485 lcodes.append(codes.replace(location='[*]')) 

486 

487 return lcodes 

488 

489 

490g_content_kind_ids = ( 

491 UNDEFINED, WAVEFORM, STATION, CHANNEL, RESPONSE, EVENT, 

492 WAVEFORM_PROMISE, EMPTY) = range(len(g_content_kinds)) 

493 

494 

495g_tmin, g_tmax = util.get_working_system_time_range()[:2] 

496 

497 

498try: 

499 g_tmin_queries = max(g_tmin, util.str_to_time_fillup('1900-01-01')) 

500except Exception: 

501 g_tmin_queries = g_tmin 

502 

503 

504def to_kind(kind_id): 

505 return g_content_kinds[kind_id] 

506 

507 

508def to_kinds(kind_ids): 

509 return [g_content_kinds[kind_id] for kind_id in kind_ids] 

510 

511 

512def to_kind_id(kind): 

513 return g_content_kinds.index(kind) 

514 

515 

516def to_kind_ids(kinds): 

517 return [g_content_kinds.index(kind) for kind in kinds] 

518 

519 

520g_kind_mask_all = 0xff 

521 

522 

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 

528 

529 

530def str_or_none(x): 

531 if x is None: 

532 return None 

533 else: 

534 return str(x) 

535 

536 

537def float_or_none(x): 

538 if x is None: 

539 return None 

540 else: 

541 return float(x) 

542 

543 

544def int_or_none(x): 

545 if x is None: 

546 return None 

547 else: 

548 return int(x) 

549 

550 

551def int_or_g_tmin(x): 

552 if x is None: 

553 return g_tmin 

554 else: 

555 return int(x) 

556 

557 

558def int_or_g_tmax(x): 

559 if x is None: 

560 return g_tmax 

561 else: 

562 return int(x) 

563 

564 

565def tmin_or_none(x): 

566 if x == g_tmin: 

567 return None 

568 else: 

569 return x 

570 

571 

572def tmax_or_none(x): 

573 if x == g_tmax: 

574 return None 

575 else: 

576 return x 

577 

578 

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) 

584 

585 

586def codes_to_str_abbreviated(codes, indent=' '): 

587 codes = [str(x) for x in codes] 

588 

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>' 

596 

597 return scodes 

598 

599 

600g_offset_time_unit_inv = 1000000000 

601g_offset_time_unit = 1.0 / g_offset_time_unit_inv 

602 

603 

604def tsplit(t): 

605 if t is None: 

606 return None, 0 

607 

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) 

613 

614 seconds = num.floor(t) 

615 offset = t - seconds 

616 return int(seconds), int(round(offset * g_offset_time_unit_inv)) 

617 

618 

619def tjoin(seconds, offset): 

620 if seconds is None: 

621 return None 

622 

623 return util.to_time_float(seconds) \ 

624 + util.to_time_float(offset*g_offset_time_unit) 

625 

626 

627tscale_min = 1 

628tscale_max = 365 * 24 * 3600 # last edge is one above 

629tscale_logbase = 20 

630 

631tscale_edges = [tscale_min] 

632while True: 

633 tscale_edges.append(tscale_edges[-1]*tscale_logbase) 

634 if tscale_edges[-1] >= tscale_max: 

635 break 

636 

637 

638tscale_edges = num.array(tscale_edges) 

639 

640 

641def tscale_to_kscale(tscale): 

642 

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) 

647 

648 return int(num.searchsorted(tscale_edges, tscale)) 

649 

650 

651@squirrel_content 

652class Station(Location): 

653 ''' 

654 A seismic station. 

655 ''' 

656 

657 codes = CodesNSL.T() 

658 

659 tmin = Timestamp.T(optional=True) 

660 tmax = Timestamp.T(optional=True) 

661 

662 description = Unicode.T(optional=True) 

663 

664 def __init__(self, **kwargs): 

665 kwargs['codes'] = CodesNSL(kwargs['codes']) 

666 Location.__init__(self, **kwargs) 

667 

668 @property 

669 def time_span(self): 

670 return (self.tmin, self.tmax) 

671 

672 def get_pyrocko_station(self): 

673 ''' 

674 Get station as a classic Pyrocko station object. 

675 

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()) 

683 

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) 

695 

696 

697@squirrel_content 

698class ChannelBase(Location): 

699 codes = CodesNSLCE.T() 

700 

701 tmin = Timestamp.T(optional=True) 

702 tmax = Timestamp.T(optional=True) 

703 

704 deltat = Float.T(optional=True) 

705 

706 def __init__(self, **kwargs): 

707 kwargs['codes'] = CodesNSLCE(kwargs['codes']) 

708 Location.__init__(self, **kwargs) 

709 

710 @property 

711 def time_span(self): 

712 return (self.tmin, self.tmax) 

713 

714 def _get_sensor_codes(self): 

715 return self.codes.replace( 

716 channel=self.codes.channel[:-1] + '?') 

717 

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) 

724 

725 return tuple(getattr_rep(k) for k in ChannelBase.T.propnames) 

726 

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) 

734 

735 return tuple(getattr_rep(k) for k in ChannelBase.T.propnames) 

736 

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) 

748 

749 

750class Channel(ChannelBase): 

751 ''' 

752 A channel of a seismic station. 

753 ''' 

754 

755 dip = Float.T(optional=True) 

756 azimuth = Float.T(optional=True) 

757 

758 def get_pyrocko_channel(self): 

759 from pyrocko import model 

760 return model.Channel(*self._get_pyrocko_channel_args()) 

761 

762 def _get_pyrocko_channel_args(self): 

763 return ( 

764 self.codes.channel, 

765 self.azimuth, 

766 self.dip) 

767 

768 @property 

769 def orientation_enz(self): 

770 if None in (self.azimuth, self.dip): 

771 return None 

772 

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) 

777 

778 

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) 

787 

788 times = sorted(times) 

789 

790 if any(channel.tmin is None for channel in channels): 

791 times[0:0] = [None] 

792 

793 if any(channel.tmax is None for channel in channels): 

794 times.append(None) 

795 

796 if len(times) <= 2: 

797 return channels 

798 

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))): 

808 

809 channel_out = clone(channel) 

810 channel_out.tmin = tmin 

811 channel_out.tmax = tmax 

812 channels_out.append(channel_out) 

813 

814 return channels_out 

815 

816 

817class Sensor(ChannelBase): 

818 ''' 

819 Representation of a channel group. 

820 ''' 

821 

822 channels = List.T(Channel.T()) 

823 

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) 

829 

830 channels_cut = [] 

831 for group in groups.values(): 

832 channels_cut.extend(cut_intervals(group)) 

833 

834 groups = defaultdict(list) 

835 for channel in channels_cut: 

836 groups[channel._get_sensor_args()].append(channel) 

837 

838 return [ 

839 cls(channels=channels, 

840 **dict(zip(ChannelBase.T.propnames, args))) 

841 for args, channels in groups.items()] 

842 

843 def channel_vectors(self): 

844 return num.vstack( 

845 [channel.orientation_enz for channel in self.channels]) 

846 

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)] 

856 

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 

861 

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)) 

867 

868 def projection_to_enz(self): 

869 return self.projection_to(num.identity(3), 'ENZ') 

870 

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. 

876 

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') 

881 

882 def project_to_enz(self, traces): 

883 from pyrocko import trace 

884 

885 matrix, in_channels, out_channels = self.projection_to_enz() 

886 

887 return trace.project(traces, matrix, in_channels, out_channels) 

888 

889 def project_to_trz(self, source, traces, azimuth=None): 

890 from pyrocko import trace 

891 

892 matrix, in_channels, out_channels = self.projection_to_trz( 

893 source, azimuth=azimuth) 

894 

895 return trace.project(traces, matrix, in_channels, out_channels) 

896 

897 

898observational_quantities = [ 

899 'acceleration', 'velocity', 'displacement', 'pressure', 

900 'rotation_displacement', 'rotation_velocity', 'rotation_acceleration', 

901 'temperature'] 

902 

903 

904technical_quantities = [ 

905 'voltage', 'counts'] 

906 

907 

908class QuantityType(StringChoice): 

909 ''' 

910 Choice of observational or technical quantity. 

911 

912 SI units are used for all quantities, where applicable. 

913 ''' 

914 choices = observational_quantities + technical_quantities 

915 

916 

917class ResponseStage(Object): 

918 ''' 

919 Representation of a response stage. 

920 

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())) 

931 

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' 

937 

938 if self.input_quantity in observational_quantities \ 

939 and self.output_quantity == 'voltage': 

940 return 'sensor' 

941 

942 elif self.input_quantity == 'voltage' \ 

943 and self.output_quantity == 'voltage': 

944 return 'electronics' 

945 

946 elif self.input_quantity == 'voltage' \ 

947 and self.output_quantity == 'counts': 

948 return 'digitizer' 

949 

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' 

954 

955 elif self.input_quantity in observational_quantities \ 

956 and self.output_quantity == 'counts': 

957 return 'combination' 

958 

959 else: 

960 return 'unknown' 

961 

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: 

968 

969 return irate / orate 

970 else: 

971 return None 

972 

973 @property 

974 def summary_quantities(self): 

975 return '%s -> %s' % ( 

976 self.input_quantity or '?', 

977 self.output_quantity or '?') 

978 

979 @property 

980 def summary_rates(self): 

981 irate = self.input_sample_rate 

982 orate = self.output_sample_rate 

983 factor = self.decimation_factor 

984 

985 if irate and orate is None: 

986 return '%g Hz' % irate 

987 

988 elif orate and irate is None: 

989 return '%g Hz' % orate 

990 

991 elif irate and orate and irate == orate: 

992 return '%g Hz' % irate 

993 

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 '' 

1001 

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') 

1006 

1007 @property 

1008 def summary_log(self): 

1009 return ''.join(sorted(set(x[0][0].upper() for x in self.log))) 

1010 

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) 

1020 

1021 @property 

1022 def summary(self): 

1023 return util.fmt_summary(self.summary_entries, (10, 15, 3, 30, 30, 0)) 

1024 

1025 def get_effective(self): 

1026 return MultiplyResponse(responses=list(self.elements)) 

1027 

1028 

1029D = 'displacement' 

1030V = 'velocity' 

1031A = 'acceleration' 

1032 

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)} 

1040 

1041 

1042def response_converters(input_quantity, output_quantity): 

1043 if input_quantity is None or input_quantity == output_quantity: 

1044 return [] 

1045 

1046 if output_quantity is None: 

1047 raise ConversionError('Unspecified target quantity.') 

1048 

1049 try: 

1050 return [g_converters[input_quantity, output_quantity]] 

1051 

1052 except KeyError: 

1053 raise ConversionError('No rule to convert from "%s" to "%s".' % ( 

1054 input_quantity, output_quantity)) 

1055 

1056 

1057@squirrel_content 

1058class Response(Object): 

1059 ''' 

1060 The instrument response of a seismic station channel. 

1061 ''' 

1062 

1063 codes = CodesNSLCE.T() 

1064 tmin = Timestamp.T(optional=True) 

1065 tmax = Timestamp.T(optional=True) 

1066 

1067 stages = List.T(ResponseStage.T()) 

1068 checkpoints = List.T(FrequencyResponseCheckpoint.T()) 

1069 

1070 deltat = Float.T(optional=True) 

1071 log = List.T(Tuple.T(3, String.T())) 

1072 

1073 def __init__(self, **kwargs): 

1074 kwargs['codes'] = CodesNSLCE(kwargs['codes']) 

1075 Object.__init__(self, **kwargs) 

1076 self._effective_responses_cache = {} 

1077 

1078 @property 

1079 def time_span(self): 

1080 return (self.tmin, self.tmax) 

1081 

1082 @property 

1083 def nstages(self): 

1084 return len(self.stages) 

1085 

1086 @property 

1087 def input_quantity(self): 

1088 return self.stages[0].input_quantity if self.stages else None 

1089 

1090 @property 

1091 def output_quantity(self): 

1092 return self.stages[-1].output_quantity if self.stages else None 

1093 

1094 @property 

1095 def output_sample_rate(self): 

1096 return self.stages[-1].output_sample_rate if self.stages else None 

1097 

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 = [] 

1108 

1109 if g: 

1110 yield g 

1111 

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)) 

1115 

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 '') 

1123 

1124 @property 

1125 def summary_log(self): 

1126 return ''.join(sorted(set(x[0][0].upper() for x in self.log))) 

1127 

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) 

1137 

1138 @property 

1139 def summary(self): 

1140 return util.fmt_summary(self.summary_entries, (10, 20, 55, 3, 35, 0)) 

1141 

1142 def get_effective( 

1143 self, 

1144 input_quantity=None, 

1145 stages=(None, None), 

1146 mode='complete', 

1147 gain_frequency=None): 

1148 

1149 assert mode in ('complete', 'sensor') 

1150 assert not (mode == 'sensor' and gain_frequency is None) 

1151 

1152 cache_key = (input_quantity, stages) 

1153 if cache_key in self._effective_responses_cache: 

1154 return self._effective_responses_cache[cache_key] 

1155 

1156 try: 

1157 elements = response_converters(input_quantity, self.input_quantity) 

1158 except ConversionError as e: 

1159 raise ConversionError(str(e) + ' (%s)' % self.summary) 

1160 

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]): 

1164 

1165 is_sensor = istage == 0 or stage.stage_type == 'sensor' 

1166 

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) 

1175 

1176 if input_quantity is None \ 

1177 or input_quantity == self.input_quantity: 

1178 checkpoints = self.checkpoints 

1179 else: 

1180 checkpoints = [] 

1181 

1182 resp = MultiplyResponse( 

1183 responses=simplify_responses(elements), 

1184 checkpoints=checkpoints) 

1185 

1186 self._effective_responses_cache[cache_key] = resp 

1187 return resp 

1188 

1189 

1190@squirrel_content 

1191class Event(Object): 

1192 ''' 

1193 A seismic event. 

1194 ''' 

1195 

1196 name = String.T(optional=True) 

1197 time = Timestamp.T() 

1198 duration = Float.T(optional=True) 

1199 

1200 lat = Float.T() 

1201 lon = Float.T() 

1202 depth = Float.T(optional=True) 

1203 

1204 magnitude = Float.T(optional=True) 

1205 

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) 

1216 

1217 @property 

1218 def time_span(self): 

1219 return (self.time, self.time) 

1220 

1221 

1222def ehash(s): 

1223 return hashlib.sha1(s.encode('utf8')).hexdigest() 

1224 

1225 

1226def random_name(n=8): 

1227 return urlsafe_b64encode(urandom(n)).rstrip(b'=').decode('ascii') 

1228 

1229 

1230@squirrel_content 

1231class WaveformPromise(Object): 

1232 ''' 

1233 Information about a waveform potentially downloadable from a remote site. 

1234 

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 ''' 

1248 

1249 codes = CodesNSLCE.T() 

1250 tmin = Timestamp.T() 

1251 tmax = Timestamp.T() 

1252 

1253 deltat = Float.T(optional=True) 

1254 

1255 source_hash = String.T() 

1256 

1257 def __init__(self, **kwargs): 

1258 kwargs['codes'] = CodesNSLCE(kwargs['codes']) 

1259 Object.__init__(self, **kwargs) 

1260 

1261 @property 

1262 def time_span(self): 

1263 return (self.tmin, self.tmax) 

1264 

1265 

1266class InvalidWaveform(Exception): 

1267 pass 

1268 

1269 

1270class WaveformOrder(Object): 

1271 ''' 

1272 Waveform request information. 

1273 ''' 

1274 

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.) 

1283 

1284 @property 

1285 def client(self): 

1286 return self.source_id.split(':')[1] 

1287 

1288 def key1(self): 

1289 return (self.source_id, self.codes, self.tmin, self.tmax) 

1290 

1291 def key2(self): 

1292 return (self.codes, self.tmin, self.tmax) 

1293 

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)) 

1298 

1299 def validate(self, tr): 

1300 if tr.ydata.size == 0: 

1301 raise InvalidWaveform( 

1302 'waveform with zero data samples') 

1303 

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)) 

1309 

1310 if not num.all(num.isfinite(tr.ydata)): 

1311 raise InvalidWaveform('waveform has NaN values') 

1312 

1313 def estimate_nsamples(self): 

1314 return int(round((self.tmax - self.tmin) / self.deltat))+1 

1315 

1316 def is_near_real_time(self): 

1317 return self.tmax > self.time_created - self.anxious 

1318 

1319 

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])) 

1328 

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)) 

1334 

1335 

1336class Nut(Object): 

1337 ''' 

1338 Index entry referencing an elementary piece of content. 

1339 

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 ''' 

1344 

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) 

1349 

1350 file_segment = Int.T(optional=True) 

1351 file_element = Int.T(optional=True) 

1352 

1353 kind_id = Int.T() 

1354 codes = Codes.T() 

1355 

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) 

1360 

1361 deltat = Float.T(default=0.0) 

1362 

1363 content = Any.T(optional=True) 

1364 raw_content = Dict.T(String.T(), Any.T()) 

1365 

1366 content_in_db = False 

1367 

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): 

1388 

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 

1397 

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) 

1405 

1406 if tmax is not None: 

1407 tmax_seconds, tmax_offset = tsplit(tmax) 

1408 

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 

1427 

1428 Object.__init__(self, init_props=False) 

1429 

1430 def __eq__(self, other): 

1431 return (isinstance(other, Nut) and 

1432 self.equality_values == other.equality_values) 

1433 

1434 def hash(self): 

1435 return ehash(','.join(str(x) for x in self.key)) 

1436 

1437 def __ne__(self, other): 

1438 return not (self == other) 

1439 

1440 def get_io_backend(self): 

1441 from . import io 

1442 return io.get_backend(self.file_format) 

1443 

1444 def file_modified(self): 

1445 return self.get_io_backend().get_stats(self.file_path) \ 

1446 != (self.file_mtime, self.file_size) 

1447 

1448 @property 

1449 def dkey(self): 

1450 return (self.kind_id, self.tmin_seconds, self.tmin_offset, self.codes) 

1451 

1452 @property 

1453 def key(self): 

1454 return ( 

1455 self.file_path, 

1456 self.file_segment, 

1457 self.file_element, 

1458 self.file_mtime) 

1459 

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) 

1467 

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'] 

1473 

1474 d = [] 

1475 for name, a, b in zip( 

1476 names, self.equality_values, other.equality_values): 

1477 

1478 if a != b: 

1479 d.append((name, a, b)) 

1480 

1481 return d 

1482 

1483 @property 

1484 def tmin(self): 

1485 return tjoin(self.tmin_seconds, self.tmin_offset) 

1486 

1487 @tmin.setter 

1488 def tmin(self, t): 

1489 self.tmin_seconds, self.tmin_offset = tsplit(t) 

1490 

1491 @property 

1492 def tmax(self): 

1493 return tjoin(self.tmax_seconds, self.tmax_offset) 

1494 

1495 @tmax.setter 

1496 def tmax(self, t): 

1497 self.tmax_seconds, self.tmax_offset = tsplit(t) 

1498 

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) 

1504 

1505 @property 

1506 def waveform_kwargs(self): 

1507 network, station, location, channel, extra = self.codes 

1508 

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) 

1518 

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) 

1526 

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)) 

1534 

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) 

1543 

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) 

1551 

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) 

1558 

1559 @property 

1560 def trace_kwargs(self): 

1561 network, station, location, channel, extra = self.codes 

1562 

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) 

1572 

1573 @property 

1574 def dummy_trace(self): 

1575 return DummyTrace(self) 

1576 

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)) 

1585 

1586 return ( 

1587 self.__class__.__name__, 

1588 to_kind(self.kind_id), 

1589 str(self.codes), 

1590 ts) 

1591 

1592 @property 

1593 def summary(self): 

1594 return util.fmt_summary(self.summary_entries, (10, 16, 20, 0)) 

1595 

1596 

1597def make_waveform_nut(**kwargs): 

1598 return Nut(kind_id=WAVEFORM, **kwargs) 

1599 

1600 

1601def make_waveform_promise_nut(**kwargs): 

1602 return Nut(kind_id=WAVEFORM_PROMISE, **kwargs) 

1603 

1604 

1605def make_station_nut(**kwargs): 

1606 return Nut(kind_id=STATION, **kwargs) 

1607 

1608 

1609def make_channel_nut(**kwargs): 

1610 return Nut(kind_id=CHANNEL, **kwargs) 

1611 

1612 

1613def make_response_nut(**kwargs): 

1614 return Nut(kind_id=RESPONSE, **kwargs) 

1615 

1616 

1617def make_event_nut(**kwargs): 

1618 return Nut(kind_id=EVENT, **kwargs) 

1619 

1620 

1621def group_channels(nuts): 

1622 by_ansl = {} 

1623 for nut in nuts: 

1624 if nut.kind_id != CHANNEL: 

1625 continue 

1626 

1627 ansl = nut.codes[:4] 

1628 

1629 if ansl not in by_ansl: 

1630 by_ansl[ansl] = {} 

1631 

1632 group = by_ansl[ansl] 

1633 

1634 k = nut.codes[4][:-1], nut.deltat, nut.tmin, nut.tmax 

1635 

1636 if k not in group: 

1637 group[k] = set() 

1638 

1639 group.add(nut.codes[4]) 

1640 

1641 return by_ansl 

1642 

1643 

1644class DummyTrace(object): 

1645 

1646 def __init__(self, nut): 

1647 self.nut = nut 

1648 self.codes = nut.codes 

1649 self.meta = {} 

1650 

1651 @property 

1652 def tmin(self): 

1653 return self.nut.tmin 

1654 

1655 @property 

1656 def tmax(self): 

1657 return self.nut.tmax 

1658 

1659 @property 

1660 def deltat(self): 

1661 return self.nut.deltat or 0.0 

1662 

1663 @property 

1664 def nslc_id(self): 

1665 return self.codes.nslc 

1666 

1667 @property 

1668 def network(self): 

1669 return self.codes.network 

1670 

1671 @property 

1672 def station(self): 

1673 return self.codes.station 

1674 

1675 @property 

1676 def location(self): 

1677 return self.codes.location 

1678 

1679 @property 

1680 def channel(self): 

1681 return self.codes.channel 

1682 

1683 @property 

1684 def extra(self): 

1685 return self.codes.extra 

1686 

1687 def overlaps(self, tmin, tmax): 

1688 return not (tmax < self.nut.tmin or self.nut.tmax < tmin) 

1689 

1690 

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 

1700 

1701 

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.') 

1728 

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) 

1740 

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)) 

1746 

1747 srate = self.sample_rate 

1748 

1749 total = self.total 

1750 

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') 

1759 

1760 @property 

1761 def summary(self): 

1762 return util.fmt_summary( 

1763 self.summary_entries, 

1764 (10, 16, 20, 55, 10, 4, 0)) 

1765 

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 

1774 

1775 @property 

1776 def labels(self): 

1777 srate = self.sample_rate 

1778 return ( 

1779 ('%s' % str(self.codes)), 

1780 '%.4g' % srate if srate else '') 

1781 

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) 

1787 

1788 return total_t 

1789 

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 

1797 

1798 last = (t, count) 

1799 

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] 

1819 

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 

1824 

1825 last = (t, ca, cb) 

1826 

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]) 

1834 

1835 group = [] 

1836 

1837 group.append((tmin, tmax)) 

1838 ib_last = ib 

1839 

1840 if group: 

1841 yield (group[0][0], group[-1][1]) 

1842 

1843 

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]