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 hashlib 

22import numpy as num 

23from os import urandom 

24from base64 import urlsafe_b64encode 

25from collections import defaultdict 

26 

27from pyrocko import util 

28from pyrocko.guts import Object, String, Timestamp, Float, Int, Unicode, \ 

29 Tuple, List, StringChoice, Any 

30from pyrocko.model import Content 

31from pyrocko.response import FrequencyResponse, MultiplyResponse, \ 

32 IntegrationResponse, DifferentiationResponse, simplify_responses, \ 

33 FrequencyResponseCheckpoint 

34 

35from .error import ConversionError 

36 

37 

38guts_prefix = 'squirrel' 

39 

40separator = '\t' 

41 

42g_content_kinds = [ 

43 'undefined', 

44 'waveform', 

45 'station', 

46 'channel', 

47 'response', 

48 'event', 

49 'waveform_promise'] 

50 

51 

52g_content_kind_ids = ( 

53 UNDEFINED, WAVEFORM, STATION, CHANNEL, RESPONSE, EVENT, 

54 WAVEFORM_PROMISE) = range(len(g_content_kinds)) 

55 

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

57 

58 

59try: 

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

61except Exception: 

62 g_tmin_queries = g_tmin 

63 

64 

65def to_kind(kind_id): 

66 return g_content_kinds[kind_id] 

67 

68 

69def to_kinds(kind_ids): 

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

71 

72 

73def to_kind_id(kind): 

74 return g_content_kinds.index(kind) 

75 

76 

77def to_kind_ids(kinds): 

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

79 

80 

81g_kind_mask_all = 0xff 

82 

83 

84def to_kind_mask(kinds): 

85 if kinds: 

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

87 else: 

88 return g_kind_mask_all 

89 

90 

91def str_or_none(x): 

92 if x is None: 

93 return None 

94 else: 

95 return str(x) 

96 

97 

98def float_or_none(x): 

99 if x is None: 

100 return None 

101 else: 

102 return float(x) 

103 

104 

105def int_or_none(x): 

106 if x is None: 

107 return None 

108 else: 

109 return int(x) 

110 

111 

112def int_or_g_tmin(x): 

113 if x is None: 

114 return g_tmin 

115 else: 

116 return int(x) 

117 

118 

119def int_or_g_tmax(x): 

120 if x is None: 

121 return g_tmax 

122 else: 

123 return int(x) 

124 

125 

126def tmin_or_none(x): 

127 if x == g_tmin: 

128 return None 

129 else: 

130 return x 

131 

132 

133def tmax_or_none(x): 

134 if x == g_tmax: 

135 return None 

136 else: 

137 return x 

138 

139 

140def time_or_none_to_str(x): 

141 if x is None: 

142 return '...'.ljust(17) 

143 else: 

144 return util.time_to_str(x) 

145 

146 

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

148 codes = ['.'.join(x) for x in codes] 

149 

150 if len(codes) > 20: 

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

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

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

154 else: 

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

156 if codes else '<none>' 

157 

158 return scodes 

159 

160 

161g_offset_time_unit_inv = 1000000000 

162g_offset_time_unit = 1.0 / g_offset_time_unit_inv 

163 

164 

165def tsplit(t): 

166 if t is None: 

167 return None, 0.0 

168 

169 t = util.to_time_float(t) 

170 if type(t) is float: 

171 t = round(t, 5) 

172 else: 

173 t = round(t, 9) 

174 

175 seconds = num.floor(t) 

176 offset = t - seconds 

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

178 

179 

180def tjoin(seconds, offset): 

181 if seconds is None: 

182 return None 

183 

184 return util.to_time_float(seconds) \ 

185 + util.to_time_float(offset*g_offset_time_unit) 

186 

187 

188tscale_min = 1 

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

190tscale_logbase = 20 

191 

192tscale_edges = [tscale_min] 

193while True: 

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

195 if tscale_edges[-1] >= tscale_max: 

196 break 

197 

198 

199tscale_edges = num.array(tscale_edges) 

200 

201 

202def tscale_to_kscale(tscale): 

203 

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

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

206 # ... 

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

208 

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

210 

211 

212class Station(Content): 

213 ''' 

214 A seismic station. 

215 ''' 

216 

217 agency = String.T(default='', help='Agency code (2-5)') 

218 network = String.T(default='', help='Deployment/network code (1-8)') 

219 station = String.T(default='', help='Station code (1-5)') 

220 location = String.T(default='', optional=True, help='Location code (0-2)') 

221 

222 tmin = Timestamp.T(optional=True) 

223 tmax = Timestamp.T(optional=True) 

224 

225 lat = Float.T() 

226 lon = Float.T() 

227 elevation = Float.T(optional=True) 

228 depth = Float.T(optional=True) 

229 

230 description = Unicode.T(optional=True) 

231 

232 @property 

233 def codes(self): 

234 return ( 

235 self.agency, self.network, self.station, 

236 self.location if self.location is not None else '*') 

237 

238 @property 

239 def time_span(self): 

240 return (self.tmin, self.tmax) 

241 

242 def get_pyrocko_station(self): 

243 from pyrocko import model 

244 return model.Station( 

245 network=self.network, 

246 station=self.station, 

247 location=self.location if self.location is not None else '*', 

248 lat=self.lat, 

249 lon=self.lon, 

250 elevation=self.elevation, 

251 depth=self.depth) 

252 

253 def _get_pyrocko_station_args(self): 

254 return ( 

255 '*', 

256 self.network, 

257 self.station, 

258 self.location if self.location is not None else '*', 

259 self.lat, 

260 self.lon, 

261 self.elevation, 

262 self.depth) 

263 

264 

265class Channel(Content): 

266 ''' 

267 A channel of a seismic station. 

268 ''' 

269 

270 agency = String.T(default='', help='Agency code (2-5)') 

271 network = String.T(default='', help='Deployment/network code (1-8)') 

272 station = String.T(default='', help='Station code (1-5)') 

273 location = String.T(default='', help='Location code (0-2)') 

274 channel = String.T(default='', help='Channel code (3)') 

275 extra = String.T(default='', help='Extra/custom code') 

276 

277 tmin = Timestamp.T(optional=True) 

278 tmax = Timestamp.T(optional=True) 

279 

280 lat = Float.T() 

281 lon = Float.T() 

282 elevation = Float.T(optional=True) 

283 depth = Float.T(optional=True) 

284 

285 dip = Float.T(optional=True) 

286 azimuth = Float.T(optional=True) 

287 deltat = Float.T(optional=True) 

288 

289 @property 

290 def codes(self): 

291 return ( 

292 self.agency, self.network, self.station, self.location, 

293 self.channel, self.extra) 

294 

295 def set_codes( 

296 self, agency=None, network=None, station=None, location=None, 

297 channel=None, extra=None): 

298 

299 if agency is not None: 

300 self.agency = agency 

301 if network is not None: 

302 self.network = network 

303 if station is not None: 

304 self.station = station 

305 if location is not None: 

306 self.location = location 

307 if channel is not None: 

308 self.channel = channel 

309 if extra is not None: 

310 self.extra = extra 

311 

312 @property 

313 def time_span(self): 

314 return (self.tmin, self.tmax) 

315 

316 def get_pyrocko_channel(self): 

317 from pyrocko import model 

318 return model.Channel( 

319 name=self.channel, 

320 azimuth=self.azimuth, 

321 dip=self.dip) 

322 

323 def _get_pyrocko_station_args(self): 

324 return ( 

325 self.channel, 

326 self.network, 

327 self.station, 

328 self.location, 

329 self.lat, 

330 self.lon, 

331 self.elevation, 

332 self.depth) 

333 

334 def _get_pyrocko_channel_args(self): 

335 return ( 

336 '*', 

337 self.channel, 

338 self.azimuth, 

339 self.dip) 

340 

341 def _get_sensor_args(self): 

342 return ( 

343 self.agency, 

344 self.network, 

345 self.station, 

346 self.location, 

347 self.channel[:-1] + '?', 

348 self.extra, 

349 self.lat, 

350 self.lon, 

351 self.elevation, 

352 self.depth, 

353 self.deltat, 

354 self.tmin, 

355 self.tmax) 

356 

357 

358class Sensor(Channel): 

359 ''' 

360 Representation of a channel group. 

361 ''' 

362 

363 def grouping(self, channel): 

364 return channel._get_sensor_args() 

365 

366 @classmethod 

367 def from_channels(cls, channels): 

368 groups = defaultdict(list) 

369 for channel in channels: 

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

371 

372 return [cls( 

373 agency=args[0], 

374 network=args[1], 

375 station=args[2], 

376 location=args[3], 

377 channel=args[4], 

378 extra=args[5], 

379 lat=args[6], 

380 lon=args[7], 

381 elevation=args[8], 

382 depth=args[9], 

383 deltat=args[10], 

384 tmin=args[11], 

385 tmax=args[12]) 

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

387 

388 

389observational_quantities = [ 

390 'acceleration', 'velocity', 'displacement', 'pressure', 'rotation', 

391 'temperature'] 

392 

393 

394technical_quantities = [ 

395 'voltage', 'counts'] 

396 

397 

398class QuantityType(StringChoice): 

399 ''' 

400 Choice of observational or technical quantity. 

401 

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

403 ''' 

404 choices = observational_quantities + technical_quantities 

405 

406 

407class ResponseStage(Object): 

408 ''' 

409 Representation of a response stage. 

410 

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

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

413 downsampling. 

414 ''' 

415 input_quantity = QuantityType.T(optional=True) 

416 input_sample_rate = Float.T(optional=True) 

417 output_quantity = QuantityType.T(optional=True) 

418 output_sample_rate = Float.T(optional=True) 

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

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

421 

422 @property 

423 def stage_type(self): 

424 if self.input_quantity in observational_quantities \ 

425 and self.output_quantity in observational_quantities: 

426 return 'conversion' 

427 

428 if self.input_quantity in observational_quantities \ 

429 and self.output_quantity == 'voltage': 

430 return 'sensor' 

431 

432 elif self.input_quantity == 'voltage' \ 

433 and self.output_quantity == 'voltage': 

434 return 'electronics' 

435 

436 elif self.input_quantity == 'voltage' \ 

437 and self.output_quantity == 'counts': 

438 return 'digitizer' 

439 

440 elif self.input_quantity == 'counts' \ 

441 and self.output_quantity == 'counts' \ 

442 and self.input_sample_rate != self.output_sample_rate: 

443 return 'decimation' 

444 

445 elif self.input_quantity in observational_quantities \ 

446 and self.output_quantity == 'counts': 

447 return 'combination' 

448 

449 else: 

450 return 'unknown' 

451 

452 @property 

453 def summary(self): 

454 irate = self.input_sample_rate 

455 orate = self.output_sample_rate 

456 factor = None 

457 if irate and orate: 

458 factor = irate / orate 

459 return 'ResponseStage, ' + ( 

460 '%s%s => %s%s%s' % ( 

461 self.input_quantity or '?', 

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

463 self.output_quantity or '?', 

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

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

466 ) 

467 

468 def get_effective(self): 

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

470 

471 

472D = 'displacement' 

473V = 'velocity' 

474A = 'acceleration' 

475 

476g_converters = { 

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

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

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

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

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

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

483 

484 

485def response_converters(input_quantity, output_quantity): 

486 if input_quantity is None or input_quantity == output_quantity: 

487 return [] 

488 

489 if output_quantity is None: 

490 raise ConversionError('Unspecified target quantity.') 

491 

492 try: 

493 return [g_converters[input_quantity, output_quantity]] 

494 

495 except KeyError: 

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

497 input_quantity, output_quantity)) 

498 

499 

500class Response(Content): 

501 ''' 

502 The instrument response of a seismic station channel. 

503 ''' 

504 

505 agency = String.T(default='', help='Agency code (2-5)') 

506 network = String.T(default='', help='Deployment/network code (1-8)') 

507 station = String.T(default='', help='Station code (1-5)') 

508 location = String.T(default='', help='Location code (0-2)') 

509 channel = String.T(default='', help='Channel code (3)') 

510 extra = String.T(default='', help='Extra/custom code') 

511 

512 tmin = Timestamp.T(optional=True) 

513 tmax = Timestamp.T(optional=True) 

514 

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

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

517 

518 deltat = Float.T(optional=True) 

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

520 

521 @property 

522 def codes(self): 

523 return ( 

524 self.agency, self.network, self.station, self.location, 

525 self.channel, self.extra) 

526 

527 @property 

528 def time_span(self): 

529 return (self.tmin, self.tmax) 

530 

531 @property 

532 def nstages(self): 

533 return len(self.stages) 

534 

535 @property 

536 def input_quantity(self): 

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

538 

539 @property 

540 def output_quantity(self): 

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

542 

543 @property 

544 def output_sample_rate(self): 

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

546 

547 @property 

548 def stages_summary(self): 

549 def grouped(xs): 

550 xs = list(xs) 

551 g = [] 

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

553 g.append(xs[i]) 

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

555 yield g 

556 g = [] 

557 

558 if g: 

559 yield g 

560 

561 return '+'.join( 

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

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

564 

565 @property 

566 def summary(self): 

567 orate = self.output_sample_rate 

568 return Content.summary.fget(self) + ', ' + ', '.join(( 

569 '%s => %s' % ( 

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

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

572 self.stages_summary, 

573 )) 

574 

575 def get_effective(self, input_quantity=None): 

576 elements = response_converters(input_quantity, self.input_quantity) 

577 

578 elements.extend( 

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

580 

581 return MultiplyResponse(responses=simplify_responses(elements)) 

582 

583 

584class Event(Content): 

585 ''' 

586 A seismic event. 

587 ''' 

588 

589 name = String.T(optional=True) 

590 time = Timestamp.T() 

591 duration = Float.T(optional=True) 

592 

593 lat = Float.T() 

594 lon = Float.T() 

595 depth = Float.T(optional=True) 

596 

597 magnitude = Float.T(optional=True) 

598 

599 def get_pyrocko_event(self): 

600 from pyrocko import model 

601 model.Event( 

602 name=self.name, 

603 time=self.time, 

604 lat=self.lat, 

605 lon=self.lon, 

606 depth=self.depth, 

607 magnitude=self.magnitude, 

608 duration=self.duration) 

609 

610 @property 

611 def time_span(self): 

612 return (self.time, self.time) 

613 

614 

615def ehash(s): 

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

617 

618 

619def random_name(n=8): 

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

621 

622 

623class WaveformPromise(Content): 

624 ''' 

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

626 

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

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

629 calls to 

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

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

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

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

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

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

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

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

638 queries for waveforms missing at the remote site. 

639 ''' 

640 

641 agency = String.T(default='', help='Agency code (2-5)') 

642 network = String.T(default='', help='Deployment/network code (1-8)') 

643 station = String.T(default='', help='Station code (1-5)') 

644 location = String.T(default='', help='Location code (0-2)') 

645 channel = String.T(default='', help='Channel code (3)') 

646 extra = String.T(default='', help='Extra/custom code') 

647 

648 tmin = Timestamp.T() 

649 tmax = Timestamp.T() 

650 

651 deltat = Float.T(optional=True) 

652 

653 source_hash = String.T() 

654 

655 @property 

656 def codes(self): 

657 return ( 

658 self.agency, self.network, self.station, self.location, 

659 self.channel, self.extra) 

660 

661 @property 

662 def time_span(self): 

663 return (self.tmin, self.tmax) 

664 

665 

666class InvalidWaveform(Exception): 

667 pass 

668 

669 

670class WaveformOrder(Object): 

671 ''' 

672 Waveform request information. 

673 ''' 

674 

675 source_id = String.T() 

676 codes = Tuple.T(None, String.T()) 

677 deltat = Float.T() 

678 tmin = Timestamp.T() 

679 tmax = Timestamp.T() 

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

681 

682 @property 

683 def client(self): 

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

685 

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

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

688 self.client, site, '.'.join(self.codes), 

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

690 

691 def validate(self, tr): 

692 if tr.ydata.size == 0: 

693 raise InvalidWaveform( 

694 'waveform with zero data samples') 

695 

696 if tr.deltat != self.deltat: 

697 raise InvalidWaveform( 

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

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

700 tr.deltat, self.deltat)) 

701 

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

703 raise InvalidWaveform('waveform has NaN values') 

704 

705 

706def order_summary(orders): 

707 codes = sorted(set(order.codes[1:-1] for order in orders)) 

708 if len(codes) >= 2: 

709 return '%i orders, %s - %s' % ( 

710 len(orders), 

711 '.'.join(codes[0]), 

712 '.'.join(codes[-1])) 

713 

714 else: 

715 return '%i orders, %s' % ( 

716 len(orders), 

717 '.'.join(codes[0])) 

718 

719 

720class Nut(Object): 

721 ''' 

722 Index entry referencing an elementary piece of content. 

723 

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

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

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

727 ''' 

728 

729 file_path = String.T(optional=True) 

730 file_format = String.T(optional=True) 

731 file_mtime = Timestamp.T(optional=True) 

732 file_size = Int.T(optional=True) 

733 

734 file_segment = Int.T(optional=True) 

735 file_element = Int.T(optional=True) 

736 

737 kind_id = Int.T() 

738 codes = String.T() 

739 

740 tmin_seconds = Timestamp.T() 

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

742 tmax_seconds = Timestamp.T() 

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

744 

745 deltat = Float.T(default=0.0) 

746 

747 content = Content.T(optional=True) 

748 

749 content_in_db = False 

750 

751 def __init__( 

752 self, 

753 file_path=None, 

754 file_format=None, 

755 file_mtime=None, 

756 file_size=None, 

757 file_segment=None, 

758 file_element=None, 

759 kind_id=0, 

760 codes='', 

761 tmin_seconds=None, 

762 tmin_offset=0, 

763 tmax_seconds=None, 

764 tmax_offset=0, 

765 deltat=None, 

766 content=None, 

767 tmin=None, 

768 tmax=None, 

769 values_nocheck=None): 

770 

771 if values_nocheck is not None: 

772 (self.file_path, self.file_format, self.file_mtime, 

773 self.file_size, 

774 self.file_segment, self.file_element, 

775 self.kind_id, self.codes, 

776 self.tmin_seconds, self.tmin_offset, 

777 self.tmax_seconds, self.tmax_offset, 

778 self.deltat) = values_nocheck 

779 

780 self.content = None 

781 else: 

782 if tmin is not None: 

783 tmin_seconds, tmin_offset = tsplit(tmin) 

784 

785 if tmax is not None: 

786 tmax_seconds, tmax_offset = tsplit(tmax) 

787 

788 self.kind_id = int(kind_id) 

789 self.codes = str(codes) 

790 self.tmin_seconds = int_or_g_tmin(tmin_seconds) 

791 self.tmin_offset = int(tmin_offset) 

792 self.tmax_seconds = int_or_g_tmax(tmax_seconds) 

793 self.tmax_offset = int(tmax_offset) 

794 self.deltat = float_or_none(deltat) 

795 self.file_path = str_or_none(file_path) 

796 self.file_segment = int_or_none(file_segment) 

797 self.file_element = int_or_none(file_element) 

798 self.file_format = str_or_none(file_format) 

799 self.file_mtime = float_or_none(file_mtime) 

800 self.file_size = int_or_none(file_size) 

801 self.content = content 

802 

803 Object.__init__(self, init_props=False) 

804 

805 def __eq__(self, other): 

806 return (isinstance(other, Nut) and 

807 self.equality_values == other.equality_values) 

808 

809 def hash(self): 

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

811 

812 def __ne__(self, other): 

813 return not (self == other) 

814 

815 def get_io_backend(self): 

816 from . import io 

817 return io.get_backend(self.file_format) 

818 

819 def file_modified(self): 

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

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

822 

823 @property 

824 def dkey(self): 

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

826 

827 @property 

828 def key(self): 

829 return ( 

830 self.file_path, 

831 self.file_segment, 

832 self.file_element, 

833 self.file_mtime) 

834 

835 @property 

836 def equality_values(self): 

837 return ( 

838 self.file_segment, self.file_element, 

839 self.kind_id, self.codes, 

840 self.tmin_seconds, self.tmin_offset, 

841 self.tmax_seconds, self.tmax_offset, self.deltat) 

842 

843 @property 

844 def tmin(self): 

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

846 

847 @tmin.setter 

848 def tmin(self, t): 

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

850 

851 @property 

852 def tmax(self): 

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

854 

855 @tmax.setter 

856 def tmax(self, t): 

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

858 

859 @property 

860 def kscale(self): 

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

862 return 0 

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

864 

865 @property 

866 def waveform_kwargs(self): 

867 agency, network, station, location, channel, extra = \ 

868 self.codes.split(separator) 

869 

870 return dict( 

871 agency=agency, 

872 network=network, 

873 station=station, 

874 location=location, 

875 channel=channel, 

876 extra=extra, 

877 tmin=self.tmin, 

878 tmax=self.tmax, 

879 deltat=self.deltat) 

880 

881 @property 

882 def waveform_promise_kwargs(self): 

883 agency, network, station, location, channel, extra = \ 

884 self.codes.split(separator) 

885 

886 return dict( 

887 agency=agency, 

888 network=network, 

889 station=station, 

890 location=location, 

891 channel=channel, 

892 extra=extra, 

893 tmin=self.tmin, 

894 tmax=self.tmax, 

895 deltat=self.deltat) 

896 

897 @property 

898 def station_kwargs(self): 

899 agency, network, station, location = self.codes.split(separator) 

900 return dict( 

901 agency=agency, 

902 network=network, 

903 station=station, 

904 location=location if location != '*' else None, 

905 tmin=tmin_or_none(self.tmin), 

906 tmax=tmax_or_none(self.tmax)) 

907 

908 @property 

909 def channel_kwargs(self): 

910 agency, network, station, location, channel, extra \ 

911 = self.codes.split(separator) 

912 

913 return dict( 

914 agency=agency, 

915 network=network, 

916 station=station, 

917 location=location, 

918 channel=channel, 

919 extra=extra, 

920 tmin=tmin_or_none(self.tmin), 

921 tmax=tmax_or_none(self.tmax), 

922 deltat=self.deltat) 

923 

924 @property 

925 def response_kwargs(self): 

926 agency, network, station, location, channel, extra \ 

927 = self.codes.split(separator) 

928 

929 return dict( 

930 agency=agency, 

931 network=network, 

932 station=station, 

933 location=location, 

934 channel=channel, 

935 extra=extra, 

936 tmin=tmin_or_none(self.tmin), 

937 tmax=tmax_or_none(self.tmax), 

938 deltat=self.deltat) 

939 

940 @property 

941 def event_kwargs(self): 

942 return dict( 

943 name=self.codes, 

944 time=self.tmin, 

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

946 

947 @property 

948 def trace_kwargs(self): 

949 agency, network, station, location, channel, extra = \ 

950 self.codes.split(separator) 

951 

952 return dict( 

953 network=network, 

954 station=station, 

955 location=location, 

956 channel=channel, 

957 extra=extra, 

958 tmin=self.tmin, 

959 tmax=self.tmax-self.deltat, 

960 deltat=self.deltat) 

961 

962 @property 

963 def dummy_trace(self): 

964 return DummyTrace(self) 

965 

966 @property 

967 def codes_tuple(self): 

968 return tuple(self.codes.split(separator)) 

969 

970 @property 

971 def summary(self): 

972 if self.tmin == self.tmax: 

973 ts = util.time_to_str(self.tmin) 

974 else: 

975 ts = '%s - %s' % ( 

976 util.time_to_str(self.tmin), 

977 util.time_to_str(self.tmax)) 

978 

979 return ' '.join(( 

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

981 ('%s,' % '.'.join(self.codes.split(separator))).ljust(18), 

982 ts)) 

983 

984 

985def make_waveform_nut( 

986 agency='', network='', station='', location='', channel='', extra='', 

987 **kwargs): 

988 

989 codes = separator.join( 

990 (agency, network, station, location, channel, extra)) 

991 

992 return Nut( 

993 kind_id=WAVEFORM, 

994 codes=codes, 

995 **kwargs) 

996 

997 

998def make_waveform_promise_nut( 

999 agency='', network='', station='', location='', channel='', extra='', 

1000 **kwargs): 

1001 

1002 codes = separator.join( 

1003 (agency, network, station, location, channel, extra)) 

1004 

1005 return Nut( 

1006 kind_id=WAVEFORM_PROMISE, 

1007 codes=codes, 

1008 **kwargs) 

1009 

1010 

1011def make_station_nut( 

1012 agency='', network='', station='', location='', **kwargs): 

1013 

1014 codes = separator.join((agency, network, station, location)) 

1015 

1016 return Nut( 

1017 kind_id=STATION, 

1018 codes=codes, 

1019 **kwargs) 

1020 

1021 

1022def make_channel_nut( 

1023 agency='', network='', station='', location='', channel='', extra='', 

1024 **kwargs): 

1025 

1026 codes = separator.join( 

1027 (agency, network, station, location, channel, extra)) 

1028 

1029 return Nut( 

1030 kind_id=CHANNEL, 

1031 codes=codes, 

1032 **kwargs) 

1033 

1034 

1035def make_response_nut( 

1036 agency='', network='', station='', location='', channel='', extra='', 

1037 **kwargs): 

1038 

1039 codes = separator.join( 

1040 (agency, network, station, location, channel, extra)) 

1041 

1042 return Nut( 

1043 kind_id=RESPONSE, 

1044 codes=codes, 

1045 **kwargs) 

1046 

1047 

1048def make_event_nut(name='', **kwargs): 

1049 

1050 codes = name 

1051 

1052 return Nut( 

1053 kind_id=EVENT, codes=codes, 

1054 **kwargs) 

1055 

1056 

1057def group_channels(nuts): 

1058 by_ansl = {} 

1059 for nut in nuts: 

1060 if nut.kind_id != CHANNEL: 

1061 continue 

1062 

1063 ansl = nut.codes[:4] 

1064 

1065 if ansl not in by_ansl: 

1066 by_ansl[ansl] = {} 

1067 

1068 group = by_ansl[ansl] 

1069 

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

1071 

1072 if k not in group: 

1073 group[k] = set() 

1074 

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

1076 

1077 return by_ansl 

1078 

1079 

1080class DummyTrace(object): 

1081 

1082 def __init__(self, nut): 

1083 self.nut = nut 

1084 self._codes = None 

1085 self.meta = {} 

1086 

1087 @property 

1088 def tmin(self): 

1089 return self.nut.tmin 

1090 

1091 @property 

1092 def tmax(self): 

1093 return self.nut.tmax 

1094 

1095 @property 

1096 def deltat(self): 

1097 return self.nut.deltat 

1098 

1099 @property 

1100 def codes(self): 

1101 if self._codes is None: 

1102 self._codes = self.nut.codes_tuple 

1103 

1104 return self._codes 

1105 

1106 @property 

1107 def nslc_id(self): 

1108 return self.codes[1:5] 

1109 

1110 @property 

1111 def agency(self): 

1112 return self.codes[0] 

1113 

1114 @property 

1115 def network(self): 

1116 return self.codes[1] 

1117 

1118 @property 

1119 def station(self): 

1120 return self.codes[2] 

1121 

1122 @property 

1123 def location(self): 

1124 return self.codes[3] 

1125 

1126 @property 

1127 def channel(self): 

1128 return self.codes[4] 

1129 

1130 @property 

1131 def extra(self): 

1132 return self.codes[5] 

1133 

1134 def overlaps(self, tmin, tmax): 

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

1136 

1137 

1138def duration_to_str(t): 

1139 if t > 24*3600: 

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

1141 elif t > 3600: 

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

1143 elif t > 60: 

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

1145 else: 

1146 return '%gs' % t 

1147 

1148 

1149class Coverage(Object): 

1150 ''' 

1151 Information about times covered by a waveform or other content type. 

1152 ''' 

1153 kind_id = Int.T() 

1154 pattern = String.T() 

1155 codes = String.T() 

1156 deltat = Float.T(optional=True) 

1157 tmin = Timestamp.T(optional=True) 

1158 tmax = Timestamp.T(optional=True) 

1159 changes = List.T(Tuple.T(2, Any.T())) 

1160 

1161 @classmethod 

1162 def from_values(cls, args): 

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

1164 return cls( 

1165 kind_id=kind_id, 

1166 pattern=pattern, 

1167 codes=codes, 

1168 deltat=deltat, 

1169 tmin=tmin, 

1170 tmax=tmax, 

1171 changes=changes) 

1172 

1173 @property 

1174 def summary(self): 

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

1176 util.time_to_str(self.tmin), 

1177 util.time_to_str(self.tmax)) 

1178 

1179 srate = self.sample_rate 

1180 

1181 return ' '.join(( 

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

1183 ('%s,' % '.'.join(self.codes.split(separator))).ljust(18), 

1184 ts, 

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

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

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

1188 

1189 @property 

1190 def sample_rate(self): 

1191 if self.deltat is None: 

1192 return None 

1193 elif self.deltat == 0.0: 

1194 return 0.0 

1195 else: 

1196 return 1.0 / self.deltat 

1197 

1198 @property 

1199 def labels(self): 

1200 srate = self.sample_rate 

1201 return ( 

1202 ('%s' % '.'.join(self.codes.split(separator))), 

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

1204 

1205 @property 

1206 def total(self): 

1207 total_t = None 

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

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

1210 

1211 return total_t 

1212 

1213 def iter_spans(self): 

1214 last = None 

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

1216 if last is not None: 

1217 last_t, last_count = last 

1218 if last_count > 0: 

1219 yield last_t, t, last_count 

1220 

1221 last = (t, count) 

1222 

1223 

1224__all__ = [ 

1225 'separator', 

1226 'to_kind', 

1227 'to_kinds', 

1228 'to_kind_id', 

1229 'to_kind_ids', 

1230 'Station', 

1231 'Channel', 

1232 'Sensor', 

1233 'Response', 

1234 'Nut', 

1235 'Coverage', 

1236 'WaveformPromise', 

1237]