Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/io/stationxml.py: 70%

1187 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-11-09 13:44 +0000

1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6''' 

7`FDSN StationXML <https://www.fdsn.org/xml/station/>`_ input, output and data 

8model. 

9''' 

10 

11import sys 

12import time 

13import logging 

14import datetime 

15import calendar 

16import math 

17import copy 

18from collections import defaultdict 

19 

20import numpy as num 

21 

22from pyrocko.guts import (StringChoice, StringPattern, UnicodePattern, String, 

23 Unicode, Int, Float, List, Object, Timestamp, 

24 ValidationError, TBase, re_tz, Any, Tuple) 

25from pyrocko.guts import load_xml # noqa 

26from pyrocko.util import hpfloat, time_to_str, get_time_float 

27 

28import pyrocko.model 

29from pyrocko import util, response 

30 

31guts_prefix = 'sx' 

32 

33guts_xmlns = 'http://www.fdsn.org/xml/station/1' 

34 

35logger = logging.getLogger('pyrocko.io.stationxml') 

36 

37conversion = { 

38 ('M', 'M'): None, 

39 ('M/S', 'M'): response.IntegrationResponse(1), 

40 ('M/S**2', 'M'): response.IntegrationResponse(2), 

41 ('M', 'M/S'): response.DifferentiationResponse(1), 

42 ('M/S', 'M/S'): None, 

43 ('M/S**2', 'M/S'): response.IntegrationResponse(1), 

44 ('M', 'M/S**2'): response.DifferentiationResponse(2), 

45 ('M/S', 'M/S**2'): response.DifferentiationResponse(1), 

46 ('M/S**2', 'M/S**2'): None, 

47 ('RAD', 'RAD'): None, 

48 ('RAD/S', 'RAD'): response.IntegrationResponse(1), 

49 ('RAD/S**2', 'RAD'): response.IntegrationResponse(2), 

50 ('RAD', 'RAD/S'): response.DifferentiationResponse(1), 

51 ('RAD/S', 'RAD/S'): None, 

52 ('RAD/S**2', 'RAD/S'): response.IntegrationResponse(1), 

53 ('RAD', 'RAD/S**2'): response.DifferentiationResponse(2), 

54 ('RAD/S', 'RAD/S**2'): response.DifferentiationResponse(1), 

55 ('RAD/S**2', 'RAD/S**2'): None} 

56 

57 

58units_to_quantity = { 

59 'M/S': 'velocity', 

60 'M': 'displacement', 

61 'M/S**2': 'acceleration', 

62 'V': 'voltage', 

63 'COUNT': 'counts', 

64 'PA': 'pressure', 

65 'RAD': 'rotation_displacement', 

66 'RAD/S': 'rotation_velocity', 

67 'RAD/S**2': 'rotation_acceleration'} 

68 

69 

70quantity_to_units = dict((v, k) for (k, v) in units_to_quantity.items()) 

71 

72 

73units_fixes = { 

74 'R': 'RAD', 

75 'R/S': 'RAD/S', 

76 'R/S**2': 'RAD/S**2', 

77 'COUNTS': 'COUNT'} 

78 

79 

80def sanitize_units(s): 

81 s = s.upper() 

82 return units_fixes.get(s, s) 

83 

84 

85def to_quantity(units, context, delivery): 

86 

87 if units is None: 

88 return None 

89 

90 name = sanitize_units(units.name) 

91 if name in units_to_quantity: 

92 return units_to_quantity[name] 

93 else: 

94 delivery.log.append(( 

95 'warning', 

96 'Units not supported by Squirrel framework: %s' % ( 

97 name + ( 

98 ' (%s)' % units.description if units.description else '')), 

99 context)) 

100 

101 return 'unsupported_quantity(%s)' % units 

102 

103 

104class StationXMLError(Exception): 

105 pass 

106 

107 

108class Inconsistencies(StationXMLError): 

109 pass 

110 

111 

112class NoResponseInformation(StationXMLError): 

113 pass 

114 

115 

116class MultipleResponseInformation(StationXMLError): 

117 pass 

118 

119 

120class InconsistentResponseInformation(StationXMLError): 

121 pass 

122 

123 

124class InconsistentChannelLocations(StationXMLError): 

125 pass 

126 

127 

128class InvalidRecord(StationXMLError): 

129 def __init__(self, line): 

130 StationXMLError.__init__(self) 

131 self._line = line 

132 

133 def __str__(self): 

134 return 'Invalid record: "%s"' % self._line 

135 

136 

137_exceptions = dict( 

138 Inconsistencies=Inconsistencies, 

139 NoResponseInformation=NoResponseInformation, 

140 MultipleResponseInformation=MultipleResponseInformation, 

141 InconsistentResponseInformation=InconsistentResponseInformation, 

142 InconsistentChannelLocations=InconsistentChannelLocations, 

143 InvalidRecord=InvalidRecord, 

144 ValueError=ValueError) 

145 

146 

147_logs = dict( 

148 info=logger.info, 

149 warning=logger.warning, 

150 error=logger.error) 

151 

152 

153class DeliveryError(StationXMLError): 

154 pass 

155 

156 

157class Delivery(Object): 

158 

159 def __init__(self, payload=None, log=None, errors=None, error=None): 

160 if payload is None: 

161 payload = [] 

162 

163 if log is None: 

164 log = [] 

165 

166 if errors is None: 

167 errors = [] 

168 

169 if error is not None: 

170 errors.append(error) 

171 

172 Object.__init__(self, payload=payload, log=log, errors=errors) 

173 

174 payload = List.T(Any.T()) 

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

176 errors = List.T(Tuple.T(3, String.T())) 

177 

178 def extend(self, other): 

179 self.payload.extend(other.payload) 

180 self.log.extend(other.log) 

181 self.errors.extend(other.errors) 

182 

183 def extend_without_payload(self, other): 

184 self.log.extend(other.log) 

185 self.errors.extend(other.errors) 

186 return other.payload 

187 

188 def emit_log(self): 

189 for name, message, context in self.log: 

190 message = '%s: %s' % (context, message) 

191 _logs[name](message) 

192 

193 def expect(self, quiet=False): 

194 if not quiet: 

195 self.emit_log() 

196 

197 if self.errors: 

198 name, message, context = self.errors[0] 

199 if context: 

200 message += ' (%s)' % context 

201 

202 if len(self.errors) > 1: 

203 message += ' Additional errors pending.' 

204 

205 raise _exceptions[name](message) 

206 

207 return self.payload 

208 

209 def expect_one(self, quiet=False): 

210 payload = self.expect(quiet=quiet) 

211 if len(payload) != 1: 

212 raise DeliveryError( 

213 'Expected 1 element but got %i.' % len(payload)) 

214 

215 return payload[0] 

216 

217 

218def wrap(s, width=80, indent=4): 

219 words = s.split() 

220 lines = [] 

221 t = [] 

222 n = 0 

223 for w in words: 

224 if n + len(w) >= width: 

225 lines.append(' '.join(t)) 

226 n = indent 

227 t = [' '*(indent-1)] 

228 

229 t.append(w) 

230 n += len(w) + 1 

231 

232 lines.append(' '.join(t)) 

233 return '\n'.join(lines) 

234 

235 

236def same(x, eps=0.0): 

237 if any(type(x[0]) != type(r) for r in x): 

238 return False 

239 

240 if isinstance(x[0], float): 

241 return all(abs(r-x[0]) <= eps for r in x) 

242 else: 

243 return all(r == x[0] for r in x) 

244 

245 

246def same_sample_rate(a, b, eps=1.0e-6): 

247 return abs(a - b) < min(a, b)*eps 

248 

249 

250def evaluate1(resp, f): 

251 return resp.evaluate(num.array([f], dtype=float))[0] 

252 

253 

254def check_resp(resp, value, frequency, limit_db, prelude, context): 

255 

256 try: 

257 value_resp = num.abs(evaluate1(resp, frequency)) 

258 except response.InvalidResponseError as e: 

259 return Delivery(log=[( 

260 'warning', 

261 'Could not check response: %s' % str(e), 

262 context)]) 

263 

264 if value_resp == 0.0: 

265 return Delivery(log=[( 

266 'warning', 

267 '%s\n' 

268 ' computed response is zero' % prelude, 

269 context)]) 

270 

271 diff_db = 20.0 * num.log10(value_resp/value) 

272 

273 if num.abs(diff_db) > limit_db: 

274 return Delivery(log=[( 

275 'warning', 

276 '%s\n' 

277 ' reported value: %g\n' 

278 ' computed value: %g\n' 

279 ' at frequency [Hz]: %g\n' 

280 ' factor: %g\n' 

281 ' difference [dB]: %g\n' 

282 ' limit [dB]: %g' % ( 

283 prelude, 

284 value, 

285 value_resp, 

286 frequency, 

287 value_resp/value, 

288 diff_db, 

289 limit_db), 

290 context)]) 

291 

292 return Delivery() 

293 

294 

295def tts(t): 

296 if t is None: 

297 return '<none>' 

298 else: 

299 return util.tts(t, format='%Y-%m-%d %H:%M:%S') 

300 

301 

302def le_open_left(a, b): 

303 return a is None or (b is not None and a <= b) 

304 

305 

306def le_open_right(a, b): 

307 return b is None or (a is not None and a <= b) 

308 

309 

310def eq_open(a, b): 

311 return (a is None and b is None) \ 

312 or (a is not None and b is not None and a == b) 

313 

314 

315def contains(a, b): 

316 return le_open_left(a.start_date, b.start_date) \ 

317 and le_open_right(b.end_date, a.end_date) 

318 

319 

320def find_containing(candidates, node): 

321 for candidate in candidates: 

322 if contains(candidate, node): 

323 return candidate 

324 

325 return None 

326 

327 

328this_year = time.gmtime()[0] 

329 

330 

331class DummyAwareOptionalTimestamp(Object): 

332 ''' 

333 Optional timestamp with support for some common placeholder values. 

334 

335 Some StationXML files contain arbitrary placeholder values for open end 

336 intervals, like "2100-01-01". Depending on the time range supported by the 

337 system, these dates are translated into ``None`` to prevent crashes with 

338 this type. 

339 ''' 

340 dummy_for = (hpfloat, float) 

341 dummy_for_description = 'pyrocko.util.get_time_float' 

342 

343 class __T(TBase): 

344 

345 def regularize_extra(self, val): 

346 time_float = get_time_float() 

347 

348 if isinstance(val, datetime.datetime): 

349 tt = val.utctimetuple() 

350 val = time_float(calendar.timegm(tt)) + val.microsecond * 1e-6 

351 

352 elif isinstance(val, datetime.date): 

353 tt = val.timetuple() 

354 val = time_float(calendar.timegm(tt)) 

355 

356 elif isinstance(val, str): 

357 val = val.strip() 

358 

359 tz_offset = 0 

360 

361 m = re_tz.search(val) 

362 if m: 

363 sh = m.group(2) 

364 sm = m.group(4) 

365 tz_offset = (int(sh)*3600 if sh else 0) \ 

366 + (int(sm)*60 if sm else 0) 

367 

368 val = re_tz.sub('', val) 

369 

370 if len(val) > 10 and val[10] == 'T': 

371 val = val.replace('T', ' ', 1) 

372 

373 try: 

374 val = util.str_to_time(val) - tz_offset 

375 

376 except util.TimeStrError: 

377 year = int(val[:4]) 

378 if sys.maxsize > 2**32: # if we're on 64bit 

379 if year > this_year + 100: 

380 return None # StationXML contained a dummy date 

381 

382 if year < 1903: # for macOS, 1900-01-01 dummy dates 

383 return None 

384 

385 else: # 32bit end of time is in 2038 

386 if this_year < 2037 and year > 2037 or year < 1903: 

387 return None # StationXML contained a dummy date 

388 

389 raise 

390 

391 elif isinstance(val, (int, float)): 

392 val = time_float(val) 

393 

394 else: 

395 raise ValidationError( 

396 '%s: cannot convert "%s" to type %s' % ( 

397 self.xname(), val, time_float)) 

398 

399 return val 

400 

401 def to_save(self, val): 

402 return time_to_str(val, format='%Y-%m-%d %H:%M:%S.9FRAC')\ 

403 .rstrip('0').rstrip('.') 

404 

405 def to_save_xml(self, val): 

406 return time_to_str(val, format='%Y-%m-%dT%H:%M:%S.9FRAC')\ 

407 .rstrip('0').rstrip('.') + 'Z' 

408 

409 

410class Nominal(StringChoice): 

411 choices = [ 

412 'NOMINAL', 

413 'CALCULATED'] 

414 

415 

416class Email(UnicodePattern): 

417 pattern = u'[\\w\\.\\-_]+@[\\w\\.\\-_]+' 

418 

419 

420class RestrictedStatus(StringChoice): 

421 choices = [ 

422 'open', 

423 'closed', 

424 'partial'] 

425 

426 

427class Type(StringChoice): 

428 choices = [ 

429 'TRIGGERED', 

430 'CONTINUOUS', 

431 'HEALTH', 

432 'GEOPHYSICAL', 

433 'WEATHER', 

434 'FLAG', 

435 'SYNTHESIZED', 

436 'INPUT', 

437 'EXPERIMENTAL', 

438 'MAINTENANCE', 

439 'BEAM'] 

440 

441 class __T(StringChoice.T): 

442 def validate_extra(self, val): 

443 if val not in self.choices: 

444 logger.warning( 

445 'channel type: "%s" is not a valid choice out of %s' % 

446 (val, repr(self.choices))) 

447 

448 

449class PzTransferFunction(StringChoice): 

450 choices = [ 

451 'LAPLACE (RADIANS/SECOND)', 

452 'LAPLACE (HERTZ)', 

453 'DIGITAL (Z-TRANSFORM)'] 

454 

455 

456class Symmetry(StringChoice): 

457 choices = [ 

458 'NONE', 

459 'EVEN', 

460 'ODD'] 

461 

462 

463class CfTransferFunction(StringChoice): 

464 

465 class __T(StringChoice.T): 

466 def validate(self, val, regularize=False, depth=-1): 

467 if regularize: 

468 try: 

469 val = str(val) 

470 except ValueError: 

471 raise ValidationError( 

472 '%s: cannot convert to string %s' % (self.xname, 

473 repr(val))) 

474 

475 val = self._dummy_cls.replacements.get(val, val) 

476 

477 self.validate_extra(val) 

478 return val 

479 

480 choices = [ 

481 'ANALOG (RADIANS/SECOND)', 

482 'ANALOG (HERTZ)', 

483 'DIGITAL'] 

484 

485 replacements = { 

486 'ANALOG (RAD/SEC)': 'ANALOG (RADIANS/SECOND)', 

487 'ANALOG (HZ)': 'ANALOG (HERTZ)', 

488 } 

489 

490 

491class Approximation(StringChoice): 

492 choices = [ 

493 'MACLAURIN'] 

494 

495 

496class PhoneNumber(StringPattern): 

497 pattern = '[0-9]+-[0-9]+' 

498 

499 

500class Site(Object): 

501 ''' 

502 Description of a site location using name and optional geopolitical 

503 boundaries (country, city, etc.). 

504 ''' 

505 

506 name = Unicode.T(default='', xmltagname='Name') 

507 description = Unicode.T(optional=True, xmltagname='Description') 

508 town = Unicode.T(optional=True, xmltagname='Town') 

509 county = Unicode.T(optional=True, xmltagname='County') 

510 region = Unicode.T(optional=True, xmltagname='Region') 

511 country = Unicode.T(optional=True, xmltagname='Country') 

512 

513 

514class ExternalReference(Object): 

515 ''' 

516 This type contains a URI and description for external data that users may 

517 want to reference in StationXML. 

518 ''' 

519 

520 uri = String.T(xmltagname='URI') 

521 description = Unicode.T(xmltagname='Description') 

522 

523 

524class Units(Object): 

525 ''' 

526 A type to document units. Corresponds to SEED blockette 34. 

527 ''' 

528 

529 def __init__(self, name=None, **kwargs): 

530 Object.__init__(self, name=name, **kwargs) 

531 

532 name = String.T(xmltagname='Name') 

533 description = Unicode.T(optional=True, xmltagname='Description') 

534 

535 

536class Counter(Int): 

537 pass 

538 

539 

540class SampleRateRatio(Object): 

541 ''' 

542 Sample rate expressed as number of samples in a number of seconds. 

543 ''' 

544 

545 number_samples = Int.T(xmltagname='NumberSamples') 

546 number_seconds = Int.T(xmltagname='NumberSeconds') 

547 

548 

549class Gain(Object): 

550 ''' 

551 Complex type for sensitivity and frequency ranges. This complex type can be 

552 used to represent both overall sensitivities and individual stage gains. 

553 The FrequencyRangeGroup is an optional construct that defines a pass band 

554 in Hertz ( FrequencyStart and FrequencyEnd) in which the SensitivityValue 

555 is valid within the number of decibels specified in FrequencyDBVariation. 

556 ''' 

557 

558 def __init__(self, value=None, **kwargs): 

559 Object.__init__(self, value=value, **kwargs) 

560 

561 value = Float.T(optional=True, xmltagname='Value') 

562 frequency = Float.T(optional=True, xmltagname='Frequency') 

563 

564 def summary(self): 

565 return 'gain(%g @ %g)' % (self.value, self.frequency) 

566 

567 

568class NumeratorCoefficient(Object): 

569 i = Int.T(optional=True, xmlstyle='attribute') 

570 value = Float.T(xmlstyle='content') 

571 

572 

573class FloatNoUnit(Object): 

574 def __init__(self, value=None, **kwargs): 

575 Object.__init__(self, value=value, **kwargs) 

576 

577 plus_error = Float.T(optional=True, xmlstyle='attribute') 

578 minus_error = Float.T(optional=True, xmlstyle='attribute') 

579 value = Float.T(xmlstyle='content') 

580 

581 

582class FloatWithUnit(FloatNoUnit): 

583 unit = String.T(optional=True, xmlstyle='attribute') 

584 

585 

586class Equipment(Object): 

587 resource_id = String.T(optional=True, xmlstyle='attribute') 

588 type = String.T(optional=True, xmltagname='Type') 

589 description = Unicode.T(optional=True, xmltagname='Description') 

590 manufacturer = Unicode.T(optional=True, xmltagname='Manufacturer') 

591 vendor = Unicode.T(optional=True, xmltagname='Vendor') 

592 model = Unicode.T(optional=True, xmltagname='Model') 

593 serial_number = String.T(optional=True, xmltagname='SerialNumber') 

594 installation_date = DummyAwareOptionalTimestamp.T( 

595 optional=True, 

596 xmltagname='InstallationDate') 

597 removal_date = DummyAwareOptionalTimestamp.T( 

598 optional=True, 

599 xmltagname='RemovalDate') 

600 calibration_date_list = List.T(Timestamp.T(xmltagname='CalibrationDate')) 

601 

602 

603class PhoneNumber(Object): 

604 description = Unicode.T(optional=True, xmlstyle='attribute') 

605 country_code = Int.T(optional=True, xmltagname='CountryCode') 

606 area_code = Int.T(xmltagname='AreaCode') 

607 phone_number = PhoneNumber.T(xmltagname='PhoneNumber') 

608 

609 

610class BaseFilter(Object): 

611 ''' 

612 The BaseFilter is derived by all filters. 

613 ''' 

614 

615 resource_id = String.T(optional=True, xmlstyle='attribute') 

616 name = String.T(optional=True, xmlstyle='attribute') 

617 description = Unicode.T(optional=True, xmltagname='Description') 

618 input_units = Units.T(optional=True, xmltagname='InputUnits') 

619 output_units = Units.T(optional=True, xmltagname='OutputUnits') 

620 

621 

622class Sensitivity(Gain): 

623 ''' 

624 Sensitivity and frequency ranges. The FrequencyRangeGroup is an optional 

625 construct that defines a pass band in Hertz (FrequencyStart and 

626 FrequencyEnd) in which the SensitivityValue is valid within the number of 

627 decibels specified in FrequencyDBVariation. 

628 ''' 

629 

630 input_units = Units.T(optional=True, xmltagname='InputUnits') 

631 output_units = Units.T(optional=True, xmltagname='OutputUnits') 

632 frequency_start = Float.T(optional=True, xmltagname='FrequencyStart') 

633 frequency_end = Float.T(optional=True, xmltagname='FrequencyEnd') 

634 frequency_db_variation = Float.T(optional=True, 

635 xmltagname='FrequencyDBVariation') 

636 

637 def get_pyrocko_response(self): 

638 return Delivery( 

639 [response.PoleZeroResponse(constant=self.value)]) 

640 

641 

642class Coefficient(FloatNoUnit): 

643 number = Counter.T(optional=True, xmlstyle='attribute') 

644 

645 

646class PoleZero(Object): 

647 ''' 

648 Complex numbers used as poles or zeros in channel response. 

649 ''' 

650 

651 number = Int.T(optional=True, xmlstyle='attribute') 

652 real = FloatNoUnit.T(xmltagname='Real') 

653 imaginary = FloatNoUnit.T(xmltagname='Imaginary') 

654 

655 def value(self): 

656 return self.real.value + 1J * self.imaginary.value 

657 

658 

659class ClockDrift(FloatWithUnit): 

660 unit = String.T(default='SECONDS/SAMPLE', optional=True, 

661 xmlstyle='attribute') # fixed 

662 

663 

664class Second(FloatWithUnit): 

665 ''' 

666 A time value in seconds. 

667 ''' 

668 

669 unit = String.T(default='SECONDS', optional=True, xmlstyle='attribute') 

670 # fixed unit 

671 

672 

673class Voltage(FloatWithUnit): 

674 unit = String.T(default='VOLTS', optional=True, xmlstyle='attribute') 

675 # fixed unit 

676 

677 

678class Angle(FloatWithUnit): 

679 unit = String.T(default='DEGREES', optional=True, xmlstyle='attribute') 

680 # fixed unit 

681 

682 

683class Azimuth(FloatWithUnit): 

684 ''' 

685 Instrument azimuth, degrees clockwise from North. 

686 ''' 

687 

688 unit = String.T(default='DEGREES', optional=True, xmlstyle='attribute') 

689 # fixed unit 

690 

691 

692class Dip(FloatWithUnit): 

693 ''' 

694 Instrument dip in degrees down from horizontal. Together azimuth and dip 

695 describe the direction of the sensitive axis of the instrument. 

696 ''' 

697 

698 unit = String.T(default='DEGREES', optional=True, xmlstyle='attribute') 

699 # fixed unit 

700 

701 

702class Distance(FloatWithUnit): 

703 ''' 

704 Extension of FloatWithUnit for distances, elevations, and depths. 

705 ''' 

706 

707 unit = String.T(default='METERS', optional=True, xmlstyle='attribute') 

708 # NOT fixed unit! 

709 

710 

711class Frequency(FloatWithUnit): 

712 unit = String.T(default='HERTZ', optional=True, xmlstyle='attribute') 

713 # fixed unit 

714 

715 

716class SampleRate(FloatWithUnit): 

717 ''' 

718 Sample rate in samples per second. 

719 ''' 

720 

721 unit = String.T(default='SAMPLES/S', optional=True, xmlstyle='attribute') 

722 # fixed unit 

723 

724 

725class Person(Object): 

726 ''' 

727 Representation of a person's contact information. A person can belong to 

728 multiple agencies and have multiple email addresses and phone numbers. 

729 ''' 

730 

731 name_list = List.T(Unicode.T(xmltagname='Name')) 

732 agency_list = List.T(Unicode.T(xmltagname='Agency')) 

733 email_list = List.T(Email.T(xmltagname='Email')) 

734 phone_list = List.T(PhoneNumber.T(xmltagname='Phone')) 

735 

736 

737class FIR(BaseFilter): 

738 ''' 

739 Response: FIR filter. Corresponds to SEED blockette 61. FIR filters are 

740 also commonly documented using the Coefficients element. 

741 ''' 

742 

743 symmetry = Symmetry.T(xmltagname='Symmetry') 

744 numerator_coefficient_list = List.T( 

745 NumeratorCoefficient.T(xmltagname='NumeratorCoefficient')) 

746 

747 def summary(self): 

748 return 'fir(%i%s)' % ( 

749 self.get_ncoefficiencs(), 

750 ',sym' if self.get_effective_symmetry() != 'NONE' else '') 

751 

752 def get_effective_coefficients(self): 

753 b = num.array( 

754 [v.value for v in self.numerator_coefficient_list], 

755 dtype=float) 

756 

757 if self.symmetry == 'ODD': 

758 b = num.concatenate((b, b[-2::-1])) 

759 elif self.symmetry == 'EVEN': 

760 b = num.concatenate((b, b[::-1])) 

761 

762 return b 

763 

764 def get_effective_symmetry(self): 

765 if self.symmetry == 'NONE': 

766 b = self.get_effective_coefficients() 

767 if num.all(b - b[::-1] == 0): 

768 return ['EVEN', 'ODD'][b.size % 2] 

769 

770 return self.symmetry 

771 

772 def get_ncoefficiencs(self): 

773 nf = len(self.numerator_coefficient_list) 

774 if self.symmetry == 'ODD': 

775 nc = nf * 2 + 1 

776 elif self.symmetry == 'EVEN': 

777 nc = nf * 2 

778 else: 

779 nc = nf 

780 

781 return nc 

782 

783 def estimate_delay(self, deltat): 

784 nc = self.get_ncoefficiencs() 

785 if nc > 0: 

786 return deltat * (nc - 1) / 2.0 

787 else: 

788 return 0.0 

789 

790 def get_pyrocko_response( 

791 self, context, deltat, delay_responses, normalization_frequency): 

792 

793 context += self.summary() 

794 

795 if not self.numerator_coefficient_list: 

796 return Delivery([]) 

797 

798 b = self.get_effective_coefficients() 

799 

800 log = [] 

801 drop_phase = self.get_effective_symmetry() != 'NONE' 

802 

803 if not deltat: 

804 log.append(( 

805 'error', 

806 'Digital response requires knowledge about sampling ' 

807 'interval. Response will be unusable.', 

808 context)) 

809 

810 resp = response.DigitalFilterResponse( 

811 b.tolist(), [1.0], deltat or 0.0, drop_phase=drop_phase) 

812 

813 if normalization_frequency is not None and deltat is not None: 

814 normalization_frequency = 0.0 

815 normalization = num.abs(evaluate1(resp, normalization_frequency)) 

816 

817 if num.abs(normalization - 1.0) > 1e-2: 

818 log.append(( 

819 'warning', 

820 'FIR filter coefficients are not normalized. Normalizing ' 

821 'them. Factor: %g' % normalization, context)) 

822 

823 resp = response.DigitalFilterResponse( 

824 (b/normalization).tolist(), [1.0], deltat, 

825 drop_phase=drop_phase) 

826 

827 resps = [resp] 

828 

829 if not drop_phase: 

830 resps.extend(delay_responses) 

831 

832 return Delivery(resps, log=log) 

833 

834 

835class Coefficients(BaseFilter): 

836 ''' 

837 Response: coefficients for FIR filter. Laplace transforms or IIR filters 

838 can be expressed using type as well but the PolesAndZeros should be used 

839 instead. Corresponds to SEED blockette 54. 

840 ''' 

841 

842 cf_transfer_function_type = CfTransferFunction.T( 

843 xmltagname='CfTransferFunctionType') 

844 numerator_list = List.T(FloatWithUnit.T(xmltagname='Numerator')) 

845 denominator_list = List.T(FloatWithUnit.T(xmltagname='Denominator')) 

846 

847 def summary(self): 

848 return 'coef_%s(%i,%i%s)' % ( 

849 'ABC?'[ 

850 CfTransferFunction.choices.index( 

851 self.cf_transfer_function_type)], 

852 len(self.numerator_list), 

853 len(self.denominator_list), 

854 ',sym' if self.is_symmetric_fir else '') 

855 

856 def estimate_delay(self, deltat): 

857 nc = len(self.numerator_list) 

858 if nc > 0: 

859 return deltat * (len(self.numerator_list) - 1) / 2.0 

860 else: 

861 return 0.0 

862 

863 def is_symmetric_fir(self): 

864 if len(self.denominator_list) != 0: 

865 return False 

866 b = [v.value for v in self.numerator_list] 

867 return b == b[::-1] 

868 

869 def get_pyrocko_response( 

870 self, context, deltat, delay_responses, normalization_frequency): 

871 

872 context += self.summary() 

873 

874 factor = 1.0 

875 if self.cf_transfer_function_type == 'ANALOG (HERTZ)': 

876 factor = 2.0*math.pi 

877 

878 if not self.numerator_list and not self.denominator_list: 

879 return Delivery(payload=[]) 

880 

881 b = num.array( 

882 [v.value*factor for v in self.numerator_list], dtype=float) 

883 

884 a = num.array( 

885 [1.0] + [v.value*factor for v in self.denominator_list], 

886 dtype=float) 

887 

888 log = [] 

889 resps = [] 

890 if self.cf_transfer_function_type in [ 

891 'ANALOG (RADIANS/SECOND)', 'ANALOG (HERTZ)']: 

892 resps.append(response.AnalogFilterResponse(b, a)) 

893 

894 elif self.cf_transfer_function_type == 'DIGITAL': 

895 if not deltat: 

896 log.append(( 

897 'error', 

898 'Digital response requires knowledge about sampling ' 

899 'interval. Response will be unusable.', 

900 context)) 

901 

902 drop_phase = self.is_symmetric_fir() 

903 resp = response.DigitalFilterResponse( 

904 b, a, deltat or 0.0, drop_phase=drop_phase) 

905 

906 if normalization_frequency is not None and deltat is not None: 

907 normalization = num.abs(evaluate1( 

908 resp, normalization_frequency)) 

909 

910 if num.abs(normalization - 1.0) > 1e-2: 

911 log.append(( 

912 'warning', 

913 'FIR filter coefficients are not normalized. ' 

914 'Normalizing them. Factor: %g' % normalization, 

915 context)) 

916 

917 resp = response.DigitalFilterResponse( 

918 (b/normalization).tolist(), [1.0], deltat, 

919 drop_phase=drop_phase) 

920 

921 resps.append(resp) 

922 

923 if not drop_phase: 

924 resps.extend(delay_responses) 

925 

926 else: 

927 return Delivery(error=( 

928 'ValueError', 

929 'Unknown transfer function type: %s' % ( 

930 self.cf_transfer_function_type))) 

931 

932 return Delivery(payload=resps, log=log) 

933 

934 

935class Latitude(FloatWithUnit): 

936 ''' 

937 Type for latitude coordinate. 

938 ''' 

939 

940 unit = String.T(default='DEGREES', optional=True, xmlstyle='attribute') 

941 # fixed unit 

942 datum = String.T(default='WGS84', optional=True, xmlstyle='attribute') 

943 

944 

945class Longitude(FloatWithUnit): 

946 ''' 

947 Type for longitude coordinate. 

948 ''' 

949 

950 unit = String.T(default='DEGREES', optional=True, xmlstyle='attribute') 

951 # fixed unit 

952 datum = String.T(default='WGS84', optional=True, xmlstyle='attribute') 

953 

954 

955class PolesZeros(BaseFilter): 

956 ''' 

957 Response: complex poles and zeros. Corresponds to SEED blockette 53. 

958 ''' 

959 

960 pz_transfer_function_type = PzTransferFunction.T( 

961 xmltagname='PzTransferFunctionType') 

962 normalization_factor = Float.T( 

963 default=1.0, 

964 xmltagname='NormalizationFactor') 

965 normalization_frequency = Frequency.T( 

966 optional=True, # but required by standard 

967 xmltagname='NormalizationFrequency') 

968 zero_list = List.T(PoleZero.T(xmltagname='Zero')) 

969 pole_list = List.T(PoleZero.T(xmltagname='Pole')) 

970 

971 def summary(self): 

972 return 'pz_%s(%i,%i)' % ( 

973 'ABC?'[ 

974 PzTransferFunction.choices.index( 

975 self.pz_transfer_function_type)], 

976 len(self.pole_list), 

977 len(self.zero_list)) 

978 

979 def get_pyrocko_response(self, context='', deltat=None): 

980 

981 context += self.summary() 

982 

983 factor = 1.0 

984 cfactor = 1.0 

985 if self.pz_transfer_function_type == 'LAPLACE (HERTZ)': 

986 factor = 2. * math.pi 

987 cfactor = (2. * math.pi)**( 

988 len(self.pole_list) - len(self.zero_list)) 

989 

990 log = [] 

991 if self.normalization_factor is None \ 

992 or self.normalization_factor == 0.0: 

993 

994 log.append(( 

995 'warning', 

996 'No pole-zero normalization factor given. ' 

997 'Assuming a value of 1.0', 

998 context)) 

999 

1000 nfactor = 1.0 

1001 else: 

1002 nfactor = self.normalization_factor 

1003 

1004 is_digital = self.pz_transfer_function_type == 'DIGITAL (Z-TRANSFORM)' 

1005 if not is_digital: 

1006 resp = response.PoleZeroResponse( 

1007 constant=nfactor*cfactor, 

1008 zeros=[z.value()*factor for z in self.zero_list], 

1009 poles=[p.value()*factor for p in self.pole_list]) 

1010 else: 

1011 if not deltat: 

1012 log.append(( 

1013 'error', 

1014 'Digital response requires knowledge about sampling ' 

1015 'interval. Response will be unusable.', 

1016 context)) 

1017 

1018 resp = response.DigitalPoleZeroResponse( 

1019 constant=nfactor*cfactor, 

1020 zeros=[z.value()*factor for z in self.zero_list], 

1021 poles=[p.value()*factor for p in self.pole_list], 

1022 deltat=deltat or 0.0) 

1023 

1024 if not self.normalization_frequency: 

1025 log.append(( 

1026 'warning', 

1027 'Cannot check pole-zero normalization factor: ' 

1028 'No normalization frequency given.', 

1029 context)) 

1030 

1031 else: 

1032 if is_digital and not deltat: 

1033 log.append(( 

1034 'warning', 

1035 'Cannot check computed vs reported normalization ' 

1036 'factor without knowing the sampling interval.', 

1037 context)) 

1038 else: 

1039 computed_normalization_factor = nfactor / abs(evaluate1( 

1040 resp, self.normalization_frequency.value)) 

1041 

1042 db = 20.0 * num.log10( 

1043 computed_normalization_factor / nfactor) 

1044 

1045 if abs(db) > 0.17: 

1046 log.append(( 

1047 'warning', 

1048 'Computed and reported normalization factors differ ' 

1049 'by %g dB: computed: %g, reported: %g' % ( 

1050 db, 

1051 computed_normalization_factor, 

1052 nfactor), 

1053 context)) 

1054 

1055 return Delivery([resp], log) 

1056 

1057 

1058class ResponseListElement(Object): 

1059 frequency = Frequency.T(xmltagname='Frequency') 

1060 amplitude = FloatWithUnit.T(xmltagname='Amplitude') 

1061 phase = Angle.T(xmltagname='Phase') 

1062 

1063 

1064class Polynomial(BaseFilter): 

1065 ''' 

1066 Response: expressed as a polynomial (allows non-linear sensors to be 

1067 described). Corresponds to SEED blockette 62. Can be used to describe a 

1068 stage of acquisition or a complete system. 

1069 ''' 

1070 

1071 approximation_type = Approximation.T(default='MACLAURIN', 

1072 xmltagname='ApproximationType') 

1073 frequency_lower_bound = Frequency.T(xmltagname='FrequencyLowerBound') 

1074 frequency_upper_bound = Frequency.T(xmltagname='FrequencyUpperBound') 

1075 approximation_lower_bound = Float.T(xmltagname='ApproximationLowerBound') 

1076 approximation_upper_bound = Float.T(xmltagname='ApproximationUpperBound') 

1077 maximum_error = Float.T(xmltagname='MaximumError') 

1078 coefficient_list = List.T(Coefficient.T(xmltagname='Coefficient')) 

1079 

1080 def summary(self): 

1081 return 'poly(%i)' % len(self.coefficient_list) 

1082 

1083 

1084class Decimation(Object): 

1085 ''' 

1086 Corresponds to SEED blockette 57. 

1087 ''' 

1088 

1089 input_sample_rate = Frequency.T(xmltagname='InputSampleRate') 

1090 factor = Int.T(xmltagname='Factor') 

1091 offset = Int.T(xmltagname='Offset') 

1092 delay = FloatWithUnit.T(xmltagname='Delay') 

1093 correction = FloatWithUnit.T(xmltagname='Correction') 

1094 

1095 def summary(self): 

1096 return 'deci(%i, %g -> %g, %g)' % ( 

1097 self.factor, 

1098 self.input_sample_rate.value, 

1099 self.input_sample_rate.value / self.factor, 

1100 self.delay.value) 

1101 

1102 def get_pyrocko_response(self): 

1103 if self.delay and self.delay.value != 0.0: 

1104 return Delivery([response.DelayResponse(delay=-self.delay.value)]) 

1105 else: 

1106 return Delivery([]) 

1107 

1108 

1109class Operator(Object): 

1110 agency_list = List.T(Unicode.T(xmltagname='Agency')) 

1111 contact_list = List.T(Person.T(xmltagname='Contact')) 

1112 web_site = String.T(optional=True, xmltagname='WebSite') 

1113 

1114 

1115class Comment(Object): 

1116 ''' 

1117 Container for a comment or log entry. Corresponds to SEED blockettes 31, 51 

1118 and 59. 

1119 ''' 

1120 

1121 id = Counter.T(optional=True, xmlstyle='attribute') 

1122 value = Unicode.T(xmltagname='Value') 

1123 begin_effective_time = DummyAwareOptionalTimestamp.T( 

1124 optional=True, 

1125 xmltagname='BeginEffectiveTime') 

1126 end_effective_time = DummyAwareOptionalTimestamp.T( 

1127 optional=True, 

1128 xmltagname='EndEffectiveTime') 

1129 author_list = List.T(Person.T(xmltagname='Author')) 

1130 

1131 

1132class ResponseList(BaseFilter): 

1133 ''' 

1134 Response: list of frequency, amplitude and phase values. Corresponds to 

1135 SEED blockette 55. 

1136 ''' 

1137 

1138 response_list_element_list = List.T( 

1139 ResponseListElement.T(xmltagname='ResponseListElement')) 

1140 

1141 def summary(self): 

1142 return 'list(%i)' % len(self.response_list_element_list) 

1143 

1144 

1145class Log(Object): 

1146 ''' 

1147 Container for log entries. 

1148 ''' 

1149 

1150 entry_list = List.T(Comment.T(xmltagname='Entry')) 

1151 

1152 

1153class ResponseStage(Object): 

1154 ''' 

1155 This complex type represents channel response and covers SEED blockettes 53 

1156 to 56. 

1157 ''' 

1158 

1159 number = Counter.T(xmlstyle='attribute') 

1160 resource_id = String.T(optional=True, xmlstyle='attribute') 

1161 poles_zeros_list = List.T( 

1162 PolesZeros.T(optional=True, xmltagname='PolesZeros')) 

1163 coefficients_list = List.T( 

1164 Coefficients.T(optional=True, xmltagname='Coefficients')) 

1165 response_list = ResponseList.T(optional=True, xmltagname='ResponseList') 

1166 fir = FIR.T(optional=True, xmltagname='FIR') 

1167 polynomial = Polynomial.T(optional=True, xmltagname='Polynomial') 

1168 decimation = Decimation.T(optional=True, xmltagname='Decimation') 

1169 stage_gain = Gain.T(optional=True, xmltagname='StageGain') 

1170 

1171 def summary(self): 

1172 elements = [] 

1173 

1174 for stuff in [ 

1175 self.poles_zeros_list, self.coefficients_list, 

1176 self.response_list, self.fir, self.polynomial, 

1177 self.decimation, self.stage_gain]: 

1178 

1179 if stuff: 

1180 if isinstance(stuff, Object): 

1181 elements.append(stuff.summary()) 

1182 else: 

1183 elements.extend(obj.summary() for obj in stuff) 

1184 

1185 return '%i: %s %s -> %s' % ( 

1186 self.number, 

1187 ', '.join(elements), 

1188 sanitize_units(self.input_units.name) 

1189 if self.input_units else '?', 

1190 sanitize_units(self.output_units.name) 

1191 if self.output_units else '?') 

1192 

1193 def get_squirrel_response_stage(self, context): 

1194 from pyrocko.squirrel.model import ResponseStage 

1195 

1196 delivery = Delivery() 

1197 delivery_pr = self.get_pyrocko_response(context) 

1198 log = delivery_pr.log 

1199 delivery_pr.log = [] 

1200 elements = delivery.extend_without_payload(delivery_pr) 

1201 

1202 delivery.payload = [ResponseStage( 

1203 input_quantity=to_quantity(self.input_units, context, delivery), 

1204 output_quantity=to_quantity(self.output_units, context, delivery), 

1205 input_sample_rate=self.input_sample_rate, 

1206 output_sample_rate=self.output_sample_rate, 

1207 elements=elements, 

1208 log=log)] 

1209 

1210 return delivery 

1211 

1212 def get_pyrocko_response(self, context, gain_only=False): 

1213 

1214 context = context + ', stage %i' % self.number 

1215 

1216 responses = [] 

1217 log = [] 

1218 if self.stage_gain: 

1219 normalization_frequency = self.stage_gain.frequency or 0.0 

1220 else: 

1221 normalization_frequency = 0.0 

1222 

1223 if not gain_only: 

1224 deltat = None 

1225 delay_responses = [] 

1226 if self.decimation: 

1227 rate = self.decimation.input_sample_rate.value 

1228 if rate > 0.0: 

1229 deltat = 1.0 / rate 

1230 delivery = self.decimation.get_pyrocko_response() 

1231 if delivery.errors: 

1232 return delivery 

1233 

1234 delay_responses = delivery.payload 

1235 log.extend(delivery.log) 

1236 

1237 for pzs in self.poles_zeros_list: 

1238 delivery = pzs.get_pyrocko_response(context, deltat) 

1239 if delivery.errors: 

1240 return delivery 

1241 

1242 pz_resps = delivery.payload 

1243 log.extend(delivery.log) 

1244 responses.extend(pz_resps) 

1245 

1246 # emulate incorrect? evalresp behaviour 

1247 if pzs.normalization_frequency is not None and \ 

1248 pzs.normalization_frequency.value \ 

1249 != normalization_frequency \ 

1250 and normalization_frequency != 0.0: 

1251 

1252 try: 

1253 trial = response.MultiplyResponse(pz_resps) 

1254 anorm = num.abs(evaluate1( 

1255 trial, pzs.normalization_frequency.value)) 

1256 asens = num.abs( 

1257 evaluate1(trial, normalization_frequency)) 

1258 

1259 factor = anorm/asens 

1260 

1261 if abs(factor - 1.0) > 0.01: 

1262 log.append(( 

1263 'warning', 

1264 'PZ normalization frequency (%g) is different ' 

1265 'from stage gain frequency (%s) -> Emulating ' 

1266 'possibly incorrect evalresp behaviour. ' 

1267 'Correction factor: %g' % ( 

1268 pzs.normalization_frequency.value, 

1269 normalization_frequency, 

1270 factor), 

1271 context)) 

1272 

1273 responses.append( 

1274 response.PoleZeroResponse(constant=factor)) 

1275 except response.InvalidResponseError as e: 

1276 log.append(( 

1277 'warning', 

1278 'Could not check response: %s' % str(e), 

1279 context)) 

1280 

1281 if len(self.poles_zeros_list) > 1: 

1282 log.append(( 

1283 'warning', 

1284 'Multiple poles and zeros records in single response ' 

1285 'stage.', 

1286 context)) 

1287 

1288 for cfs in self.coefficients_list + ( 

1289 [self.fir] if self.fir else []): 

1290 

1291 delivery = cfs.get_pyrocko_response( 

1292 context, deltat, delay_responses, 

1293 normalization_frequency) 

1294 

1295 if delivery.errors: 

1296 return delivery 

1297 

1298 responses.extend(delivery.payload) 

1299 log.extend(delivery.log) 

1300 

1301 if len(self.coefficients_list) > 1: 

1302 log.append(( 

1303 'warning', 

1304 'Multiple filter coefficients lists in single response ' 

1305 'stage.', 

1306 context)) 

1307 

1308 if self.response_list: 

1309 log.append(( 

1310 'warning', 

1311 'Unhandled response element of type: ResponseList', 

1312 context)) 

1313 

1314 if self.polynomial: 

1315 log.append(( 

1316 'warning', 

1317 'Unhandled response element of type: Polynomial', 

1318 context)) 

1319 

1320 if self.stage_gain: 

1321 responses.append( 

1322 response.PoleZeroResponse(constant=self.stage_gain.value)) 

1323 

1324 return Delivery(responses, log) 

1325 

1326 @property 

1327 def input_units(self): 

1328 for e in (self.poles_zeros_list + self.coefficients_list + 

1329 [self.response_list, self.fir, self.polynomial]): 

1330 if e is not None: 

1331 return e.input_units 

1332 

1333 return None 

1334 

1335 @property 

1336 def output_units(self): 

1337 for e in (self.poles_zeros_list + self.coefficients_list + 

1338 [self.response_list, self.fir, self.polynomial]): 

1339 if e is not None: 

1340 return e.output_units 

1341 

1342 return None 

1343 

1344 @property 

1345 def input_sample_rate(self): 

1346 if self.decimation: 

1347 return self.decimation.input_sample_rate.value 

1348 

1349 return None 

1350 

1351 @property 

1352 def output_sample_rate(self): 

1353 if self.decimation: 

1354 return self.decimation.input_sample_rate.value \ 

1355 / self.decimation.factor 

1356 

1357 return None 

1358 

1359 

1360class Response(Object): 

1361 resource_id = String.T(optional=True, xmlstyle='attribute') 

1362 instrument_sensitivity = Sensitivity.T(optional=True, 

1363 xmltagname='InstrumentSensitivity') 

1364 instrument_polynomial = Polynomial.T(optional=True, 

1365 xmltagname='InstrumentPolynomial') 

1366 stage_list = List.T(ResponseStage.T(xmltagname='Stage')) 

1367 

1368 @property 

1369 def output_sample_rate(self): 

1370 if self.stage_list: 

1371 return self.stage_list[-1].output_sample_rate 

1372 else: 

1373 return None 

1374 

1375 def check_sample_rates(self, channel): 

1376 

1377 if self.stage_list: 

1378 sample_rate = None 

1379 

1380 for stage in self.stage_list: 

1381 if stage.decimation: 

1382 input_sample_rate = \ 

1383 stage.decimation.input_sample_rate.value 

1384 

1385 if sample_rate is not None and not same_sample_rate( 

1386 sample_rate, input_sample_rate): 

1387 

1388 logger.warning( 

1389 'Response stage %i has unexpected input sample ' 

1390 'rate: %g Hz (expected: %g Hz)' % ( 

1391 stage.number, 

1392 input_sample_rate, 

1393 sample_rate)) 

1394 

1395 sample_rate = input_sample_rate / stage.decimation.factor 

1396 

1397 if sample_rate is not None and channel.sample_rate \ 

1398 and not same_sample_rate( 

1399 sample_rate, channel.sample_rate.value): 

1400 

1401 logger.warning( 

1402 'Channel sample rate (%g Hz) does not match sample rate ' 

1403 'deducted from response stages information (%g Hz).' % ( 

1404 channel.sample_rate.value, 

1405 sample_rate)) 

1406 

1407 def check_units(self): 

1408 

1409 if self.instrument_sensitivity \ 

1410 and self.instrument_sensitivity.input_units: 

1411 

1412 units = sanitize_units( 

1413 self.instrument_sensitivity.input_units.name) 

1414 

1415 if self.stage_list: 

1416 for stage in self.stage_list: 

1417 if units and stage.input_units \ 

1418 and sanitize_units(stage.input_units.name) != units: 

1419 

1420 logger.warning( 

1421 'Input units of stage %i (%s) do not match %s (%s).' 

1422 % ( 

1423 stage.number, 

1424 units, 

1425 'output units of stage %i' 

1426 if stage.number == 0 

1427 else 'sensitivity input units', 

1428 units)) 

1429 

1430 if stage.output_units: 

1431 units = sanitize_units(stage.output_units.name) 

1432 else: 

1433 units = None 

1434 

1435 sout_units = self.instrument_sensitivity.output_units 

1436 if self.instrument_sensitivity and sout_units: 

1437 if units is not None and units != sanitize_units( 

1438 sout_units.name): 

1439 logger.warning( 

1440 'Output units of stage %i (%s) do not match %s (%s).' 

1441 % ( 

1442 stage.number, 

1443 units, 

1444 'sensitivity output units', 

1445 sanitize_units(sout_units.name))) 

1446 

1447 def _sensitivity_checkpoints(self, responses, context): 

1448 delivery = Delivery() 

1449 

1450 if self.instrument_sensitivity: 

1451 sval = self.instrument_sensitivity.value 

1452 sfreq = self.instrument_sensitivity.frequency 

1453 if sval is None: 

1454 delivery.log.append(( 

1455 'warning', 

1456 'No sensitivity value given.', 

1457 context)) 

1458 

1459 elif sval is None: 

1460 delivery.log.append(( 

1461 'warning', 

1462 'Reported sensitivity value is zero.', 

1463 context)) 

1464 

1465 elif sfreq is None: 

1466 delivery.log.append(( 

1467 'warning', 

1468 'Sensitivity frequency not given.', 

1469 context)) 

1470 

1471 else: 

1472 trial = response.MultiplyResponse(responses) 

1473 

1474 delivery.extend( 

1475 check_resp( 

1476 trial, sval, sfreq, 0.1, 

1477 'Instrument sensitivity value inconsistent with ' 

1478 'sensitivity computed from complete response.', 

1479 context)) 

1480 

1481 delivery.payload.append(response.FrequencyResponseCheckpoint( 

1482 frequency=sfreq, value=sval)) 

1483 

1484 return delivery 

1485 

1486 def get_squirrel_response(self, context, **kwargs): 

1487 from pyrocko.squirrel.model import Response 

1488 

1489 if self.stage_list: 

1490 delivery = Delivery() 

1491 for istage, stage in enumerate(self.stage_list): 

1492 delivery.extend(stage.get_squirrel_response_stage(context)) 

1493 

1494 checkpoints = [] 

1495 if not delivery.errors: 

1496 all_responses = [] 

1497 for sq_stage in delivery.payload: 

1498 all_responses.extend(sq_stage.elements) 

1499 

1500 checkpoints.extend(delivery.extend_without_payload( 

1501 self._sensitivity_checkpoints(all_responses, context))) 

1502 

1503 sq_stages = delivery.payload 

1504 if sq_stages: 

1505 if sq_stages[0].input_quantity is None \ 

1506 and self.instrument_sensitivity is not None: 

1507 

1508 sq_stages[0].input_quantity = to_quantity( 

1509 self.instrument_sensitivity.input_units, 

1510 context, delivery) 

1511 sq_stages[-1].output_quantity = to_quantity( 

1512 self.instrument_sensitivity.output_units, 

1513 context, delivery) 

1514 

1515 sq_stages = delivery.expect() 

1516 

1517 return Response( 

1518 stages=sq_stages, 

1519 log=delivery.log, 

1520 checkpoints=checkpoints, 

1521 **kwargs) 

1522 

1523 elif self.instrument_sensitivity: 

1524 raise NoResponseInformation( 

1525 "Only instrument sensitivity given (won't use it). (%s)." 

1526 % context) 

1527 else: 

1528 raise NoResponseInformation( 

1529 'Empty instrument response (%s).' 

1530 % context) 

1531 

1532 def get_pyrocko_response( 

1533 self, context, fake_input_units=None, stages=(0, 1)): 

1534 

1535 if fake_input_units is not None: 

1536 fake_input_units = sanitize_units(fake_input_units) 

1537 

1538 delivery = Delivery() 

1539 if self.stage_list: 

1540 for istage, stage in enumerate(self.stage_list): 

1541 delivery.extend(stage.get_pyrocko_response( 

1542 context, gain_only=not ( 

1543 stages is None or stages[0] <= istage < stages[1]))) 

1544 

1545 elif self.instrument_sensitivity: 

1546 delivery.extend(self.instrument_sensitivity.get_pyrocko_response()) 

1547 

1548 delivery_cp = self._sensitivity_checkpoints(delivery.payload, context) 

1549 checkpoints = delivery.extend_without_payload(delivery_cp) 

1550 if delivery.errors: 

1551 return delivery 

1552 

1553 if fake_input_units is not None: 

1554 if not self.instrument_sensitivity or \ 

1555 self.instrument_sensitivity.input_units is None: 

1556 

1557 delivery.errors.append(( 

1558 'NoResponseInformation', 

1559 'No input units given, so cannot convert to requested ' 

1560 'units: %s' % fake_input_units, 

1561 context)) 

1562 

1563 return delivery 

1564 

1565 input_units = sanitize_units( 

1566 self.instrument_sensitivity.input_units.name) 

1567 

1568 conresp = None 

1569 try: 

1570 conresp = conversion[ 

1571 fake_input_units, input_units] 

1572 

1573 except KeyError: 

1574 delivery.errors.append(( 

1575 'NoResponseInformation', 

1576 'Cannot convert between units: %s, %s' 

1577 % (fake_input_units, input_units), 

1578 context)) 

1579 

1580 if conresp is not None: 

1581 delivery.payload.append(conresp) 

1582 for checkpoint in checkpoints: 

1583 checkpoint.value *= num.abs(evaluate1( 

1584 conresp, checkpoint.frequency)) 

1585 

1586 delivery.payload = [ 

1587 response.MultiplyResponse( 

1588 delivery.payload, 

1589 checkpoints=checkpoints)] 

1590 

1591 return delivery 

1592 

1593 @classmethod 

1594 def from_pyrocko_pz_response(cls, presponse, input_unit, output_unit, 

1595 normalization_frequency=1.0): 

1596 ''' 

1597 Convert Pyrocko pole-zero response to StationXML response. 

1598 

1599 :param presponse: Pyrocko pole-zero response 

1600 :type presponse: :py:class:`~pyrocko.response.PoleZeroResponse` 

1601 :param input_unit: Input unit to be reported in the StationXML 

1602 response. 

1603 :type input_unit: str 

1604 :param output_unit: Output unit to be reported in the StationXML 

1605 response. 

1606 :type output_unit: str 

1607 :param normalization_frequency: Frequency where the normalization 

1608 factor for the StationXML response should be computed. 

1609 :type normalization_frequency: float 

1610 ''' 

1611 

1612 norm_factor = 1.0/float(abs( 

1613 evaluate1(presponse, normalization_frequency) 

1614 / presponse.constant)) 

1615 

1616 pzs = PolesZeros( 

1617 pz_transfer_function_type='LAPLACE (RADIANS/SECOND)', 

1618 normalization_factor=norm_factor, 

1619 normalization_frequency=Frequency(normalization_frequency), 

1620 zero_list=[PoleZero(real=FloatNoUnit(z.real), 

1621 imaginary=FloatNoUnit(z.imag)) 

1622 for z in presponse.zeros], 

1623 pole_list=[PoleZero(real=FloatNoUnit(z.real), 

1624 imaginary=FloatNoUnit(z.imag)) 

1625 for z in presponse.poles]) 

1626 

1627 pzs.validate() 

1628 

1629 stage = ResponseStage( 

1630 number=1, 

1631 poles_zeros_list=[pzs], 

1632 stage_gain=Gain(float(abs(presponse.constant))/norm_factor)) 

1633 

1634 resp = Response( 

1635 instrument_sensitivity=Sensitivity( 

1636 value=stage.stage_gain.value, 

1637 frequency=normalization_frequency, 

1638 input_units=Units(input_unit), 

1639 output_units=Units(output_unit)), 

1640 

1641 stage_list=[stage]) 

1642 

1643 return resp 

1644 

1645 

1646class BaseNode(Object): 

1647 ''' 

1648 A base node type for derivation from: Network, Station and Channel types. 

1649 ''' 

1650 

1651 code = String.T(xmlstyle='attribute') 

1652 start_date = DummyAwareOptionalTimestamp.T(optional=True, 

1653 xmlstyle='attribute') 

1654 end_date = DummyAwareOptionalTimestamp.T(optional=True, 

1655 xmlstyle='attribute') 

1656 restricted_status = RestrictedStatus.T(optional=True, xmlstyle='attribute') 

1657 alternate_code = String.T(optional=True, xmlstyle='attribute') 

1658 historical_code = String.T(optional=True, xmlstyle='attribute') 

1659 description = Unicode.T(optional=True, xmltagname='Description') 

1660 comment_list = List.T(Comment.T(xmltagname='Comment')) 

1661 

1662 def spans(self, *args): 

1663 if len(args) == 0: 

1664 return True 

1665 elif len(args) == 1: 

1666 return ((self.start_date is None or 

1667 self.start_date <= args[0]) and 

1668 (self.end_date is None or 

1669 args[0] <= self.end_date)) 

1670 

1671 elif len(args) == 2: 

1672 return ((self.start_date is None or 

1673 args[1] >= self.start_date) and 

1674 (self.end_date is None or 

1675 self.end_date >= args[0])) 

1676 

1677 

1678def overlaps(a, b): 

1679 return ( 

1680 a.start_date is None or b.end_date is None 

1681 or a.start_date < b.end_date 

1682 ) and ( 

1683 b.start_date is None or a.end_date is None 

1684 or b.start_date < a.end_date 

1685 ) 

1686 

1687 

1688def check_overlaps(node_type_name, codes, nodes): 

1689 errors = [] 

1690 for ia, a in enumerate(nodes): 

1691 for b in nodes[ia+1:]: 

1692 if overlaps(a, b): 

1693 errors.append( 

1694 '%s epochs overlap for %s:\n' 

1695 ' %s - %s\n %s - %s' % ( 

1696 node_type_name, 

1697 '.'.join(codes), 

1698 tts(a.start_date), tts(a.end_date), 

1699 tts(b.start_date), tts(b.end_date))) 

1700 

1701 return errors 

1702 

1703 

1704class Channel(BaseNode): 

1705 ''' 

1706 Equivalent to SEED blockette 52 and parent element for the related the 

1707 response blockettes. 

1708 ''' 

1709 

1710 location_code = String.T(xmlstyle='attribute') 

1711 external_reference_list = List.T( 

1712 ExternalReference.T(xmltagname='ExternalReference')) 

1713 latitude = Latitude.T(xmltagname='Latitude') 

1714 longitude = Longitude.T(xmltagname='Longitude') 

1715 elevation = Distance.T(xmltagname='Elevation') 

1716 depth = Distance.T(xmltagname='Depth') 

1717 azimuth = Azimuth.T(optional=True, xmltagname='Azimuth') 

1718 dip = Dip.T(optional=True, xmltagname='Dip') 

1719 type_list = List.T(Type.T(xmltagname='Type')) 

1720 sample_rate = SampleRate.T(optional=True, xmltagname='SampleRate') 

1721 sample_rate_ratio = SampleRateRatio.T(optional=True, 

1722 xmltagname='SampleRateRatio') 

1723 storage_format = String.T(optional=True, xmltagname='StorageFormat') 

1724 clock_drift = ClockDrift.T(optional=True, xmltagname='ClockDrift') 

1725 calibration_units = Units.T(optional=True, xmltagname='CalibrationUnits') 

1726 sensor = Equipment.T(optional=True, xmltagname='Sensor') 

1727 pre_amplifier = Equipment.T(optional=True, xmltagname='PreAmplifier') 

1728 data_logger = Equipment.T(optional=True, xmltagname='DataLogger') 

1729 equipment_list = List.T(Equipment.T(xmltagname='Equipment')) 

1730 response = Response.T(optional=True, xmltagname='Response') 

1731 

1732 @property 

1733 def position_values(self): 

1734 lat = self.latitude.value 

1735 lon = self.longitude.value 

1736 elevation = value_or_none(self.elevation) 

1737 depth = value_or_none(self.depth) 

1738 return lat, lon, elevation, depth 

1739 

1740 

1741class Station(BaseNode): 

1742 ''' 

1743 This type represents a Station epoch. It is common to only have a single 

1744 station epoch with the station's creation and termination dates as the 

1745 epoch start and end dates. 

1746 ''' 

1747 

1748 latitude = Latitude.T(xmltagname='Latitude') 

1749 longitude = Longitude.T(xmltagname='Longitude') 

1750 elevation = Distance.T(xmltagname='Elevation') 

1751 site = Site.T(default=Site.D(name=''), xmltagname='Site') 

1752 vault = Unicode.T(optional=True, xmltagname='Vault') 

1753 geology = Unicode.T(optional=True, xmltagname='Geology') 

1754 equipment_list = List.T(Equipment.T(xmltagname='Equipment')) 

1755 operator_list = List.T(Operator.T(xmltagname='Operator')) 

1756 creation_date = DummyAwareOptionalTimestamp.T( 

1757 optional=True, xmltagname='CreationDate') 

1758 termination_date = DummyAwareOptionalTimestamp.T( 

1759 optional=True, xmltagname='TerminationDate') 

1760 total_number_channels = Counter.T( 

1761 optional=True, xmltagname='TotalNumberChannels') 

1762 selected_number_channels = Counter.T( 

1763 optional=True, xmltagname='SelectedNumberChannels') 

1764 external_reference_list = List.T( 

1765 ExternalReference.T(xmltagname='ExternalReference')) 

1766 channel_list = List.T(Channel.T(xmltagname='Channel')) 

1767 

1768 @property 

1769 def position_values(self): 

1770 lat = self.latitude.value 

1771 lon = self.longitude.value 

1772 elevation = value_or_none(self.elevation) 

1773 return lat, lon, elevation 

1774 

1775 

1776class Network(BaseNode): 

1777 ''' 

1778 This type represents the Network layer, all station metadata is contained 

1779 within this element. The official name of the network or other descriptive 

1780 information can be included in the Description element. The Network can 

1781 contain 0 or more Stations. 

1782 ''' 

1783 

1784 total_number_stations = Counter.T(optional=True, 

1785 xmltagname='TotalNumberStations') 

1786 selected_number_stations = Counter.T(optional=True, 

1787 xmltagname='SelectedNumberStations') 

1788 station_list = List.T(Station.T(xmltagname='Station')) 

1789 

1790 @property 

1791 def station_code_list(self): 

1792 return sorted(set(s.code for s in self.station_list)) 

1793 

1794 @property 

1795 def sl_code_list(self): 

1796 sls = set() 

1797 for station in self.station_list: 

1798 for channel in station.channel_list: 

1799 sls.add((station.code, channel.location_code)) 

1800 

1801 return sorted(sls) 

1802 

1803 def summary(self, width=80, indent=4): 

1804 sls = self.sl_code_list or [(x,) for x in self.station_code_list] 

1805 lines = ['%s (%i):' % (self.code, len(sls))] 

1806 if sls: 

1807 ssls = ['.'.join(x for x in c if x) for c in sls] 

1808 w = max(len(x) for x in ssls) 

1809 n = (width - indent) / (w+1) 

1810 while ssls: 

1811 lines.append( 

1812 ' ' * indent + ' '.join(x.ljust(w) for x in ssls[:n])) 

1813 

1814 ssls[:n] = [] 

1815 

1816 return '\n'.join(lines) 

1817 

1818 

1819def value_or_none(x): 

1820 if x is not None: 

1821 return x.value 

1822 else: 

1823 return None 

1824 

1825 

1826def pyrocko_station_from_channels(nsl, channels, inconsistencies='warn'): 

1827 

1828 pos = lat, lon, elevation, depth = \ 

1829 channels[0].position_values 

1830 

1831 if not all(pos == x.position_values for x in channels): 

1832 info = '\n'.join( 

1833 ' %s: %s' % (x.code, x.position_values) for 

1834 x in channels) 

1835 

1836 mess = 'encountered inconsistencies in channel ' \ 

1837 'lat/lon/elevation/depth ' \ 

1838 'for %s.%s.%s: \n%s' % (nsl + (info,)) 

1839 

1840 if inconsistencies == 'raise': 

1841 raise InconsistentChannelLocations(mess) 

1842 

1843 elif inconsistencies == 'warn': 

1844 logger.warning(mess) 

1845 logger.warning(' -> using mean values') 

1846 

1847 apos = num.array([x.position_values for x in channels], dtype=float) 

1848 mlat, mlon, mele, mdep = num.nansum(apos, axis=0) \ 

1849 / num.sum(num.isfinite(apos), axis=0) 

1850 

1851 groups = {} 

1852 for channel in channels: 

1853 if channel.code not in groups: 

1854 groups[channel.code] = [] 

1855 

1856 groups[channel.code].append(channel) 

1857 

1858 pchannels = [] 

1859 for code in sorted(groups.keys()): 

1860 data = [ 

1861 (channel.code, value_or_none(channel.azimuth), 

1862 value_or_none(channel.dip)) 

1863 for channel in groups[code]] 

1864 

1865 azimuth, dip = util.consistency_merge( 

1866 data, 

1867 message='channel orientation values differ:', 

1868 error=inconsistencies) 

1869 

1870 pchannels.append( 

1871 pyrocko.model.Channel(code, azimuth=azimuth, dip=dip)) 

1872 

1873 return pyrocko.model.Station( 

1874 *nsl, 

1875 lat=mlat, 

1876 lon=mlon, 

1877 elevation=mele, 

1878 depth=mdep, 

1879 channels=pchannels) 

1880 

1881 

1882class FDSNStationXML(Object): 

1883 ''' 

1884 Top-level type for Station XML. Required field are Source (network ID of 

1885 the institution sending the message) and one or more Network containers or 

1886 one or more Station containers. 

1887 ''' 

1888 

1889 schema_version = Float.T(default=1.0, xmlstyle='attribute') 

1890 source = String.T(xmltagname='Source') 

1891 sender = String.T(optional=True, xmltagname='Sender') 

1892 module = String.T(optional=True, xmltagname='Module') 

1893 module_uri = String.T(optional=True, xmltagname='ModuleURI') 

1894 created = Timestamp.T(optional=True, xmltagname='Created') 

1895 network_list = List.T(Network.T(xmltagname='Network')) 

1896 

1897 xmltagname = 'FDSNStationXML' 

1898 guessable_xmlns = [guts_xmlns] 

1899 

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

1901 Object.__init__(self, *args, **kwargs) 

1902 if self.created is None: 

1903 self.created = time.time() 

1904 

1905 def get_pyrocko_stations(self, nslcs=None, nsls=None, 

1906 time=None, timespan=None, 

1907 inconsistencies='warn', 

1908 sloppy=False): 

1909 

1910 assert inconsistencies in ('raise', 'warn') 

1911 

1912 if nslcs is not None: 

1913 nslcs = set(nslcs) 

1914 

1915 if nsls is not None: 

1916 nsls = set(nsls) 

1917 

1918 tt = () 

1919 if time is not None: 

1920 tt = (time,) 

1921 elif timespan is not None: 

1922 tt = timespan 

1923 

1924 ns_have = set() 

1925 pstations = [] 

1926 sensor_to_channels = defaultdict(list) 

1927 for network in self.network_list: 

1928 if not network.spans(*tt): 

1929 continue 

1930 

1931 for station in network.station_list: 

1932 if not station.spans(*tt): 

1933 continue 

1934 

1935 if station.channel_list: 

1936 loc_to_channels = {} 

1937 for channel in station.channel_list: 

1938 if not channel.spans(*tt): 

1939 continue 

1940 

1941 loc = channel.location_code.strip() 

1942 if loc not in loc_to_channels: 

1943 loc_to_channels[loc] = [] 

1944 

1945 loc_to_channels[loc].append(channel) 

1946 

1947 for loc in sorted(loc_to_channels.keys()): 

1948 channels = loc_to_channels[loc] 

1949 if nslcs is not None: 

1950 channels = [channel for channel in channels 

1951 if (network.code, station.code, loc, 

1952 channel.code) in nslcs] 

1953 

1954 if not channels: 

1955 continue 

1956 

1957 nsl = network.code, station.code, loc 

1958 if nsls is not None and nsl not in nsls: 

1959 continue 

1960 

1961 for channel in channels: 

1962 k = (nsl, channel.code[:-1]) 

1963 if not sloppy: 

1964 k += (channel.start_date, channel.end_date) 

1965 

1966 sensor_to_channels[k].append(channel) 

1967 

1968 else: 

1969 ns = (network.code, station.code) 

1970 if ns in ns_have: 

1971 message = 'Duplicate station ' \ 

1972 '(multiple epochs match): %s.%s ' % ns 

1973 

1974 if inconsistencies == 'raise': 

1975 raise Inconsistencies(message) 

1976 else: 

1977 logger.warning(message) 

1978 

1979 ns_have.add(ns) 

1980 

1981 pstations.append(pyrocko.model.Station( 

1982 network.code, station.code, '*', 

1983 lat=station.latitude.value, 

1984 lon=station.longitude.value, 

1985 elevation=value_or_none(station.elevation), 

1986 name=station.description or '')) 

1987 

1988 sensor_have = set() 

1989 for k, channels in sensor_to_channels.items(): 

1990 (nsl, bi) = k[:2] 

1991 if (nsl, bi) in sensor_have: 

1992 message = 'Duplicate station ' \ 

1993 '(multiple epochs match): %s.%s.%s' % nsl 

1994 

1995 if inconsistencies == 'raise': 

1996 raise Inconsistencies(message) 

1997 else: 

1998 logger.warning(message) 

1999 

2000 sensor_have.add((nsl, bi)) 

2001 

2002 pstations.append( 

2003 pyrocko_station_from_channels( 

2004 nsl, 

2005 channels, 

2006 inconsistencies=inconsistencies)) 

2007 

2008 return pstations 

2009 

2010 @classmethod 

2011 def from_pyrocko_stations( 

2012 cls, pyrocko_stations, add_flat_responses_from=None): 

2013 

2014 ''' 

2015 Generate :py:class:`FDSNStationXML` from list of 

2016 :py:class;`pyrocko.model.Station` instances. 

2017 

2018 :param pyrocko_stations: list of :py:class;`pyrocko.model.Station` 

2019 instances. 

2020 :param add_flat_responses_from: unit, 'M', 'M/S' or 'M/S**2' 

2021 ''' 

2022 network_dict = defaultdict(list) 

2023 

2024 if add_flat_responses_from: 

2025 assert add_flat_responses_from in ('M', 'M/S', 'M/S**2') 

2026 extra = dict( 

2027 response=Response( 

2028 instrument_sensitivity=Sensitivity( 

2029 value=1.0, 

2030 frequency=1.0, 

2031 input_units=Units(name=add_flat_responses_from)))) 

2032 else: 

2033 extra = {} 

2034 

2035 have_offsets = set() 

2036 for s in pyrocko_stations: 

2037 

2038 if s.north_shift != 0.0 or s.east_shift != 0.0: 

2039 have_offsets.add(s.nsl()) 

2040 

2041 network, station, location = s.nsl() 

2042 channel_list = [] 

2043 for c in s.channels: 

2044 channel_list.append( 

2045 Channel( 

2046 location_code=location, 

2047 code=c.name, 

2048 latitude=Latitude(value=s.effective_lat), 

2049 longitude=Longitude(value=s.effective_lon), 

2050 elevation=Distance(value=s.elevation), 

2051 depth=Distance(value=s.depth), 

2052 azimuth=Azimuth(value=c.azimuth), 

2053 dip=Dip(value=c.dip), 

2054 **extra 

2055 ) 

2056 ) 

2057 

2058 network_dict[network].append( 

2059 Station( 

2060 code=station, 

2061 latitude=Latitude(value=s.effective_lat), 

2062 longitude=Longitude(value=s.effective_lon), 

2063 elevation=Distance(value=s.elevation), 

2064 channel_list=channel_list) 

2065 ) 

2066 

2067 if have_offsets: 

2068 logger.warning( 

2069 'StationXML does not support Cartesian offsets in ' 

2070 'coordinates. Storing effective lat/lon for stations: %s' % 

2071 ', '.join('.'.join(nsl) for nsl in sorted(have_offsets))) 

2072 

2073 timestamp = util.to_time_float(time.time()) 

2074 network_list = [] 

2075 for k, station_list in network_dict.items(): 

2076 

2077 network_list.append( 

2078 Network( 

2079 code=k, station_list=station_list, 

2080 total_number_stations=len(station_list))) 

2081 

2082 sxml = FDSNStationXML( 

2083 source='from pyrocko stations list', created=timestamp, 

2084 network_list=network_list) 

2085 

2086 sxml.validate() 

2087 return sxml 

2088 

2089 def iter_network_stations( 

2090 self, net=None, sta=None, time=None, timespan=None): 

2091 

2092 tt = () 

2093 if time is not None: 

2094 tt = (time,) 

2095 elif timespan is not None: 

2096 tt = timespan 

2097 

2098 for network in self.network_list: 

2099 if not network.spans(*tt) or ( 

2100 net is not None and network.code != net): 

2101 continue 

2102 

2103 for station in network.station_list: 

2104 if not station.spans(*tt) or ( 

2105 sta is not None and station.code != sta): 

2106 continue 

2107 

2108 yield (network, station) 

2109 

2110 def iter_network_station_channels( 

2111 self, net=None, sta=None, loc=None, cha=None, 

2112 time=None, timespan=None): 

2113 

2114 if loc is not None: 

2115 loc = loc.strip() 

2116 

2117 tt = () 

2118 if time is not None: 

2119 tt = (time,) 

2120 elif timespan is not None: 

2121 tt = timespan 

2122 

2123 for network in self.network_list: 

2124 if not network.spans(*tt) or ( 

2125 net is not None and network.code != net): 

2126 continue 

2127 

2128 for station in network.station_list: 

2129 if not station.spans(*tt) or ( 

2130 sta is not None and station.code != sta): 

2131 continue 

2132 

2133 if station.channel_list: 

2134 for channel in station.channel_list: 

2135 if (not channel.spans(*tt) or 

2136 (cha is not None and channel.code != cha) or 

2137 (loc is not None and 

2138 channel.location_code.strip() != loc)): 

2139 continue 

2140 

2141 yield (network, station, channel) 

2142 

2143 def get_channel_groups(self, net=None, sta=None, loc=None, cha=None, 

2144 time=None, timespan=None): 

2145 

2146 groups = {} 

2147 for network, station, channel in self.iter_network_station_channels( 

2148 net, sta, loc, cha, time=time, timespan=timespan): 

2149 

2150 net = network.code 

2151 sta = station.code 

2152 cha = channel.code 

2153 loc = channel.location_code.strip() 

2154 if len(cha) == 3: 

2155 bic = cha[:2] # band and intrument code according to SEED 

2156 elif len(cha) == 1: 

2157 bic = '' 

2158 else: 

2159 bic = cha 

2160 

2161 if channel.response and \ 

2162 channel.response.instrument_sensitivity and \ 

2163 channel.response.instrument_sensitivity.input_units: 

2164 

2165 unit = sanitize_units( 

2166 channel.response.instrument_sensitivity.input_units.name) 

2167 else: 

2168 unit = None 

2169 

2170 bic = (bic, unit) 

2171 

2172 k = net, sta, loc 

2173 if k not in groups: 

2174 groups[k] = {} 

2175 

2176 if bic not in groups[k]: 

2177 groups[k][bic] = [] 

2178 

2179 groups[k][bic].append(channel) 

2180 

2181 for nsl, bic_to_channels in groups.items(): 

2182 bad_bics = [] 

2183 for bic, channels in bic_to_channels.items(): 

2184 sample_rates = [] 

2185 for channel in channels: 

2186 sample_rates.append(channel.sample_rate.value) 

2187 

2188 if not same(sample_rates): 

2189 scs = ','.join(channel.code for channel in channels) 

2190 srs = ', '.join('%e' % x for x in sample_rates) 

2191 err = 'ignoring channels with inconsistent sampling ' + \ 

2192 'rates (%s.%s.%s.%s: %s)' % (nsl + (scs, srs)) 

2193 

2194 logger.warning(err) 

2195 bad_bics.append(bic) 

2196 

2197 for bic in bad_bics: 

2198 del bic_to_channels[bic] 

2199 

2200 return groups 

2201 

2202 def choose_channels( 

2203 self, 

2204 target_sample_rate=None, 

2205 priority_band_code=['H', 'B', 'M', 'L', 'V', 'E', 'S'], 

2206 priority_units=['M/S', 'M/S**2'], 

2207 priority_instrument_code=['H', 'L'], 

2208 time=None, 

2209 timespan=None): 

2210 

2211 nslcs = {} 

2212 for nsl, bic_to_channels in self.get_channel_groups( 

2213 time=time, timespan=timespan).items(): 

2214 

2215 useful_bics = [] 

2216 for bic, channels in bic_to_channels.items(): 

2217 rate = channels[0].sample_rate.value 

2218 

2219 if target_sample_rate is not None and \ 

2220 rate < target_sample_rate*0.99999: 

2221 continue 

2222 

2223 if len(bic[0]) == 2: 

2224 if bic[0][0] not in priority_band_code: 

2225 continue 

2226 

2227 if bic[0][1] not in priority_instrument_code: 

2228 continue 

2229 

2230 unit = bic[1] 

2231 

2232 prio_unit = len(priority_units) 

2233 try: 

2234 prio_unit = priority_units.index(unit) 

2235 except ValueError: 

2236 pass 

2237 

2238 prio_inst = len(priority_instrument_code) 

2239 prio_band = len(priority_band_code) 

2240 if len(channels[0].code) == 3: 

2241 try: 

2242 prio_inst = priority_instrument_code.index( 

2243 channels[0].code[1]) 

2244 except ValueError: 

2245 pass 

2246 

2247 try: 

2248 prio_band = priority_band_code.index( 

2249 channels[0].code[0]) 

2250 except ValueError: 

2251 pass 

2252 

2253 if target_sample_rate is None: 

2254 rate = -rate 

2255 

2256 useful_bics.append((-len(channels), prio_band, rate, prio_unit, 

2257 prio_inst, bic)) 

2258 

2259 useful_bics.sort() 

2260 

2261 for _, _, rate, _, _, bic in useful_bics: 

2262 channels = sorted( 

2263 bic_to_channels[bic], 

2264 key=lambda channel: channel.code) 

2265 

2266 if channels: 

2267 for channel in channels: 

2268 nslcs[nsl + (channel.code,)] = channel 

2269 

2270 break 

2271 

2272 return nslcs 

2273 

2274 def get_pyrocko_response( 

2275 self, nslc, 

2276 time=None, timespan=None, fake_input_units=None, stages=(0, 1)): 

2277 

2278 net, sta, loc, cha = nslc 

2279 resps = [] 

2280 for _, _, channel in self.iter_network_station_channels( 

2281 net, sta, loc, cha, time=time, timespan=timespan): 

2282 resp = channel.response 

2283 if resp: 

2284 resp.check_sample_rates(channel) 

2285 resp.check_units() 

2286 resps.append(resp.get_pyrocko_response( 

2287 '.'.join(nslc), 

2288 fake_input_units=fake_input_units, 

2289 stages=stages).expect_one()) 

2290 

2291 if not resps: 

2292 raise NoResponseInformation('%s.%s.%s.%s' % nslc) 

2293 elif len(resps) > 1: 

2294 raise MultipleResponseInformation('%s.%s.%s.%s' % nslc) 

2295 

2296 return resps[0] 

2297 

2298 @property 

2299 def n_code_list(self): 

2300 return sorted(set(x.code for x in self.network_list)) 

2301 

2302 @property 

2303 def ns_code_list(self): 

2304 nss = set() 

2305 for network in self.network_list: 

2306 for station in network.station_list: 

2307 nss.add((network.code, station.code)) 

2308 

2309 return sorted(nss) 

2310 

2311 @property 

2312 def nsl_code_list(self): 

2313 nsls = set() 

2314 for network in self.network_list: 

2315 for station in network.station_list: 

2316 for channel in station.channel_list: 

2317 nsls.add( 

2318 (network.code, station.code, channel.location_code)) 

2319 

2320 return sorted(nsls) 

2321 

2322 @property 

2323 def nslc_code_list(self): 

2324 nslcs = set() 

2325 for network in self.network_list: 

2326 for station in network.station_list: 

2327 for channel in station.channel_list: 

2328 nslcs.add( 

2329 (network.code, station.code, channel.location_code, 

2330 channel.code)) 

2331 

2332 return sorted(nslcs) 

2333 

2334 def summary(self): 

2335 lst = [ 

2336 'number of n codes: %i' % len(self.n_code_list), 

2337 'number of ns codes: %i' % len(self.ns_code_list), 

2338 'number of nsl codes: %i' % len(self.nsl_code_list), 

2339 'number of nslc codes: %i' % len(self.nslc_code_list) 

2340 ] 

2341 return '\n'.join(lst) 

2342 

2343 def summary_stages(self): 

2344 data = [] 

2345 for network, station, channel in self.iter_network_station_channels(): 

2346 nslc = (network.code, station.code, channel.location_code, 

2347 channel.code) 

2348 

2349 stages = [] 

2350 in_units = '?' 

2351 out_units = '?' 

2352 if channel.response: 

2353 sens = channel.response.instrument_sensitivity 

2354 if sens: 

2355 in_units = sanitize_units(sens.input_units.name) 

2356 out_units = sanitize_units(sens.output_units.name) 

2357 

2358 for stage in channel.response.stage_list: 

2359 stages.append(stage.summary()) 

2360 

2361 data.append( 

2362 (nslc, tts(channel.start_date), tts(channel.end_date), 

2363 in_units, out_units, stages)) 

2364 

2365 data.sort() 

2366 

2367 lst = [] 

2368 for nslc, stmin, stmax, in_units, out_units, stages in data: 

2369 lst.append(' %s: %s - %s, %s -> %s' % ( 

2370 '.'.join(nslc), stmin, stmax, in_units, out_units)) 

2371 for stage in stages: 

2372 lst.append(' %s' % stage) 

2373 

2374 return '\n'.join(lst) 

2375 

2376 def _check_overlaps(self): 

2377 by_nslc = {} 

2378 for network in self.network_list: 

2379 for station in network.station_list: 

2380 for channel in station.channel_list: 

2381 nslc = (network.code, station.code, channel.location_code, 

2382 channel.code) 

2383 if nslc not in by_nslc: 

2384 by_nslc[nslc] = [] 

2385 

2386 by_nslc[nslc].append(channel) 

2387 

2388 errors = [] 

2389 for nslc, channels in by_nslc.items(): 

2390 errors.extend(check_overlaps('Channel', nslc, channels)) 

2391 

2392 return errors 

2393 

2394 def check(self): 

2395 errors = [] 

2396 for meth in [self._check_overlaps]: 

2397 errors.extend(meth()) 

2398 

2399 if errors: 

2400 raise Inconsistencies( 

2401 'Inconsistencies found in StationXML:\n ' 

2402 + '\n '.join(errors)) 

2403 

2404 

2405def load_channel_table(stream): 

2406 

2407 networks = {} 

2408 stations = {} 

2409 

2410 for line in stream: 

2411 line = str(line.decode('ascii')) 

2412 if line.startswith('#'): 

2413 continue 

2414 

2415 t = line.rstrip().split('|') 

2416 

2417 if len(t) != 17: 

2418 logger.warning('Invalid channel record: %s' % line) 

2419 continue 

2420 

2421 (net, sta, loc, cha, lat, lon, ele, dep, azi, dip, sens, scale, 

2422 scale_freq, scale_units, sample_rate, start_date, end_date) = t 

2423 

2424 try: 

2425 scale = float(scale) 

2426 except ValueError: 

2427 scale = None 

2428 

2429 try: 

2430 scale_freq = float(scale_freq) 

2431 except ValueError: 

2432 scale_freq = None 

2433 

2434 try: 

2435 depth = float(dep) 

2436 except ValueError: 

2437 depth = 0.0 

2438 

2439 try: 

2440 azi = float(azi) 

2441 dip = float(dip) 

2442 except ValueError: 

2443 azi = None 

2444 dip = None 

2445 

2446 try: 

2447 if net not in networks: 

2448 network = Network(code=net) 

2449 else: 

2450 network = networks[net] 

2451 

2452 if (net, sta) not in stations: 

2453 station = Station( 

2454 code=sta, latitude=lat, longitude=lon, elevation=ele) 

2455 

2456 station.regularize() 

2457 else: 

2458 station = stations[net, sta] 

2459 

2460 if scale: 

2461 resp = Response( 

2462 instrument_sensitivity=Sensitivity( 

2463 value=scale, 

2464 frequency=scale_freq, 

2465 input_units=scale_units)) 

2466 else: 

2467 resp = None 

2468 

2469 channel = Channel( 

2470 code=cha, 

2471 location_code=loc.strip(), 

2472 latitude=lat, 

2473 longitude=lon, 

2474 elevation=ele, 

2475 depth=depth, 

2476 azimuth=azi, 

2477 dip=dip, 

2478 sensor=Equipment(description=sens), 

2479 response=resp, 

2480 sample_rate=sample_rate, 

2481 start_date=start_date, 

2482 end_date=end_date or None) 

2483 

2484 channel.regularize() 

2485 

2486 except ValidationError: 

2487 raise InvalidRecord(line) 

2488 

2489 if net not in networks: 

2490 networks[net] = network 

2491 

2492 if (net, sta) not in stations: 

2493 stations[net, sta] = station 

2494 network.station_list.append(station) 

2495 

2496 station.channel_list.append(channel) 

2497 

2498 return FDSNStationXML( 

2499 source='created from table input', 

2500 created=time.time(), 

2501 network_list=sorted(networks.values(), key=lambda x: x.code)) 

2502 

2503 

2504def primitive_merge(sxs): 

2505 networks = [] 

2506 for sx in sxs: 

2507 networks.extend(sx.network_list) 

2508 

2509 return FDSNStationXML( 

2510 source='merged from different sources', 

2511 created=time.time(), 

2512 network_list=copy.deepcopy( 

2513 sorted(networks, key=lambda x: x.code))) 

2514 

2515 

2516def split_channels(sx): 

2517 for nslc in sx.nslc_code_list: 

2518 network_list = sx.network_list 

2519 network_list_filtered = [ 

2520 network for network in network_list 

2521 if network.code == nslc[0]] 

2522 

2523 for network in network_list_filtered: 

2524 sx.network_list = [network] 

2525 station_list = network.station_list 

2526 station_list_filtered = [ 

2527 station for station in station_list 

2528 if station.code == nslc[1]] 

2529 

2530 for station in station_list_filtered: 

2531 network.station_list = [station] 

2532 channel_list = station.channel_list 

2533 station.channel_list = [ 

2534 channel for channel in channel_list 

2535 if (channel.location_code, channel.code) == nslc[2:4]] 

2536 

2537 yield nslc, copy.deepcopy(sx) 

2538 

2539 station.channel_list = channel_list 

2540 

2541 network.station_list = station_list 

2542 

2543 sx.network_list = network_list 

2544 

2545 

2546if __name__ == '__main__': 

2547 from optparse import OptionParser 

2548 

2549 util.setup_logging('pyrocko.io.stationxml', 'warning') 

2550 

2551 usage = \ 

2552 'python -m pyrocko.io.stationxml check|stats|stages ' \ 

2553 '<filename> [options]' 

2554 

2555 description = '''Torture StationXML file.''' 

2556 

2557 parser = OptionParser( 

2558 usage=usage, 

2559 description=description, 

2560 formatter=util.BetterHelpFormatter()) 

2561 

2562 (options, args) = parser.parse_args(sys.argv[1:]) 

2563 

2564 if len(args) != 2: 

2565 parser.print_help() 

2566 sys.exit(1) 

2567 

2568 action, path = args 

2569 

2570 sx = load_xml(filename=path) 

2571 if action == 'check': 

2572 try: 

2573 sx.check() 

2574 except Inconsistencies as e: 

2575 logger.error(e) 

2576 sys.exit(1) 

2577 

2578 elif action == 'stats': 

2579 print(sx.summary()) 

2580 

2581 elif action == 'stages': 

2582 print(sx.summary_stages()) 

2583 

2584 else: 

2585 parser.print_help() 

2586 sys.exit('unknown action: %s' % action)