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 

19from __future__ import absolute_import, print_function 

20 

21import re 

22import fnmatch 

23import hashlib 

24import numpy as num 

25from os import urandom 

26from base64 import urlsafe_b64encode 

27from collections import defaultdict, namedtuple 

28 

29from pyrocko import util 

30from pyrocko.guts import Object, SObject, String, Timestamp, Float, Int, \ 

31 Unicode, Tuple, List, StringChoice, Any 

32from pyrocko.model import squirrel_content, Location 

33from pyrocko.response import FrequencyResponse, MultiplyResponse, \ 

34 IntegrationResponse, DifferentiationResponse, simplify_responses, \ 

35 FrequencyResponseCheckpoint 

36 

37from .error import ConversionError, SquirrelError 

38 

39 

40guts_prefix = 'squirrel' 

41 

42 

43g_codes_pool = {} 

44 

45 

46class CodesError(SquirrelError): 

47 pass 

48 

49 

50class Codes(SObject): 

51 pass 

52 

53 

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

55 if args and kwargs: 

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

57 

58 if len(args) == 1: 

59 if isinstance(args[0], str): 

60 args = tuple(args[0].split('.')) 

61 elif isinstance(args[0], tuple): 

62 args = args[0] 

63 else: 

64 raise ValueError('Invalid argument type: %s' % type(args[0])) 

65 

66 nargs = len(args) 

67 if nargs == 5: 

68 t = args 

69 

70 elif nargs == 4: 

71 t = args + ('',) 

72 

73 elif nargs == 0: 

74 d = dict( 

75 network='', 

76 station='', 

77 location='', 

78 channel='', 

79 extra='') 

80 

81 d.update(kwargs) 

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

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

84 

85 else: 

86 raise CodesError( 

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

88 

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

90 raise CodesError( 

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

92 

93 return t 

94 

95 

96CodesNSLCEBase = namedtuple( 

97 'CodesNSLCEBase', [ 

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

99 

100 

101class CodesNSLCE(CodesNSLCEBase, Codes): 

102 ''' 

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

104 

105 FDSN/SEED style NET.STA.LOC.CHA is accepted or NET.STA.LOC.CHA.EXTRA, where 

106 the EXTRA part in the latter form can be used to identify a custom 

107 processing applied to a channel. 

108 ''' 

109 

110 __slots__ = () 

111 __hash__ = CodesNSLCEBase.__hash__ 

112 

113 as_dict = CodesNSLCEBase._asdict 

114 

115 def __new__(cls, *args, safe_str=None, **kwargs): 

116 nargs = len(args) 

117 if nargs == 1 and isinstance(args[0], CodesNSLCE): 

118 return args[0] 

119 elif nargs == 1 and isinstance(args[0], CodesNSL): 

120 t = (args[0].tuple) + ('*', '*') 

121 elif nargs == 1 and isinstance(args[0], CodesX): 

122 t = ('*', '*', '*', '*', '*') 

123 elif safe_str is not None: 

124 t = safe_str.split('.') 

125 else: 

126 t = normalize_nslce(*args, **kwargs) 

127 

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

129 return g_codes_pool.setdefault(x, x) 

130 

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

132 Codes.__init__(self) 

133 

134 def __str__(self): 

135 if self.extra == '': 

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

137 else: 

138 return '.'.join(self) 

139 

140 def __eq__(self, other): 

141 if not isinstance(other, CodesNSLCE): 

142 other = CodesNSLCE(other) 

143 

144 return CodesNSLCEBase.__eq__(self, other) 

145 

146 @property 

147 def safe_str(self): 

148 return '.'.join(self) 

149 

150 @property 

151 def nslce(self): 

152 return self[:4] 

153 

154 @property 

155 def nslc(self): 

156 return self[:4] 

157 

158 @property 

159 def nsl(self): 

160 return self[:3] 

161 

162 @property 

163 def ns(self): 

164 return self[:2] 

165 

166 def as_tuple(self): 

167 return tuple(self) 

168 

169 def replace(self, **kwargs): 

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

171 return g_codes_pool.setdefault(x, x) 

172 

173 

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

175 if args and kwargs: 

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

177 

178 if len(args) == 1: 

179 if isinstance(args[0], str): 

180 args = tuple(args[0].split('.')) 

181 elif isinstance(args[0], tuple): 

182 args = args[0] 

183 else: 

184 raise ValueError('Invalid argument type: %s' % type(args[0])) 

185 

186 nargs = len(args) 

187 if nargs == 3: 

188 t = args 

189 

190 elif nargs == 0: 

191 d = dict( 

192 network='', 

193 station='', 

194 location='') 

195 

196 d.update(kwargs) 

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

198 'network', 'station', 'location')) 

199 

200 else: 

201 raise CodesError( 

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

203 

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

205 raise CodesError( 

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

207 

208 return t 

209 

210 

211CodesNSLBase = namedtuple( 

212 'CodesNSLBase', [ 

213 'network', 'station', 'location']) 

214 

215 

216class CodesNSL(CodesNSLBase, Codes): 

217 ''' 

218 Codes denominating a seismic station (NSL). 

219 

220 NET.STA.LOC is accepted, slightly different from SEED/StationXML, where 

221 LOC is part of the channel. By setting location='*' is possible to get 

222 compatible behaviour in most cases. 

223 ''' 

224 

225 __slots__ = () 

226 __hash__ = CodesNSLBase.__hash__ 

227 

228 as_dict = CodesNSLBase._asdict 

229 

230 def __new__(cls, *args, safe_str=None, **kwargs): 

231 nargs = len(args) 

232 if nargs == 1 and isinstance(args[0], CodesNSL): 

233 return args[0] 

234 elif nargs == 1 and isinstance(args[0], CodesNSLCE): 

235 t = args[0].nsl 

236 elif nargs == 1 and isinstance(args[0], CodesX): 

237 t = ('*', '*', '*') 

238 elif safe_str is not None: 

239 t = safe_str.split('.') 

240 else: 

241 t = normalize_nsl(*args, **kwargs) 

242 

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

244 return g_codes_pool.setdefault(x, x) 

245 

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

247 Codes.__init__(self) 

248 

249 def __str__(self): 

250 return '.'.join(self) 

251 

252 def __eq__(self, other): 

253 if not isinstance(other, CodesNSL): 

254 other = CodesNSL(other) 

255 

256 return CodesNSLBase.__eq__(self, other) 

257 

258 @property 

259 def safe_str(self): 

260 return '.'.join(self) 

261 

262 @property 

263 def ns(self): 

264 return self[:2] 

265 

266 @property 

267 def nsl(self): 

268 return self[:3] 

269 

270 def as_tuple(self): 

271 return tuple(self) 

272 

273 def replace(self, **kwargs): 

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

275 return g_codes_pool.setdefault(x, x) 

276 

277 

278CodesXBase = namedtuple( 

279 'CodesXBase', [ 

280 'name']) 

281 

282 

283class CodesX(CodesXBase, Codes): 

284 ''' 

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

286 ''' 

287 

288 __slots__ = () 

289 __hash__ = CodesXBase.__hash__ 

290 __eq__ = CodesXBase.__eq__ 

291 

292 as_dict = CodesXBase._asdict 

293 

294 def __new__(cls, name='', safe_str=None): 

295 if isinstance(name, CodesX): 

296 return name 

297 elif isinstance(name, (CodesNSLCE, CodesNSL)): 

298 name = '*' 

299 elif safe_str is not None: 

300 name = safe_str 

301 else: 

302 if '.' in name: 

303 raise CodesError('Code may not contain a ".": %s' % name) 

304 

305 x = CodesXBase.__new__(cls, name) 

306 return g_codes_pool.setdefault(x, x) 

307 

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

309 Codes.__init__(self) 

310 

311 def __str__(self): 

312 return '.'.join(self) 

313 

314 @property 

315 def safe_str(self): 

316 return '.'.join(self) 

317 

318 @property 

319 def ns(self): 

320 return self[:2] 

321 

322 def as_tuple(self): 

323 return tuple(self) 

324 

325 def replace(self, **kwargs): 

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

327 return g_codes_pool.setdefault(x, x) 

328 

329 

330g_codes_patterns = {} 

331 

332 

333def match_codes(pattern, codes): 

334 spattern = pattern.safe_str 

335 scodes = codes.safe_str 

336 if spattern not in g_codes_patterns: 

337 rpattern = re.compile(fnmatch.translate(spattern), re.I) 

338 g_codes_patterns[spattern] = rpattern 

339 

340 rpattern = g_codes_patterns[spattern] 

341 return bool(rpattern.match(scodes)) 

342 

343 

344g_content_kinds = [ 

345 'undefined', 

346 'waveform', 

347 'station', 

348 'channel', 

349 'response', 

350 'event', 

351 'waveform_promise'] 

352 

353 

354g_codes_classes = [ 

355 CodesX, 

356 CodesNSLCE, 

357 CodesNSL, 

358 CodesNSLCE, 

359 CodesNSLCE, 

360 CodesX, 

361 CodesNSLCE] 

362 

363 

364def to_codes_simple(kind_id, codes_safe_str): 

365 return g_codes_classes[kind_id](safe_str=codes_safe_str) 

366 

367 

368def to_codes(kind_id, obj): 

369 return g_codes_classes[kind_id](obj) 

370 

371 

372g_content_kind_ids = ( 

373 UNDEFINED, WAVEFORM, STATION, CHANNEL, RESPONSE, EVENT, 

374 WAVEFORM_PROMISE) = range(len(g_content_kinds)) 

375 

376 

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

378 

379 

380try: 

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

382except Exception: 

383 g_tmin_queries = g_tmin 

384 

385 

386def to_kind(kind_id): 

387 return g_content_kinds[kind_id] 

388 

389 

390def to_kinds(kind_ids): 

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

392 

393 

394def to_kind_id(kind): 

395 return g_content_kinds.index(kind) 

396 

397 

398def to_kind_ids(kinds): 

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

400 

401 

402g_kind_mask_all = 0xff 

403 

404 

405def to_kind_mask(kinds): 

406 if kinds: 

407 return sum(1 << kind_id for kind_id in to_kind_ids(kinds)) 

408 else: 

409 return g_kind_mask_all 

410 

411 

412def str_or_none(x): 

413 if x is None: 

414 return None 

415 else: 

416 return str(x) 

417 

418 

419def float_or_none(x): 

420 if x is None: 

421 return None 

422 else: 

423 return float(x) 

424 

425 

426def int_or_none(x): 

427 if x is None: 

428 return None 

429 else: 

430 return int(x) 

431 

432 

433def int_or_g_tmin(x): 

434 if x is None: 

435 return g_tmin 

436 else: 

437 return int(x) 

438 

439 

440def int_or_g_tmax(x): 

441 if x is None: 

442 return g_tmax 

443 else: 

444 return int(x) 

445 

446 

447def tmin_or_none(x): 

448 if x == g_tmin: 

449 return None 

450 else: 

451 return x 

452 

453 

454def tmax_or_none(x): 

455 if x == g_tmax: 

456 return None 

457 else: 

458 return x 

459 

460 

461def time_or_none_to_str(x): 

462 if x is None: 

463 return '...'.ljust(17) 

464 else: 

465 return util.time_to_str(x) 

466 

467 

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

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

470 

471 if len(codes) > 20: 

472 scodes = '\n' + util.ewrap(codes[:10], indent=indent) \ 

473 + '\n%s[%i more]\n' % (indent, len(codes) - 20) \ 

474 + util.ewrap(codes[-10:], indent=' ') 

475 else: 

476 scodes = '\n' + util.ewrap(codes, indent=indent) \ 

477 if codes else '<none>' 

478 

479 return scodes 

480 

481 

482g_offset_time_unit_inv = 1000000000 

483g_offset_time_unit = 1.0 / g_offset_time_unit_inv 

484 

485 

486def tsplit(t): 

487 if t is None: 

488 return None, 0.0 

489 

490 t = util.to_time_float(t) 

491 if type(t) is float: 

492 t = round(t, 5) 

493 else: 

494 t = round(t, 9) 

495 

496 seconds = num.floor(t) 

497 offset = t - seconds 

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

499 

500 

501def tjoin(seconds, offset): 

502 if seconds is None: 

503 return None 

504 

505 return util.to_time_float(seconds) \ 

506 + util.to_time_float(offset*g_offset_time_unit) 

507 

508 

509tscale_min = 1 

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

511tscale_logbase = 20 

512 

513tscale_edges = [tscale_min] 

514while True: 

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

516 if tscale_edges[-1] >= tscale_max: 

517 break 

518 

519 

520tscale_edges = num.array(tscale_edges) 

521 

522 

523def tscale_to_kscale(tscale): 

524 

525 # 0 <= x < tscale_edges[1]: 0 

526 # tscale_edges[1] <= x < tscale_edges[2]: 1 

527 # ... 

528 # tscale_edges[len(tscale_edges)-1] <= x: len(tscale_edges) 

529 

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

531 

532 

533@squirrel_content 

534class Station(Location): 

535 ''' 

536 A seismic station. 

537 ''' 

538 

539 codes = CodesNSL.T() 

540 

541 tmin = Timestamp.T(optional=True) 

542 tmax = Timestamp.T(optional=True) 

543 

544 description = Unicode.T(optional=True) 

545 

546 def __init__(self, **kwargs): 

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

548 Location.__init__(self, **kwargs) 

549 

550 @property 

551 def time_span(self): 

552 return (self.tmin, self.tmax) 

553 

554 def get_pyrocko_station(self): 

555 from pyrocko import model 

556 return model.Station(*self._get_pyrocko_station_args()) 

557 

558 def _get_pyrocko_station_args(self): 

559 return ( 

560 self.codes.network, 

561 self.codes.station, 

562 self.codes.location, 

563 self.lat, 

564 self.lon, 

565 self.elevation, 

566 self.depth, 

567 self.north_shift, 

568 self.east_shift) 

569 

570 

571class Sensor(Location): 

572 ''' 

573 Representation of a channel group. 

574 ''' 

575 

576 codes = CodesNSLCE.T() 

577 

578 tmin = Timestamp.T(optional=True) 

579 tmax = Timestamp.T(optional=True) 

580 

581 deltat = Float.T(optional=True) 

582 

583 @property 

584 def time_span(self): 

585 return (self.tmin, self.tmax) 

586 

587 def __init__(self, **kwargs): 

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

589 Location.__init__(self, **kwargs) 

590 

591 def _get_sensor_args(self): 

592 def getattr_rep(k): 

593 if k == 'codes': 

594 return self.codes.replace(self.codes[:-1]) 

595 else: 

596 return getattr(self, k) 

597 

598 return [getattr_rep(k) for k in self.T.propnames] 

599 

600 @classmethod 

601 def from_channels(cls, channels): 

602 groups = defaultdict(list) 

603 for channel in channels: 

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

605 

606 return [ 

607 cls(**dict((k, v) for (k, v) in zip(cls.T.propnames, args))) 

608 for args, _ in groups.items()] 

609 

610 def _get_pyrocko_station_args(self): 

611 return ( 

612 self.codes.network, 

613 self.codes.station, 

614 self.codes.location, 

615 self.lat, 

616 self.lon, 

617 self.elevation, 

618 self.depth, 

619 self.north_shift, 

620 self.east_shift) 

621 

622 

623@squirrel_content 

624class Channel(Sensor): 

625 ''' 

626 A channel of a seismic station. 

627 ''' 

628 

629 dip = Float.T(optional=True) 

630 azimuth = Float.T(optional=True) 

631 

632 @classmethod 

633 def from_channels(cls, channels): 

634 raise NotImplementedError() 

635 

636 def get_pyrocko_channel(self): 

637 from pyrocko import model 

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

639 

640 def _get_pyrocko_channel_args(self): 

641 return ( 

642 self.codes.channel, 

643 self.azimuth, 

644 self.dip) 

645 

646 

647observational_quantities = [ 

648 'acceleration', 'velocity', 'displacement', 'pressure', 'rotation', 

649 'temperature'] 

650 

651 

652technical_quantities = [ 

653 'voltage', 'counts'] 

654 

655 

656class QuantityType(StringChoice): 

657 ''' 

658 Choice of observational or technical quantity. 

659 

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

661 ''' 

662 choices = observational_quantities + technical_quantities 

663 

664 

665class ResponseStage(Object): 

666 ''' 

667 Representation of a response stage. 

668 

669 Components of a seismic recording system are represented as a sequence of 

670 response stages, e.g. sensor, pre-amplifier, digitizer, digital 

671 downsampling. 

672 ''' 

673 input_quantity = QuantityType.T(optional=True) 

674 input_sample_rate = Float.T(optional=True) 

675 output_quantity = QuantityType.T(optional=True) 

676 output_sample_rate = Float.T(optional=True) 

677 elements = List.T(FrequencyResponse.T()) 

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

679 

680 @property 

681 def stage_type(self): 

682 if self.input_quantity in observational_quantities \ 

683 and self.output_quantity in observational_quantities: 

684 return 'conversion' 

685 

686 if self.input_quantity in observational_quantities \ 

687 and self.output_quantity == 'voltage': 

688 return 'sensor' 

689 

690 elif self.input_quantity == 'voltage' \ 

691 and self.output_quantity == 'voltage': 

692 return 'electronics' 

693 

694 elif self.input_quantity == 'voltage' \ 

695 and self.output_quantity == 'counts': 

696 return 'digitizer' 

697 

698 elif self.input_quantity == 'counts' \ 

699 and self.output_quantity == 'counts' \ 

700 and self.input_sample_rate != self.output_sample_rate: 

701 return 'decimation' 

702 

703 elif self.input_quantity in observational_quantities \ 

704 and self.output_quantity == 'counts': 

705 return 'combination' 

706 

707 else: 

708 return 'unknown' 

709 

710 @property 

711 def summary(self): 

712 irate = self.input_sample_rate 

713 orate = self.output_sample_rate 

714 factor = None 

715 if irate and orate: 

716 factor = irate / orate 

717 return 'ResponseStage, ' + ( 

718 '%s%s => %s%s%s' % ( 

719 self.input_quantity or '?', 

720 ' @ %g Hz' % irate if irate else '', 

721 self.output_quantity or '?', 

722 ' @ %g Hz' % orate if orate else '', 

723 ' :%g' % factor if factor else '') 

724 ) 

725 

726 def get_effective(self): 

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

728 

729 

730D = 'displacement' 

731V = 'velocity' 

732A = 'acceleration' 

733 

734g_converters = { 

735 (V, D): IntegrationResponse(1), 

736 (A, D): IntegrationResponse(2), 

737 (D, V): DifferentiationResponse(1), 

738 (A, V): IntegrationResponse(1), 

739 (D, A): DifferentiationResponse(2), 

740 (V, A): DifferentiationResponse(1)} 

741 

742 

743def response_converters(input_quantity, output_quantity): 

744 if input_quantity is None or input_quantity == output_quantity: 

745 return [] 

746 

747 if output_quantity is None: 

748 raise ConversionError('Unspecified target quantity.') 

749 

750 try: 

751 return [g_converters[input_quantity, output_quantity]] 

752 

753 except KeyError: 

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

755 input_quantity, output_quantity)) 

756 

757 

758@squirrel_content 

759class Response(Object): 

760 ''' 

761 The instrument response of a seismic station channel. 

762 ''' 

763 

764 codes = CodesNSLCE.T() 

765 tmin = Timestamp.T(optional=True) 

766 tmax = Timestamp.T(optional=True) 

767 

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

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

770 

771 deltat = Float.T(optional=True) 

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

773 

774 def __init__(self, **kwargs): 

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

776 Object.__init__(self, **kwargs) 

777 

778 @property 

779 def time_span(self): 

780 return (self.tmin, self.tmax) 

781 

782 @property 

783 def nstages(self): 

784 return len(self.stages) 

785 

786 @property 

787 def input_quantity(self): 

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

789 

790 @property 

791 def output_quantity(self): 

792 return self.stages[-1].input_quantity if self.stages else None 

793 

794 @property 

795 def output_sample_rate(self): 

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

797 

798 @property 

799 def stages_summary(self): 

800 def grouped(xs): 

801 xs = list(xs) 

802 g = [] 

803 for i in range(len(xs)): 

804 g.append(xs[i]) 

805 if i+1 < len(xs) and xs[i+1] != xs[i]: 

806 yield g 

807 g = [] 

808 

809 if g: 

810 yield g 

811 

812 return '+'.join( 

813 '%s%s' % (g[0], '(%i)' % len(g) if len(g) > 1 else '') 

814 for g in grouped(stage.stage_type for stage in self.stages)) 

815 

816 @property 

817 def summary(self): 

818 orate = self.output_sample_rate 

819 return '%s %-16s %s' % ( 

820 self.__class__.__name__, self.str_codes, self.str_time_span) \ 

821 + ', ' + ', '.join(( 

822 '%s => %s' % ( 

823 self.input_quantity or '?', self.output_quantity or '?') 

824 + (' @ %g Hz' % orate if orate else ''), 

825 self.stages_summary, 

826 )) 

827 

828 def get_effective(self, input_quantity=None): 

829 elements = response_converters(input_quantity, self.input_quantity) 

830 

831 elements.extend( 

832 stage.get_effective() for stage in self.stages) 

833 

834 return MultiplyResponse(responses=simplify_responses(elements)) 

835 

836 

837@squirrel_content 

838class Event(Object): 

839 ''' 

840 A seismic event. 

841 ''' 

842 

843 name = String.T(optional=True) 

844 time = Timestamp.T() 

845 duration = Float.T(optional=True) 

846 

847 lat = Float.T() 

848 lon = Float.T() 

849 depth = Float.T(optional=True) 

850 

851 magnitude = Float.T(optional=True) 

852 

853 def get_pyrocko_event(self): 

854 from pyrocko import model 

855 model.Event( 

856 name=self.name, 

857 time=self.time, 

858 lat=self.lat, 

859 lon=self.lon, 

860 depth=self.depth, 

861 magnitude=self.magnitude, 

862 duration=self.duration) 

863 

864 @property 

865 def time_span(self): 

866 return (self.time, self.time) 

867 

868 

869def ehash(s): 

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

871 

872 

873def random_name(n=8): 

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

875 

876 

877@squirrel_content 

878class WaveformPromise(Object): 

879 ''' 

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

881 

882 In the Squirrel framework, waveform promises are used to permit download of 

883 selected waveforms from a remote site. They are typically generated by 

884 calls to 

885 :py:meth:`~pyrocko.squirrel.base.Squirrel.update_waveform_promises`. 

886 Waveform promises are inserted and indexed in the database similar to 

887 normal waveforms. When processing a waveform query, e.g. from 

888 :py:meth:`~pyrocko.squirrel.base.Squirrel.get_waveform`, and no local 

889 waveform is available for the queried time span, a matching promise can be 

890 resolved, i.e. an attempt is made to download the waveform from the remote 

891 site. The promise is removed after the download attempt (except when a 

892 network error occurs). This prevents Squirrel from making unnecessary 

893 queries for waveforms missing at the remote site. 

894 ''' 

895 

896 codes = CodesNSLCE.T() 

897 tmin = Timestamp.T() 

898 tmax = Timestamp.T() 

899 

900 deltat = Float.T(optional=True) 

901 

902 source_hash = String.T() 

903 

904 def __init__(self, **kwargs): 

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

906 Object.__init__(self, **kwargs) 

907 

908 @property 

909 def time_span(self): 

910 return (self.tmin, self.tmax) 

911 

912 

913class InvalidWaveform(Exception): 

914 pass 

915 

916 

917class WaveformOrder(Object): 

918 ''' 

919 Waveform request information. 

920 ''' 

921 

922 source_id = String.T() 

923 codes = CodesNSLCE.T() 

924 deltat = Float.T() 

925 tmin = Timestamp.T() 

926 tmax = Timestamp.T() 

927 gaps = List.T(Tuple.T(2, Timestamp.T())) 

928 

929 @property 

930 def client(self): 

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

932 

933 def describe(self, site='?'): 

934 return '%s:%s %s [%s - %s]' % ( 

935 self.client, site, str(self.codes), 

936 util.time_to_str(self.tmin), util.time_to_str(self.tmax)) 

937 

938 def validate(self, tr): 

939 if tr.ydata.size == 0: 

940 raise InvalidWaveform( 

941 'waveform with zero data samples') 

942 

943 if tr.deltat != self.deltat: 

944 raise InvalidWaveform( 

945 'incorrect sampling interval - waveform: %g s, ' 

946 'meta-data: %g s' % ( 

947 tr.deltat, self.deltat)) 

948 

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

950 raise InvalidWaveform('waveform has NaN values') 

951 

952 

953def order_summary(orders): 

954 codes_list = sorted(set(order.codes for order in orders)) 

955 if len(codes_list) > 3: 

956 return '%i order%s: %s - %s' % ( 

957 len(orders), 

958 util.plural_s(orders), 

959 str(codes_list[0]), 

960 str(codes_list[1])) 

961 

962 else: 

963 return '%i order%s: %s' % ( 

964 len(orders), 

965 util.plural_s(orders), 

966 ', '.join(str(codes) for codes in codes_list)) 

967 

968 

969class Nut(Object): 

970 ''' 

971 Index entry referencing an elementary piece of content. 

972 

973 So-called *nuts* are used in Pyrocko's Squirrel framework to hold common 

974 meta-information about individual pieces of waveforms, stations, channels, 

975 etc. together with the information where it was found or generated. 

976 ''' 

977 

978 file_path = String.T(optional=True) 

979 file_format = String.T(optional=True) 

980 file_mtime = Timestamp.T(optional=True) 

981 file_size = Int.T(optional=True) 

982 

983 file_segment = Int.T(optional=True) 

984 file_element = Int.T(optional=True) 

985 

986 kind_id = Int.T() 

987 codes = Codes.T() 

988 

989 tmin_seconds = Int.T(default=0) 

990 tmin_offset = Int.T(default=0, optional=True) 

991 tmax_seconds = Int.T(default=0) 

992 tmax_offset = Int.T(default=0, optional=True) 

993 

994 deltat = Float.T(default=0.0) 

995 

996 content = Any.T(optional=True) 

997 

998 content_in_db = False 

999 

1000 def __init__( 

1001 self, 

1002 file_path=None, 

1003 file_format=None, 

1004 file_mtime=None, 

1005 file_size=None, 

1006 file_segment=None, 

1007 file_element=None, 

1008 kind_id=0, 

1009 codes=CodesX(''), 

1010 tmin_seconds=None, 

1011 tmin_offset=0, 

1012 tmax_seconds=None, 

1013 tmax_offset=0, 

1014 deltat=None, 

1015 content=None, 

1016 tmin=None, 

1017 tmax=None, 

1018 values_nocheck=None): 

1019 

1020 if values_nocheck is not None: 

1021 (self.file_path, self.file_format, self.file_mtime, 

1022 self.file_size, 

1023 self.file_segment, self.file_element, 

1024 self.kind_id, codes_safe_str, 

1025 self.tmin_seconds, self.tmin_offset, 

1026 self.tmax_seconds, self.tmax_offset, 

1027 self.deltat) = values_nocheck 

1028 

1029 self.codes = to_codes_simple(self.kind_id, codes_safe_str) 

1030 self.content = None 

1031 else: 

1032 if tmin is not None: 

1033 tmin_seconds, tmin_offset = tsplit(tmin) 

1034 

1035 if tmax is not None: 

1036 tmax_seconds, tmax_offset = tsplit(tmax) 

1037 

1038 self.kind_id = int(kind_id) 

1039 self.codes = codes 

1040 self.tmin_seconds = int_or_g_tmin(tmin_seconds) 

1041 self.tmin_offset = int(tmin_offset) 

1042 self.tmax_seconds = int_or_g_tmax(tmax_seconds) 

1043 self.tmax_offset = int(tmax_offset) 

1044 self.deltat = float_or_none(deltat) 

1045 self.file_path = str_or_none(file_path) 

1046 self.file_segment = int_or_none(file_segment) 

1047 self.file_element = int_or_none(file_element) 

1048 self.file_format = str_or_none(file_format) 

1049 self.file_mtime = float_or_none(file_mtime) 

1050 self.file_size = int_or_none(file_size) 

1051 self.content = content 

1052 

1053 Object.__init__(self, init_props=False) 

1054 

1055 def __eq__(self, other): 

1056 return (isinstance(other, Nut) and 

1057 self.equality_values == other.equality_values) 

1058 

1059 def hash(self): 

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

1061 

1062 def __ne__(self, other): 

1063 return not (self == other) 

1064 

1065 def get_io_backend(self): 

1066 from . import io 

1067 return io.get_backend(self.file_format) 

1068 

1069 def file_modified(self): 

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

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

1072 

1073 @property 

1074 def dkey(self): 

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

1076 

1077 @property 

1078 def key(self): 

1079 return ( 

1080 self.file_path, 

1081 self.file_segment, 

1082 self.file_element, 

1083 self.file_mtime) 

1084 

1085 @property 

1086 def equality_values(self): 

1087 return ( 

1088 self.file_segment, self.file_element, 

1089 self.kind_id, self.codes, 

1090 self.tmin_seconds, self.tmin_offset, 

1091 self.tmax_seconds, self.tmax_offset, self.deltat) 

1092 

1093 @property 

1094 def tmin(self): 

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

1096 

1097 @tmin.setter 

1098 def tmin(self, t): 

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

1100 

1101 @property 

1102 def tmax(self): 

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

1104 

1105 @tmax.setter 

1106 def tmax(self, t): 

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

1108 

1109 @property 

1110 def kscale(self): 

1111 if self.tmin_seconds is None or self.tmax_seconds is None: 

1112 return 0 

1113 return tscale_to_kscale(self.tmax_seconds - self.tmin_seconds) 

1114 

1115 @property 

1116 def waveform_kwargs(self): 

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

1118 

1119 return dict( 

1120 network=network, 

1121 station=station, 

1122 location=location, 

1123 channel=channel, 

1124 extra=extra, 

1125 tmin=self.tmin, 

1126 tmax=self.tmax, 

1127 deltat=self.deltat) 

1128 

1129 @property 

1130 def waveform_promise_kwargs(self): 

1131 return dict( 

1132 codes=self.codes, 

1133 tmin=self.tmin, 

1134 tmax=self.tmax, 

1135 deltat=self.deltat) 

1136 

1137 @property 

1138 def station_kwargs(self): 

1139 network, station, location = self.codes 

1140 return dict( 

1141 codes=self.codes, 

1142 tmin=tmin_or_none(self.tmin), 

1143 tmax=tmax_or_none(self.tmax)) 

1144 

1145 @property 

1146 def channel_kwargs(self): 

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

1148 return dict( 

1149 codes=self.codes, 

1150 tmin=tmin_or_none(self.tmin), 

1151 tmax=tmax_or_none(self.tmax), 

1152 deltat=self.deltat) 

1153 

1154 @property 

1155 def response_kwargs(self): 

1156 return dict( 

1157 codes=self.codes, 

1158 tmin=tmin_or_none(self.tmin), 

1159 tmax=tmax_or_none(self.tmax), 

1160 deltat=self.deltat) 

1161 

1162 @property 

1163 def event_kwargs(self): 

1164 return dict( 

1165 name=self.codes, 

1166 time=self.tmin, 

1167 duration=(self.tmax - self.tmin) or None) 

1168 

1169 @property 

1170 def trace_kwargs(self): 

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

1172 

1173 return dict( 

1174 network=network, 

1175 station=station, 

1176 location=location, 

1177 channel=channel, 

1178 extra=extra, 

1179 tmin=self.tmin, 

1180 tmax=self.tmax-self.deltat, 

1181 deltat=self.deltat) 

1182 

1183 @property 

1184 def dummy_trace(self): 

1185 return DummyTrace(self) 

1186 

1187 @property 

1188 def summary(self): 

1189 if self.tmin == self.tmax: 

1190 ts = util.time_to_str(self.tmin) 

1191 else: 

1192 ts = '%s - %s' % ( 

1193 util.time_to_str(self.tmin), 

1194 util.time_to_str(self.tmax)) 

1195 

1196 return ' '.join(( 

1197 ('%s,' % to_kind(self.kind_id)).ljust(9), 

1198 ('%s,' % str(self.codes)).ljust(18), 

1199 ts)) 

1200 

1201 

1202def make_waveform_nut(**kwargs): 

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

1204 

1205 

1206def make_waveform_promise_nut(**kwargs): 

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

1208 

1209 

1210def make_station_nut(**kwargs): 

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

1212 

1213 

1214def make_channel_nut(**kwargs): 

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

1216 

1217 

1218def make_response_nut(**kwargs): 

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

1220 

1221 

1222def make_event_nut(**kwargs): 

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

1224 

1225 

1226def group_channels(nuts): 

1227 by_ansl = {} 

1228 for nut in nuts: 

1229 if nut.kind_id != CHANNEL: 

1230 continue 

1231 

1232 ansl = nut.codes[:4] 

1233 

1234 if ansl not in by_ansl: 

1235 by_ansl[ansl] = {} 

1236 

1237 group = by_ansl[ansl] 

1238 

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

1240 

1241 if k not in group: 

1242 group[k] = set() 

1243 

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

1245 

1246 return by_ansl 

1247 

1248 

1249class DummyTrace(object): 

1250 

1251 def __init__(self, nut): 

1252 self.nut = nut 

1253 self.codes = nut.codes 

1254 self.meta = {} 

1255 

1256 @property 

1257 def tmin(self): 

1258 return self.nut.tmin 

1259 

1260 @property 

1261 def tmax(self): 

1262 return self.nut.tmax 

1263 

1264 @property 

1265 def deltat(self): 

1266 return self.nut.deltat 

1267 

1268 @property 

1269 def nslc_id(self): 

1270 return self.codes.nslc 

1271 

1272 @property 

1273 def network(self): 

1274 return self.codes.network 

1275 

1276 @property 

1277 def station(self): 

1278 return self.codes.station 

1279 

1280 @property 

1281 def location(self): 

1282 return self.codes.location 

1283 

1284 @property 

1285 def channel(self): 

1286 return self.codes.channel 

1287 

1288 @property 

1289 def extra(self): 

1290 return self.codes.extra 

1291 

1292 def overlaps(self, tmin, tmax): 

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

1294 

1295 

1296def duration_to_str(t): 

1297 if t > 24*3600: 

1298 return '%gd' % (t / (24.*3600.)) 

1299 elif t > 3600: 

1300 return '%gh' % (t / 3600.) 

1301 elif t > 60: 

1302 return '%gm' % (t / 60.) 

1303 else: 

1304 return '%gs' % t 

1305 

1306 

1307class Coverage(Object): 

1308 ''' 

1309 Information about times covered by a waveform or other time series data. 

1310 ''' 

1311 kind_id = Int.T( 

1312 help='Content type.') 

1313 pattern = Codes.T( 

1314 help='The codes pattern in the request, which caused this entry to ' 

1315 'match.') 

1316 codes = Codes.T( 

1317 help='NSLCE or NSL codes identifier of the time series.') 

1318 deltat = Float.T( 

1319 help='Sampling interval [s]', 

1320 optional=True) 

1321 tmin = Timestamp.T( 

1322 help='Global start time of time series.', 

1323 optional=True) 

1324 tmax = Timestamp.T( 

1325 help='Global end time of time series.', 

1326 optional=True) 

1327 changes = List.T( 

1328 Tuple.T(2, Any.T()), 

1329 help='List of change points, with entries of the form ' 

1330 '``(time, count)``, where a ``count`` of zero indicates start of ' 

1331 'a gap, a value of 1 start of normal data coverage and a higher ' 

1332 'value duplicate or redundant data coverage.') 

1333 

1334 @classmethod 

1335 def from_values(cls, args): 

1336 pattern, codes, deltat, tmin, tmax, changes, kind_id = args 

1337 return cls( 

1338 kind_id=kind_id, 

1339 pattern=pattern, 

1340 codes=codes, 

1341 deltat=deltat, 

1342 tmin=tmin, 

1343 tmax=tmax, 

1344 changes=changes) 

1345 

1346 @property 

1347 def summary(self): 

1348 ts = '%s - %s,' % ( 

1349 util.time_to_str(self.tmin), 

1350 util.time_to_str(self.tmax)) 

1351 

1352 srate = self.sample_rate 

1353 

1354 return ' '.join(( 

1355 ('%s,' % to_kind(self.kind_id)).ljust(9), 

1356 ('%s,' % str(self.codes)).ljust(18), 

1357 ts, 

1358 '%10.3g,' % srate if srate else '', 

1359 '%4i' % len(self.changes), 

1360 '%s' % duration_to_str(self.total))) 

1361 

1362 @property 

1363 def sample_rate(self): 

1364 if self.deltat is None: 

1365 return None 

1366 elif self.deltat == 0.0: 

1367 return 0.0 

1368 else: 

1369 return 1.0 / self.deltat 

1370 

1371 @property 

1372 def labels(self): 

1373 srate = self.sample_rate 

1374 return ( 

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

1376 '%.3g' % srate if srate else '') 

1377 

1378 @property 

1379 def total(self): 

1380 total_t = None 

1381 for tmin, tmax, _ in self.iter_spans(): 

1382 total_t = (total_t or 0.0) + (tmax - tmin) 

1383 

1384 return total_t 

1385 

1386 def iter_spans(self): 

1387 last = None 

1388 for (t, count) in self.changes: 

1389 if last is not None: 

1390 last_t, last_count = last 

1391 if last_count > 0: 

1392 yield last_t, t, last_count 

1393 

1394 last = (t, count) 

1395 

1396 

1397__all__ = [ 

1398 'to_kind', 

1399 'to_kinds', 

1400 'to_kind_id', 

1401 'to_kind_ids', 

1402 'CodesError', 

1403 'Codes', 

1404 'CodesNSLCE', 

1405 'CodesNSL', 

1406 'CodesX', 

1407 'Station', 

1408 'Channel', 

1409 'Sensor', 

1410 'Response', 

1411 'Nut', 

1412 'Coverage', 

1413 'WaveformPromise', 

1414]