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

1190 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-12-04 10:41 +0000

1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6''' 

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

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]) is not 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 if value == 0.0: 

272 return Delivery(log=[( 

273 'warning', 

274 '%s\n' 

275 ' response value reported in file is zero' % prelude, 

276 context)]) 

277 

278 if value < 0.0: 

279 return Delivery(log=[( 

280 'warning', 

281 '%s\n' 

282 ' response value reported in file is negative' % prelude, 

283 context)]) 

284 

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

286 

287 if num.abs(diff_db) > limit_db: 

288 return Delivery(log=[( 

289 'warning', 

290 '%s\n' 

291 ' reported value: %g\n' 

292 ' computed value: %g\n' 

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

294 ' factor: %g\n' 

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

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

297 prelude, 

298 abs(value), 

299 value_resp, 

300 frequency, 

301 value_resp/abs(value), 

302 diff_db, 

303 limit_db), 

304 context)]) 

305 

306 return Delivery() 

307 

308 

309def tts(t): 

310 if t is None: 

311 return '<none>' 

312 else: 

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

314 

315 

316def le_open_left(a, b): 

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

318 

319 

320def le_open_right(a, b): 

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

322 

323 

324def eq_open(a, b): 

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

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

327 

328 

329def contains(a, b): 

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

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

332 

333 

334def find_containing(candidates, node): 

335 for candidate in candidates: 

336 if contains(candidate, node): 

337 return candidate 

338 

339 return None 

340 

341 

342this_year = time.gmtime()[0] 

343 

344 

345class DummyAwareOptionalTimestamp(Object): 

346 ''' 

347 Optional timestamp with support for some common placeholder values. 

348 

349 Some StationXML files contain arbitrary placeholder values for open end 

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

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

352 this type. 

353 ''' 

354 dummy_for = (hpfloat, float) 

355 dummy_for_description = 'pyrocko.util.get_time_float' 

356 

357 class __T(TBase): 

358 

359 def regularize_extra(self, val): 

360 time_float = get_time_float() 

361 

362 if isinstance(val, datetime.datetime): 

363 tt = val.utctimetuple() 

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

365 

366 elif isinstance(val, datetime.date): 

367 tt = val.timetuple() 

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

369 

370 elif isinstance(val, str): 

371 val = val.strip() 

372 

373 tz_offset = 0 

374 

375 m = re_tz.search(val) 

376 if m: 

377 sh = m.group(2) 

378 sm = m.group(4) 

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

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

381 

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

383 

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

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

386 

387 try: 

388 val = util.str_to_time(val) - tz_offset 

389 

390 except util.TimeStrError: 

391 year = int(val[:4]) 

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

393 if year > this_year + 100: 

394 return None # StationXML contained a dummy date 

395 

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

397 return None 

398 

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

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

401 return None # StationXML contained a dummy date 

402 

403 raise 

404 

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

406 val = time_float(val) 

407 

408 else: 

409 raise ValidationError( 

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

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

412 

413 return val 

414 

415 def to_save(self, val): 

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

417 .rstrip('0').rstrip('.') 

418 

419 def to_save_xml(self, val): 

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

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

422 

423 

424class Nominal(StringChoice): 

425 choices = [ 

426 'NOMINAL', 

427 'CALCULATED'] 

428 

429 

430class Email(UnicodePattern): 

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

432 

433 

434class RestrictedStatus(StringChoice): 

435 choices = [ 

436 'open', 

437 'closed', 

438 'partial'] 

439 

440 

441class Type(StringChoice): 

442 choices = [ 

443 'TRIGGERED', 

444 'CONTINUOUS', 

445 'HEALTH', 

446 'GEOPHYSICAL', 

447 'WEATHER', 

448 'FLAG', 

449 'SYNTHESIZED', 

450 'INPUT', 

451 'EXPERIMENTAL', 

452 'MAINTENANCE', 

453 'BEAM'] 

454 

455 class __T(StringChoice.T): 

456 def validate_extra(self, val): 

457 if val not in self.choices: 

458 logger.warning( 

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

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

461 

462 

463class PzTransferFunction(StringChoice): 

464 choices = [ 

465 'LAPLACE (RADIANS/SECOND)', 

466 'LAPLACE (HERTZ)', 

467 'DIGITAL (Z-TRANSFORM)'] 

468 

469 

470class Symmetry(StringChoice): 

471 choices = [ 

472 'NONE', 

473 'EVEN', 

474 'ODD'] 

475 

476 

477class CfTransferFunction(StringChoice): 

478 

479 class __T(StringChoice.T): 

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

481 if regularize: 

482 try: 

483 val = str(val) 

484 except ValueError: 

485 raise ValidationError( 

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

487 repr(val))) 

488 

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

490 

491 self.validate_extra(val) 

492 return val 

493 

494 choices = [ 

495 'ANALOG (RADIANS/SECOND)', 

496 'ANALOG (HERTZ)', 

497 'DIGITAL'] 

498 

499 replacements = { 

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

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

502 } 

503 

504 

505class Approximation(StringChoice): 

506 choices = [ 

507 'MACLAURIN'] 

508 

509 

510class PhoneNumber(StringPattern): 

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

512 

513 

514class Site(Object): 

515 ''' 

516 Description of a site location using name and optional geopolitical 

517 boundaries (country, city, etc.). 

518 ''' 

519 

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

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

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

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

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

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

526 

527 

528class ExternalReference(Object): 

529 ''' 

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

531 want to reference in StationXML. 

532 ''' 

533 

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

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

536 

537 

538class Units(Object): 

539 ''' 

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

541 ''' 

542 

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

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

545 

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

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

548 

549 

550class Counter(Int): 

551 pass 

552 

553 

554class SampleRateRatio(Object): 

555 ''' 

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

557 ''' 

558 

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

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

561 

562 

563class Gain(Object): 

564 ''' 

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

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

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

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

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

570 ''' 

571 

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

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

574 

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

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

577 

578 def summary(self): 

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

580 

581 

582class NumeratorCoefficient(Object): 

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

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

585 

586 

587class FloatNoUnit(Object): 

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

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

590 

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

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

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

594 

595 

596class FloatWithUnit(FloatNoUnit): 

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

598 

599 

600class Equipment(Object): 

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

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

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

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

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

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

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

608 installation_date = DummyAwareOptionalTimestamp.T( 

609 optional=True, 

610 xmltagname='InstallationDate') 

611 removal_date = DummyAwareOptionalTimestamp.T( 

612 optional=True, 

613 xmltagname='RemovalDate') 

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

615 

616 

617class PhoneNumber(Object): 

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

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

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

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

622 

623 

624class BaseFilter(Object): 

625 ''' 

626 The BaseFilter is derived by all filters. 

627 ''' 

628 

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

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

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

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

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

634 

635 

636class Sensitivity(Gain): 

637 ''' 

638 Sensitivity and frequency ranges. The FrequencyRangeGroup is an optional 

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

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

641 decibels specified in FrequencyDBVariation. 

642 ''' 

643 

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

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

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

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

648 frequency_db_variation = Float.T(optional=True, 

649 xmltagname='FrequencyDBVariation') 

650 

651 def get_pyrocko_response(self): 

652 return Delivery( 

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

654 

655 

656class Coefficient(FloatNoUnit): 

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

658 

659 

660class PoleZero(Object): 

661 ''' 

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

663 ''' 

664 

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

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

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

668 

669 def value(self): 

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

671 

672 

673class ClockDrift(FloatWithUnit): 

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

675 xmlstyle='attribute') # fixed 

676 

677 

678class Second(FloatWithUnit): 

679 ''' 

680 A time value in seconds. 

681 ''' 

682 

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

684 # fixed unit 

685 

686 

687class Voltage(FloatWithUnit): 

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

689 # fixed unit 

690 

691 

692class Angle(FloatWithUnit): 

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

694 # fixed unit 

695 

696 

697class Azimuth(FloatWithUnit): 

698 ''' 

699 Instrument azimuth, degrees clockwise from North. 

700 ''' 

701 

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

703 # fixed unit 

704 

705 

706class Dip(FloatWithUnit): 

707 ''' 

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

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

710 ''' 

711 

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

713 # fixed unit 

714 

715 

716class Distance(FloatWithUnit): 

717 ''' 

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

719 ''' 

720 

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

722 # NOT fixed unit! 

723 

724 

725class Frequency(FloatWithUnit): 

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

727 # fixed unit 

728 

729 

730class SampleRate(FloatWithUnit): 

731 ''' 

732 Sample rate in samples per second. 

733 ''' 

734 

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

736 # fixed unit 

737 

738 

739class Person(Object): 

740 ''' 

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

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

743 ''' 

744 

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

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

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

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

749 

750 

751class FIR(BaseFilter): 

752 ''' 

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

754 also commonly documented using the Coefficients element. 

755 ''' 

756 

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

758 numerator_coefficient_list = List.T( 

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

760 

761 def summary(self): 

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

763 self.get_ncoefficiencs(), 

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

765 

766 def get_effective_coefficients(self): 

767 b = num.array( 

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

769 dtype=float) 

770 

771 if self.symmetry == 'ODD': 

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

773 elif self.symmetry == 'EVEN': 

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

775 

776 return b 

777 

778 def get_effective_symmetry(self): 

779 if self.symmetry == 'NONE': 

780 b = self.get_effective_coefficients() 

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

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

783 

784 return self.symmetry 

785 

786 def get_ncoefficiencs(self): 

787 nf = len(self.numerator_coefficient_list) 

788 if self.symmetry == 'ODD': 

789 nc = nf * 2 + 1 

790 elif self.symmetry == 'EVEN': 

791 nc = nf * 2 

792 else: 

793 nc = nf 

794 

795 return nc 

796 

797 def estimate_delay(self, deltat): 

798 nc = self.get_ncoefficiencs() 

799 if nc > 0: 

800 return deltat * (nc - 1) / 2.0 

801 else: 

802 return 0.0 

803 

804 def get_pyrocko_response( 

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

806 

807 context += self.summary() 

808 

809 if not self.numerator_coefficient_list: 

810 return Delivery([]) 

811 

812 b = self.get_effective_coefficients() 

813 

814 log = [] 

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

816 

817 if not deltat: 

818 log.append(( 

819 'error', 

820 'Digital response requires knowledge about sampling ' 

821 'interval. Response will be unusable.', 

822 context)) 

823 

824 resp = response.DigitalFilterResponse( 

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

826 

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

828 normalization_frequency = 0.0 

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

830 

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

832 log.append(( 

833 'warning', 

834 'FIR filter coefficients are not normalized. Normalizing ' 

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

836 

837 resp = response.DigitalFilterResponse( 

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

839 drop_phase=drop_phase) 

840 

841 resps = [resp] 

842 

843 if not drop_phase: 

844 resps.extend(delay_responses) 

845 

846 return Delivery(resps, log=log) 

847 

848 

849class Coefficients(BaseFilter): 

850 ''' 

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

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

853 instead. Corresponds to SEED blockette 54. 

854 ''' 

855 

856 cf_transfer_function_type = CfTransferFunction.T( 

857 xmltagname='CfTransferFunctionType') 

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

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

860 

861 def summary(self): 

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

863 'ABC?'[ 

864 CfTransferFunction.choices.index( 

865 self.cf_transfer_function_type)], 

866 len(self.numerator_list), 

867 len(self.denominator_list), 

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

869 

870 def estimate_delay(self, deltat): 

871 nc = len(self.numerator_list) 

872 if nc > 0: 

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

874 else: 

875 return 0.0 

876 

877 def is_symmetric_fir(self): 

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

879 return False 

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

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

882 

883 def get_pyrocko_response( 

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

885 

886 context += self.summary() 

887 

888 factor = 1.0 

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

890 factor = 2.0*math.pi 

891 

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

893 return Delivery(payload=[]) 

894 

895 b = num.array( 

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

897 

898 a = num.array( 

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

900 dtype=float) 

901 

902 log = [] 

903 resps = [] 

904 if self.cf_transfer_function_type in [ 

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

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

907 

908 elif self.cf_transfer_function_type == 'DIGITAL': 

909 if not deltat: 

910 log.append(( 

911 'error', 

912 'Digital response requires knowledge about sampling ' 

913 'interval. Response will be unusable.', 

914 context)) 

915 

916 drop_phase = self.is_symmetric_fir() 

917 resp = response.DigitalFilterResponse( 

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

919 

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

921 normalization = num.abs(evaluate1( 

922 resp, normalization_frequency)) 

923 

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

925 log.append(( 

926 'warning', 

927 'FIR filter coefficients are not normalized. ' 

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

929 context)) 

930 

931 resp = response.DigitalFilterResponse( 

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

933 drop_phase=drop_phase) 

934 

935 resps.append(resp) 

936 

937 if not drop_phase: 

938 resps.extend(delay_responses) 

939 

940 else: 

941 return Delivery(error=( 

942 'ValueError', 

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

944 self.cf_transfer_function_type))) 

945 

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

947 

948 

949class Latitude(FloatWithUnit): 

950 ''' 

951 Type for latitude coordinate. 

952 ''' 

953 

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

955 # fixed unit 

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

957 

958 

959class Longitude(FloatWithUnit): 

960 ''' 

961 Type for longitude coordinate. 

962 ''' 

963 

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

965 # fixed unit 

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

967 

968 

969class PolesZeros(BaseFilter): 

970 ''' 

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

972 ''' 

973 

974 pz_transfer_function_type = PzTransferFunction.T( 

975 xmltagname='PzTransferFunctionType') 

976 normalization_factor = Float.T( 

977 default=1.0, 

978 xmltagname='NormalizationFactor') 

979 normalization_frequency = Frequency.T( 

980 optional=True, # but required by standard 

981 xmltagname='NormalizationFrequency') 

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

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

984 

985 def summary(self): 

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

987 'ABC?'[ 

988 PzTransferFunction.choices.index( 

989 self.pz_transfer_function_type)], 

990 len(self.pole_list), 

991 len(self.zero_list)) 

992 

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

994 

995 context += self.summary() 

996 

997 factor = 1.0 

998 cfactor = 1.0 

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

1000 factor = 2. * math.pi 

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

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

1003 

1004 log = [] 

1005 if self.normalization_factor is None \ 

1006 or self.normalization_factor == 0.0: 

1007 

1008 log.append(( 

1009 'warning', 

1010 'No pole-zero normalization factor given. ' 

1011 'Assuming a value of 1.0', 

1012 context)) 

1013 

1014 nfactor = 1.0 

1015 else: 

1016 nfactor = self.normalization_factor 

1017 

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

1019 if not is_digital: 

1020 resp = response.PoleZeroResponse( 

1021 constant=nfactor*cfactor, 

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

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

1024 else: 

1025 if not deltat: 

1026 log.append(( 

1027 'error', 

1028 'Digital response requires knowledge about sampling ' 

1029 'interval. Response will be unusable.', 

1030 context)) 

1031 

1032 resp = response.DigitalPoleZeroResponse( 

1033 constant=nfactor*cfactor, 

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

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

1036 deltat=deltat or 0.0) 

1037 

1038 if not self.normalization_frequency: 

1039 log.append(( 

1040 'warning', 

1041 'Cannot check pole-zero normalization factor: ' 

1042 'No normalization frequency given.', 

1043 context)) 

1044 

1045 else: 

1046 if is_digital and not deltat: 

1047 log.append(( 

1048 'warning', 

1049 'Cannot check computed vs reported normalization ' 

1050 'factor without knowing the sampling interval.', 

1051 context)) 

1052 else: 

1053 computed_normalization_factor = nfactor / abs(evaluate1( 

1054 resp, self.normalization_frequency.value)) 

1055 

1056 db = 20.0 * num.log10( 

1057 computed_normalization_factor / nfactor) 

1058 

1059 if abs(db) > 0.17: 

1060 log.append(( 

1061 'warning', 

1062 'Computed and reported normalization factors differ ' 

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

1064 db, 

1065 computed_normalization_factor, 

1066 nfactor), 

1067 context)) 

1068 

1069 return Delivery([resp], log) 

1070 

1071 

1072class ResponseListElement(Object): 

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

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

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

1076 

1077 

1078class Polynomial(BaseFilter): 

1079 ''' 

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

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

1082 stage of acquisition or a complete system. 

1083 ''' 

1084 

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

1086 xmltagname='ApproximationType') 

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

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

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

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

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

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

1093 

1094 def summary(self): 

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

1096 

1097 

1098class Decimation(Object): 

1099 ''' 

1100 Corresponds to SEED blockette 57. 

1101 ''' 

1102 

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

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

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

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

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

1108 

1109 def summary(self): 

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

1111 self.factor, 

1112 self.input_sample_rate.value, 

1113 self.input_sample_rate.value / self.factor, 

1114 self.delay.value) 

1115 

1116 def get_pyrocko_response(self): 

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

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

1119 else: 

1120 return Delivery([]) 

1121 

1122 

1123class Operator(Object): 

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

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

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

1127 

1128 

1129class Comment(Object): 

1130 ''' 

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

1132 and 59. 

1133 ''' 

1134 

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

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

1137 begin_effective_time = DummyAwareOptionalTimestamp.T( 

1138 optional=True, 

1139 xmltagname='BeginEffectiveTime') 

1140 end_effective_time = DummyAwareOptionalTimestamp.T( 

1141 optional=True, 

1142 xmltagname='EndEffectiveTime') 

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

1144 

1145 

1146class ResponseList(BaseFilter): 

1147 ''' 

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

1149 SEED blockette 55. 

1150 ''' 

1151 

1152 response_list_element_list = List.T( 

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

1154 

1155 def summary(self): 

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

1157 

1158 

1159class Log(Object): 

1160 ''' 

1161 Container for log entries. 

1162 ''' 

1163 

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

1165 

1166 

1167class ResponseStage(Object): 

1168 ''' 

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

1170 to 56. 

1171 ''' 

1172 

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

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

1175 poles_zeros_list = List.T( 

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

1177 coefficients_list = List.T( 

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

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

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

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

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

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

1184 

1185 def summary(self): 

1186 elements = [] 

1187 

1188 for stuff in [ 

1189 self.poles_zeros_list, self.coefficients_list, 

1190 self.response_list, self.fir, self.polynomial, 

1191 self.decimation, self.stage_gain]: 

1192 

1193 if stuff: 

1194 if isinstance(stuff, Object): 

1195 elements.append(stuff.summary()) 

1196 else: 

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

1198 

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

1200 self.number, 

1201 ', '.join(elements), 

1202 sanitize_units(self.input_units.name) 

1203 if self.input_units else '?', 

1204 sanitize_units(self.output_units.name) 

1205 if self.output_units else '?') 

1206 

1207 def get_squirrel_response_stage(self, context): 

1208 from pyrocko.squirrel.model import ResponseStage 

1209 

1210 delivery = Delivery() 

1211 delivery_pr = self.get_pyrocko_response(context) 

1212 log = delivery_pr.log 

1213 delivery_pr.log = [] 

1214 elements = delivery.extend_without_payload(delivery_pr) 

1215 

1216 delivery.payload = [ResponseStage( 

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

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

1219 input_sample_rate=self.input_sample_rate, 

1220 output_sample_rate=self.output_sample_rate, 

1221 elements=elements, 

1222 log=log)] 

1223 

1224 return delivery 

1225 

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

1227 

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

1229 

1230 responses = [] 

1231 log = [] 

1232 if self.stage_gain: 

1233 normalization_frequency = self.stage_gain.frequency or 0.0 

1234 else: 

1235 normalization_frequency = 0.0 

1236 

1237 if not gain_only: 

1238 deltat = None 

1239 delay_responses = [] 

1240 if self.decimation: 

1241 rate = self.decimation.input_sample_rate.value 

1242 if rate > 0.0: 

1243 deltat = 1.0 / rate 

1244 delivery = self.decimation.get_pyrocko_response() 

1245 if delivery.errors: 

1246 return delivery 

1247 

1248 delay_responses = delivery.payload 

1249 log.extend(delivery.log) 

1250 

1251 for pzs in self.poles_zeros_list: 

1252 delivery = pzs.get_pyrocko_response(context, deltat) 

1253 if delivery.errors: 

1254 return delivery 

1255 

1256 pz_resps = delivery.payload 

1257 log.extend(delivery.log) 

1258 responses.extend(pz_resps) 

1259 

1260 # emulate incorrect? evalresp behaviour 

1261 if pzs.normalization_frequency is not None and \ 

1262 pzs.normalization_frequency.value \ 

1263 != normalization_frequency \ 

1264 and normalization_frequency != 0.0: 

1265 

1266 try: 

1267 trial = response.MultiplyResponse(pz_resps) 

1268 anorm = num.abs(evaluate1( 

1269 trial, pzs.normalization_frequency.value)) 

1270 asens = num.abs( 

1271 evaluate1(trial, normalization_frequency)) 

1272 

1273 factor = anorm/asens 

1274 

1275 if abs(factor - 1.0) > 0.01: 

1276 log.append(( 

1277 'warning', 

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

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

1280 'possibly incorrect evalresp behaviour. ' 

1281 'Correction factor: %g' % ( 

1282 pzs.normalization_frequency.value, 

1283 normalization_frequency, 

1284 factor), 

1285 context)) 

1286 

1287 responses.append( 

1288 response.PoleZeroResponse(constant=factor)) 

1289 except response.InvalidResponseError as e: 

1290 log.append(( 

1291 'warning', 

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

1293 context)) 

1294 

1295 if len(self.poles_zeros_list) > 1: 

1296 log.append(( 

1297 'warning', 

1298 'Multiple poles and zeros records in single response ' 

1299 'stage.', 

1300 context)) 

1301 

1302 for cfs in self.coefficients_list + ( 

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

1304 

1305 delivery = cfs.get_pyrocko_response( 

1306 context, deltat, delay_responses, 

1307 normalization_frequency) 

1308 

1309 if delivery.errors: 

1310 return delivery 

1311 

1312 responses.extend(delivery.payload) 

1313 log.extend(delivery.log) 

1314 

1315 if len(self.coefficients_list) > 1: 

1316 log.append(( 

1317 'warning', 

1318 'Multiple filter coefficients lists in single response ' 

1319 'stage.', 

1320 context)) 

1321 

1322 if self.response_list: 

1323 log.append(( 

1324 'warning', 

1325 'Unhandled response element of type: ResponseList', 

1326 context)) 

1327 

1328 if self.polynomial: 

1329 log.append(( 

1330 'warning', 

1331 'Unhandled response element of type: Polynomial', 

1332 context)) 

1333 

1334 if self.stage_gain: 

1335 responses.append( 

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

1337 

1338 return Delivery(responses, log) 

1339 

1340 @property 

1341 def input_units(self): 

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

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

1344 if e is not None: 

1345 return e.input_units 

1346 

1347 return None 

1348 

1349 @property 

1350 def output_units(self): 

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

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

1353 if e is not None: 

1354 return e.output_units 

1355 

1356 return None 

1357 

1358 @property 

1359 def input_sample_rate(self): 

1360 if self.decimation: 

1361 return self.decimation.input_sample_rate.value 

1362 

1363 return None 

1364 

1365 @property 

1366 def output_sample_rate(self): 

1367 if self.decimation: 

1368 return self.decimation.input_sample_rate.value \ 

1369 / self.decimation.factor 

1370 

1371 return None 

1372 

1373 

1374class Response(Object): 

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

1376 instrument_sensitivity = Sensitivity.T(optional=True, 

1377 xmltagname='InstrumentSensitivity') 

1378 instrument_polynomial = Polynomial.T(optional=True, 

1379 xmltagname='InstrumentPolynomial') 

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

1381 

1382 @property 

1383 def output_sample_rate(self): 

1384 if self.stage_list: 

1385 return self.stage_list[-1].output_sample_rate 

1386 else: 

1387 return None 

1388 

1389 def check_sample_rates(self, channel): 

1390 

1391 if self.stage_list: 

1392 sample_rate = None 

1393 

1394 for stage in self.stage_list: 

1395 if stage.decimation: 

1396 input_sample_rate = \ 

1397 stage.decimation.input_sample_rate.value 

1398 

1399 if sample_rate is not None and not same_sample_rate( 

1400 sample_rate, input_sample_rate): 

1401 

1402 logger.warning( 

1403 'Response stage %i has unexpected input sample ' 

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

1405 stage.number, 

1406 input_sample_rate, 

1407 sample_rate)) 

1408 

1409 sample_rate = input_sample_rate / stage.decimation.factor 

1410 

1411 if sample_rate is not None and channel.sample_rate \ 

1412 and not same_sample_rate( 

1413 sample_rate, channel.sample_rate.value): 

1414 

1415 logger.warning( 

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

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

1418 channel.sample_rate.value, 

1419 sample_rate)) 

1420 

1421 def check_units(self): 

1422 

1423 if self.instrument_sensitivity \ 

1424 and self.instrument_sensitivity.input_units: 

1425 

1426 units = sanitize_units( 

1427 self.instrument_sensitivity.input_units.name) 

1428 

1429 if self.stage_list: 

1430 for stage in self.stage_list: 

1431 if units and stage.input_units \ 

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

1433 

1434 logger.warning( 

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

1436 % ( 

1437 stage.number, 

1438 units, 

1439 'output units of stage %i' 

1440 if stage.number == 0 

1441 else 'sensitivity input units', 

1442 units)) 

1443 

1444 if stage.output_units: 

1445 units = sanitize_units(stage.output_units.name) 

1446 else: 

1447 units = None 

1448 

1449 sout_units = self.instrument_sensitivity.output_units 

1450 if self.instrument_sensitivity and sout_units: 

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

1452 sout_units.name): 

1453 logger.warning( 

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

1455 % ( 

1456 stage.number, 

1457 units, 

1458 'sensitivity output units', 

1459 sanitize_units(sout_units.name))) 

1460 

1461 def _sensitivity_checkpoints(self, responses, context): 

1462 delivery = Delivery() 

1463 

1464 if self.instrument_sensitivity: 

1465 sval = self.instrument_sensitivity.value 

1466 sfreq = self.instrument_sensitivity.frequency 

1467 if sval is None: 

1468 delivery.log.append(( 

1469 'warning', 

1470 'No sensitivity value given.', 

1471 context)) 

1472 

1473 elif sval is None: 

1474 delivery.log.append(( 

1475 'warning', 

1476 'Reported sensitivity value is zero.', 

1477 context)) 

1478 

1479 elif sfreq is None: 

1480 delivery.log.append(( 

1481 'warning', 

1482 'Sensitivity frequency not given.', 

1483 context)) 

1484 

1485 else: 

1486 trial = response.MultiplyResponse(responses) 

1487 

1488 delivery.extend( 

1489 check_resp( 

1490 trial, sval, sfreq, 0.1, 

1491 'Instrument sensitivity value inconsistent with ' 

1492 'sensitivity computed from complete response.', 

1493 context)) 

1494 

1495 delivery.payload.append(response.FrequencyResponseCheckpoint( 

1496 frequency=sfreq, value=sval)) 

1497 

1498 return delivery 

1499 

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

1501 from pyrocko.squirrel.model import Response 

1502 

1503 if self.stage_list: 

1504 delivery = Delivery() 

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

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

1507 

1508 checkpoints = [] 

1509 if not delivery.errors: 

1510 all_responses = [] 

1511 for sq_stage in delivery.payload: 

1512 all_responses.extend(sq_stage.elements) 

1513 

1514 checkpoints.extend(delivery.extend_without_payload( 

1515 self._sensitivity_checkpoints(all_responses, context))) 

1516 

1517 sq_stages = delivery.payload 

1518 if sq_stages: 

1519 if sq_stages[0].input_quantity is None \ 

1520 and self.instrument_sensitivity is not None: 

1521 

1522 sq_stages[0].input_quantity = to_quantity( 

1523 self.instrument_sensitivity.input_units, 

1524 context, delivery) 

1525 sq_stages[-1].output_quantity = to_quantity( 

1526 self.instrument_sensitivity.output_units, 

1527 context, delivery) 

1528 

1529 sq_stages = delivery.expect() 

1530 

1531 return Response( 

1532 stages=sq_stages, 

1533 log=delivery.log, 

1534 checkpoints=checkpoints, 

1535 **kwargs) 

1536 

1537 elif self.instrument_sensitivity: 

1538 raise NoResponseInformation( 

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

1540 % context) 

1541 else: 

1542 raise NoResponseInformation( 

1543 'Empty instrument response (%s).' 

1544 % context) 

1545 

1546 def get_pyrocko_response( 

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

1548 

1549 if fake_input_units is not None: 

1550 fake_input_units = sanitize_units(fake_input_units) 

1551 

1552 delivery = Delivery() 

1553 if self.stage_list: 

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

1555 delivery.extend(stage.get_pyrocko_response( 

1556 context, gain_only=not ( 

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

1558 

1559 elif self.instrument_sensitivity: 

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

1561 

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

1563 checkpoints = delivery.extend_without_payload(delivery_cp) 

1564 if delivery.errors: 

1565 return delivery 

1566 

1567 if fake_input_units is not None: 

1568 if not self.instrument_sensitivity or \ 

1569 self.instrument_sensitivity.input_units is None: 

1570 

1571 delivery.errors.append(( 

1572 'NoResponseInformation', 

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

1574 'units: %s' % fake_input_units, 

1575 context)) 

1576 

1577 return delivery 

1578 

1579 input_units = sanitize_units( 

1580 self.instrument_sensitivity.input_units.name) 

1581 

1582 conresp = None 

1583 try: 

1584 conresp = conversion[ 

1585 fake_input_units, input_units] 

1586 

1587 except KeyError: 

1588 delivery.errors.append(( 

1589 'NoResponseInformation', 

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

1591 % (fake_input_units, input_units), 

1592 context)) 

1593 

1594 if conresp is not None: 

1595 delivery.payload.append(conresp) 

1596 for checkpoint in checkpoints: 

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

1598 conresp, checkpoint.frequency)) 

1599 

1600 delivery.payload = [ 

1601 response.MultiplyResponse( 

1602 delivery.payload, 

1603 checkpoints=checkpoints)] 

1604 

1605 return delivery 

1606 

1607 @classmethod 

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

1609 normalization_frequency=1.0): 

1610 ''' 

1611 Convert Pyrocko pole-zero response to StationXML response. 

1612 

1613 :param presponse: Pyrocko pole-zero response 

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

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

1616 response. 

1617 :type input_unit: str 

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

1619 response. 

1620 :type output_unit: str 

1621 :param normalization_frequency: Frequency where the normalization 

1622 factor for the StationXML response should be computed. 

1623 :type normalization_frequency: float 

1624 ''' 

1625 

1626 norm_factor = 1.0/float(abs( 

1627 evaluate1(presponse, normalization_frequency) 

1628 / presponse.constant)) 

1629 

1630 pzs = PolesZeros( 

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

1632 normalization_factor=norm_factor, 

1633 normalization_frequency=Frequency(normalization_frequency), 

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

1635 imaginary=FloatNoUnit(z.imag)) 

1636 for z in presponse.zeros], 

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

1638 imaginary=FloatNoUnit(z.imag)) 

1639 for z in presponse.poles]) 

1640 

1641 pzs.validate() 

1642 

1643 stage = ResponseStage( 

1644 number=1, 

1645 poles_zeros_list=[pzs], 

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

1647 

1648 resp = Response( 

1649 instrument_sensitivity=Sensitivity( 

1650 value=stage.stage_gain.value, 

1651 frequency=normalization_frequency, 

1652 input_units=Units(input_unit), 

1653 output_units=Units(output_unit)), 

1654 

1655 stage_list=[stage]) 

1656 

1657 return resp 

1658 

1659 

1660class BaseNode(Object): 

1661 ''' 

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

1663 ''' 

1664 

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

1666 start_date = DummyAwareOptionalTimestamp.T(optional=True, 

1667 xmlstyle='attribute') 

1668 end_date = DummyAwareOptionalTimestamp.T(optional=True, 

1669 xmlstyle='attribute') 

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

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

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

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

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

1675 

1676 def spans(self, *args): 

1677 if len(args) == 0: 

1678 return True 

1679 elif len(args) == 1: 

1680 return ((self.start_date is None or 

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

1682 (self.end_date is None or 

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

1684 

1685 elif len(args) == 2: 

1686 return ((self.start_date is None or 

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

1688 (self.end_date is None or 

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

1690 

1691 

1692def overlaps(a, b): 

1693 return ( 

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

1695 or a.start_date < b.end_date 

1696 ) and ( 

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

1698 or b.start_date < a.end_date 

1699 ) 

1700 

1701 

1702def check_overlaps(node_type_name, codes, nodes): 

1703 errors = [] 

1704 for ia, a in enumerate(nodes): 

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

1706 if overlaps(a, b): 

1707 errors.append( 

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

1709 ' %s - %s\n %s - %s' % ( 

1710 node_type_name, 

1711 '.'.join(codes), 

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

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

1714 

1715 return errors 

1716 

1717 

1718class Channel(BaseNode): 

1719 ''' 

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

1721 response blockettes. 

1722 ''' 

1723 

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

1725 external_reference_list = List.T( 

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

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

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

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

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

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

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

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

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

1735 sample_rate_ratio = SampleRateRatio.T(optional=True, 

1736 xmltagname='SampleRateRatio') 

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

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

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

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

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

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

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

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

1745 

1746 @property 

1747 def position_values(self): 

1748 lat = self.latitude.value 

1749 lon = self.longitude.value 

1750 elevation = value_or_none(self.elevation) 

1751 depth = value_or_none(self.depth) 

1752 return lat, lon, elevation, depth 

1753 

1754 

1755class Station(BaseNode): 

1756 ''' 

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

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

1759 epoch start and end dates. 

1760 ''' 

1761 

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

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

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

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

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

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

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

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

1770 creation_date = DummyAwareOptionalTimestamp.T( 

1771 optional=True, xmltagname='CreationDate') 

1772 termination_date = DummyAwareOptionalTimestamp.T( 

1773 optional=True, xmltagname='TerminationDate') 

1774 total_number_channels = Counter.T( 

1775 optional=True, xmltagname='TotalNumberChannels') 

1776 selected_number_channels = Counter.T( 

1777 optional=True, xmltagname='SelectedNumberChannels') 

1778 external_reference_list = List.T( 

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

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

1781 

1782 @property 

1783 def position_values(self): 

1784 lat = self.latitude.value 

1785 lon = self.longitude.value 

1786 elevation = value_or_none(self.elevation) 

1787 return lat, lon, elevation 

1788 

1789 

1790class Network(BaseNode): 

1791 ''' 

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

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

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

1795 contain 0 or more Stations. 

1796 ''' 

1797 

1798 total_number_stations = Counter.T(optional=True, 

1799 xmltagname='TotalNumberStations') 

1800 selected_number_stations = Counter.T(optional=True, 

1801 xmltagname='SelectedNumberStations') 

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

1803 

1804 @property 

1805 def station_code_list(self): 

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

1807 

1808 @property 

1809 def sl_code_list(self): 

1810 sls = set() 

1811 for station in self.station_list: 

1812 for channel in station.channel_list: 

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

1814 

1815 return sorted(sls) 

1816 

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

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

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

1820 if sls: 

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

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

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

1824 while ssls: 

1825 lines.append( 

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

1827 

1828 ssls[:n] = [] 

1829 

1830 return '\n'.join(lines) 

1831 

1832 

1833def value_or_none(x): 

1834 if x is not None: 

1835 return x.value 

1836 else: 

1837 return None 

1838 

1839 

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

1841 

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

1843 channels[0].position_values 

1844 

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

1846 info = '\n'.join( 

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

1848 x in channels) 

1849 

1850 mess = 'encountered inconsistencies in channel ' \ 

1851 'lat/lon/elevation/depth ' \ 

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

1853 

1854 if inconsistencies == 'raise': 

1855 raise InconsistentChannelLocations(mess) 

1856 

1857 elif inconsistencies == 'warn': 

1858 logger.warning(mess) 

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

1860 

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

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

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

1864 

1865 groups = {} 

1866 for channel in channels: 

1867 if channel.code not in groups: 

1868 groups[channel.code] = [] 

1869 

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

1871 

1872 pchannels = [] 

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

1874 data = [ 

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

1876 value_or_none(channel.dip)) 

1877 for channel in groups[code]] 

1878 

1879 azimuth, dip = util.consistency_merge( 

1880 data, 

1881 message='channel orientation values differ:', 

1882 error=inconsistencies) 

1883 

1884 pchannels.append( 

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

1886 

1887 return pyrocko.model.Station( 

1888 *nsl, 

1889 lat=mlat, 

1890 lon=mlon, 

1891 elevation=mele, 

1892 depth=mdep, 

1893 channels=pchannels) 

1894 

1895 

1896class FDSNStationXML(Object): 

1897 ''' 

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

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

1900 one or more Station containers. 

1901 ''' 

1902 

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

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

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

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

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

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

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

1910 

1911 xmltagname = 'FDSNStationXML' 

1912 guessable_xmlns = [guts_xmlns] 

1913 

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

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

1916 if self.created is None: 

1917 self.created = time.time() 

1918 

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

1920 time=None, timespan=None, 

1921 inconsistencies='warn', 

1922 sloppy=False): 

1923 

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

1925 

1926 if nslcs is not None: 

1927 nslcs = set(nslcs) 

1928 

1929 if nsls is not None: 

1930 nsls = set(nsls) 

1931 

1932 tt = () 

1933 if time is not None: 

1934 tt = (time,) 

1935 elif timespan is not None: 

1936 tt = timespan 

1937 

1938 ns_have = set() 

1939 pstations = [] 

1940 sensor_to_channels = defaultdict(list) 

1941 for network in self.network_list: 

1942 if not network.spans(*tt): 

1943 continue 

1944 

1945 for station in network.station_list: 

1946 if not station.spans(*tt): 

1947 continue 

1948 

1949 if station.channel_list: 

1950 loc_to_channels = {} 

1951 for channel in station.channel_list: 

1952 if not channel.spans(*tt): 

1953 continue 

1954 

1955 loc = channel.location_code.strip() 

1956 if loc not in loc_to_channels: 

1957 loc_to_channels[loc] = [] 

1958 

1959 loc_to_channels[loc].append(channel) 

1960 

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

1962 channels = loc_to_channels[loc] 

1963 if nslcs is not None: 

1964 channels = [channel for channel in channels 

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

1966 channel.code) in nslcs] 

1967 

1968 if not channels: 

1969 continue 

1970 

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

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

1973 continue 

1974 

1975 for channel in channels: 

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

1977 if not sloppy: 

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

1979 

1980 sensor_to_channels[k].append(channel) 

1981 

1982 else: 

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

1984 if ns in ns_have: 

1985 message = 'Duplicate station ' \ 

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

1987 

1988 if inconsistencies == 'raise': 

1989 raise Inconsistencies(message) 

1990 else: 

1991 logger.warning(message) 

1992 

1993 ns_have.add(ns) 

1994 

1995 pstations.append(pyrocko.model.Station( 

1996 network.code, station.code, '*', 

1997 lat=station.latitude.value, 

1998 lon=station.longitude.value, 

1999 elevation=value_or_none(station.elevation), 

2000 name=station.description or '')) 

2001 

2002 sensor_have = set() 

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

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

2005 if (nsl, bi) in sensor_have: 

2006 message = 'Duplicate station ' \ 

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

2008 

2009 if inconsistencies == 'raise': 

2010 raise Inconsistencies(message) 

2011 else: 

2012 logger.warning(message) 

2013 

2014 sensor_have.add((nsl, bi)) 

2015 

2016 pstations.append( 

2017 pyrocko_station_from_channels( 

2018 nsl, 

2019 channels, 

2020 inconsistencies=inconsistencies)) 

2021 

2022 return pstations 

2023 

2024 @classmethod 

2025 def from_pyrocko_stations( 

2026 cls, pyrocko_stations, add_flat_responses_from=None): 

2027 

2028 ''' 

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

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

2031 

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

2033 instances. 

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

2035 ''' 

2036 network_dict = defaultdict(list) 

2037 

2038 if add_flat_responses_from: 

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

2040 extra = dict( 

2041 response=Response( 

2042 instrument_sensitivity=Sensitivity( 

2043 value=1.0, 

2044 frequency=1.0, 

2045 input_units=Units(name=add_flat_responses_from)))) 

2046 else: 

2047 extra = {} 

2048 

2049 have_offsets = set() 

2050 for s in pyrocko_stations: 

2051 

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

2053 have_offsets.add(s.nsl()) 

2054 

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

2056 channel_list = [] 

2057 for c in s.channels: 

2058 channel_list.append( 

2059 Channel( 

2060 location_code=location, 

2061 code=c.name, 

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

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

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

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

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

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

2068 **extra 

2069 ) 

2070 ) 

2071 

2072 network_dict[network].append( 

2073 Station( 

2074 code=station, 

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

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

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

2078 channel_list=channel_list) 

2079 ) 

2080 

2081 if have_offsets: 

2082 logger.warning( 

2083 'StationXML does not support Cartesian offsets in ' 

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

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

2086 

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

2088 network_list = [] 

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

2090 

2091 network_list.append( 

2092 Network( 

2093 code=k, station_list=station_list, 

2094 total_number_stations=len(station_list))) 

2095 

2096 sxml = FDSNStationXML( 

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

2098 network_list=network_list) 

2099 

2100 sxml.validate() 

2101 return sxml 

2102 

2103 def iter_network_stations( 

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

2105 

2106 tt = () 

2107 if time is not None: 

2108 tt = (time,) 

2109 elif timespan is not None: 

2110 tt = timespan 

2111 

2112 for network in self.network_list: 

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

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

2115 continue 

2116 

2117 for station in network.station_list: 

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

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

2120 continue 

2121 

2122 yield (network, station) 

2123 

2124 def iter_network_station_channels( 

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

2126 time=None, timespan=None): 

2127 

2128 if loc is not None: 

2129 loc = loc.strip() 

2130 

2131 tt = () 

2132 if time is not None: 

2133 tt = (time,) 

2134 elif timespan is not None: 

2135 tt = timespan 

2136 

2137 for network in self.network_list: 

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

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

2140 continue 

2141 

2142 for station in network.station_list: 

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

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

2145 continue 

2146 

2147 if station.channel_list: 

2148 for channel in station.channel_list: 

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

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

2151 (loc is not None and 

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

2153 continue 

2154 

2155 yield (network, station, channel) 

2156 

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

2158 time=None, timespan=None): 

2159 

2160 groups = {} 

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

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

2163 

2164 net = network.code 

2165 sta = station.code 

2166 cha = channel.code 

2167 loc = channel.location_code.strip() 

2168 if len(cha) == 3: 

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

2170 elif len(cha) == 1: 

2171 bic = '' 

2172 else: 

2173 bic = cha 

2174 

2175 if channel.response and \ 

2176 channel.response.instrument_sensitivity and \ 

2177 channel.response.instrument_sensitivity.input_units: 

2178 

2179 unit = sanitize_units( 

2180 channel.response.instrument_sensitivity.input_units.name) 

2181 else: 

2182 unit = None 

2183 

2184 bic = (bic, unit) 

2185 

2186 k = net, sta, loc 

2187 if k not in groups: 

2188 groups[k] = {} 

2189 

2190 if bic not in groups[k]: 

2191 groups[k][bic] = [] 

2192 

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

2194 

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

2196 bad_bics = [] 

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

2198 sample_rates = [] 

2199 for channel in channels: 

2200 sample_rates.append(channel.sample_rate.value) 

2201 

2202 if not same(sample_rates): 

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

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

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

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

2207 

2208 logger.warning(err) 

2209 bad_bics.append(bic) 

2210 

2211 for bic in bad_bics: 

2212 del bic_to_channels[bic] 

2213 

2214 return groups 

2215 

2216 def choose_channels( 

2217 self, 

2218 target_sample_rate=None, 

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

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

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

2222 time=None, 

2223 timespan=None): 

2224 

2225 nslcs = {} 

2226 for nsl, bic_to_channels in self.get_channel_groups( 

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

2228 

2229 useful_bics = [] 

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

2231 rate = channels[0].sample_rate.value 

2232 

2233 if target_sample_rate is not None and \ 

2234 rate < target_sample_rate*0.99999: 

2235 continue 

2236 

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

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

2239 continue 

2240 

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

2242 continue 

2243 

2244 unit = bic[1] 

2245 

2246 prio_unit = len(priority_units) 

2247 try: 

2248 prio_unit = priority_units.index(unit) 

2249 except ValueError: 

2250 pass 

2251 

2252 prio_inst = len(priority_instrument_code) 

2253 prio_band = len(priority_band_code) 

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

2255 try: 

2256 prio_inst = priority_instrument_code.index( 

2257 channels[0].code[1]) 

2258 except ValueError: 

2259 pass 

2260 

2261 try: 

2262 prio_band = priority_band_code.index( 

2263 channels[0].code[0]) 

2264 except ValueError: 

2265 pass 

2266 

2267 if target_sample_rate is None: 

2268 rate = -rate 

2269 

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

2271 prio_inst, bic)) 

2272 

2273 useful_bics.sort() 

2274 

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

2276 channels = sorted( 

2277 bic_to_channels[bic], 

2278 key=lambda channel: channel.code) 

2279 

2280 if channels: 

2281 for channel in channels: 

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

2283 

2284 break 

2285 

2286 return nslcs 

2287 

2288 def get_pyrocko_response( 

2289 self, nslc, 

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

2291 

2292 net, sta, loc, cha = nslc 

2293 resps = [] 

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

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

2296 resp = channel.response 

2297 if resp: 

2298 resp.check_sample_rates(channel) 

2299 resp.check_units() 

2300 resps.append(resp.get_pyrocko_response( 

2301 '.'.join(nslc), 

2302 fake_input_units=fake_input_units, 

2303 stages=stages).expect_one()) 

2304 

2305 if not resps: 

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

2307 elif len(resps) > 1: 

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

2309 

2310 return resps[0] 

2311 

2312 @property 

2313 def n_code_list(self): 

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

2315 

2316 @property 

2317 def ns_code_list(self): 

2318 nss = set() 

2319 for network in self.network_list: 

2320 for station in network.station_list: 

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

2322 

2323 return sorted(nss) 

2324 

2325 @property 

2326 def nsl_code_list(self): 

2327 nsls = set() 

2328 for network in self.network_list: 

2329 for station in network.station_list: 

2330 for channel in station.channel_list: 

2331 nsls.add( 

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

2333 

2334 return sorted(nsls) 

2335 

2336 @property 

2337 def nslc_code_list(self): 

2338 nslcs = set() 

2339 for network in self.network_list: 

2340 for station in network.station_list: 

2341 for channel in station.channel_list: 

2342 nslcs.add( 

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

2344 channel.code)) 

2345 

2346 return sorted(nslcs) 

2347 

2348 def summary(self): 

2349 lst = [ 

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

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

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

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

2354 ] 

2355 return '\n'.join(lst) 

2356 

2357 def summary_stages(self): 

2358 data = [] 

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

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

2361 channel.code) 

2362 

2363 stages = [] 

2364 in_units = '?' 

2365 out_units = '?' 

2366 if channel.response: 

2367 sens = channel.response.instrument_sensitivity 

2368 if sens: 

2369 in_units = sanitize_units(sens.input_units.name) 

2370 out_units = sanitize_units(sens.output_units.name) 

2371 

2372 for stage in channel.response.stage_list: 

2373 stages.append(stage.summary()) 

2374 

2375 data.append( 

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

2377 in_units, out_units, stages)) 

2378 

2379 data.sort() 

2380 

2381 lst = [] 

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

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

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

2385 for stage in stages: 

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

2387 

2388 return '\n'.join(lst) 

2389 

2390 def _check_overlaps(self): 

2391 by_nslc = {} 

2392 for network in self.network_list: 

2393 for station in network.station_list: 

2394 for channel in station.channel_list: 

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

2396 channel.code) 

2397 if nslc not in by_nslc: 

2398 by_nslc[nslc] = [] 

2399 

2400 by_nslc[nslc].append(channel) 

2401 

2402 errors = [] 

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

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

2405 

2406 return errors 

2407 

2408 def check(self): 

2409 errors = [] 

2410 for meth in [self._check_overlaps]: 

2411 errors.extend(meth()) 

2412 

2413 if errors: 

2414 raise Inconsistencies( 

2415 'Inconsistencies found in StationXML:\n ' 

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

2417 

2418 

2419def load_channel_table(stream): 

2420 

2421 networks = {} 

2422 stations = {} 

2423 

2424 for line in stream: 

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

2426 if line.startswith('#'): 

2427 continue 

2428 

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

2430 

2431 if len(t) != 17: 

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

2433 continue 

2434 

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

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

2437 

2438 try: 

2439 scale = float(scale) 

2440 except ValueError: 

2441 scale = None 

2442 

2443 try: 

2444 scale_freq = float(scale_freq) 

2445 except ValueError: 

2446 scale_freq = None 

2447 

2448 try: 

2449 depth = float(dep) 

2450 except ValueError: 

2451 depth = 0.0 

2452 

2453 try: 

2454 azi = float(azi) 

2455 dip = float(dip) 

2456 except ValueError: 

2457 azi = None 

2458 dip = None 

2459 

2460 try: 

2461 if net not in networks: 

2462 network = Network(code=net) 

2463 else: 

2464 network = networks[net] 

2465 

2466 if (net, sta) not in stations: 

2467 station = Station( 

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

2469 

2470 station.regularize() 

2471 else: 

2472 station = stations[net, sta] 

2473 

2474 if scale: 

2475 resp = Response( 

2476 instrument_sensitivity=Sensitivity( 

2477 value=scale, 

2478 frequency=scale_freq, 

2479 input_units=scale_units)) 

2480 else: 

2481 resp = None 

2482 

2483 channel = Channel( 

2484 code=cha, 

2485 location_code=loc.strip(), 

2486 latitude=lat, 

2487 longitude=lon, 

2488 elevation=ele, 

2489 depth=depth, 

2490 azimuth=azi, 

2491 dip=dip, 

2492 sensor=Equipment(description=sens), 

2493 response=resp, 

2494 sample_rate=sample_rate, 

2495 start_date=start_date, 

2496 end_date=end_date or None) 

2497 

2498 channel.regularize() 

2499 

2500 except ValidationError: 

2501 raise InvalidRecord(line) 

2502 

2503 if net not in networks: 

2504 networks[net] = network 

2505 

2506 if (net, sta) not in stations: 

2507 stations[net, sta] = station 

2508 network.station_list.append(station) 

2509 

2510 station.channel_list.append(channel) 

2511 

2512 return FDSNStationXML( 

2513 source='created from table input', 

2514 created=time.time(), 

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

2516 

2517 

2518def primitive_merge(sxs): 

2519 networks = [] 

2520 for sx in sxs: 

2521 networks.extend(sx.network_list) 

2522 

2523 return FDSNStationXML( 

2524 source='merged from different sources', 

2525 created=time.time(), 

2526 network_list=copy.deepcopy( 

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

2528 

2529 

2530def split_channels(sx): 

2531 for nslc in sx.nslc_code_list: 

2532 network_list = sx.network_list 

2533 network_list_filtered = [ 

2534 network for network in network_list 

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

2536 

2537 for network in network_list_filtered: 

2538 sx.network_list = [network] 

2539 station_list = network.station_list 

2540 station_list_filtered = [ 

2541 station for station in station_list 

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

2543 

2544 for station in station_list_filtered: 

2545 network.station_list = [station] 

2546 channel_list = station.channel_list 

2547 station.channel_list = [ 

2548 channel for channel in channel_list 

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

2550 

2551 yield nslc, copy.deepcopy(sx) 

2552 

2553 station.channel_list = channel_list 

2554 

2555 network.station_list = station_list 

2556 

2557 sx.network_list = network_list 

2558 

2559 

2560if __name__ == '__main__': 

2561 from optparse import OptionParser 

2562 

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

2564 

2565 usage = \ 

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

2567 '<filename> [options]' 

2568 

2569 description = '''Torture StationXML file.''' 

2570 

2571 parser = OptionParser( 

2572 usage=usage, 

2573 description=description, 

2574 formatter=util.BetterHelpFormatter()) 

2575 

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

2577 

2578 if len(args) != 2: 

2579 parser.print_help() 

2580 sys.exit(1) 

2581 

2582 action, path = args 

2583 

2584 sx = load_xml(filename=path) 

2585 if action == 'check': 

2586 try: 

2587 sx.check() 

2588 except Inconsistencies as e: 

2589 logger.error(e) 

2590 sys.exit(1) 

2591 

2592 elif action == 'stats': 

2593 print(sx.summary()) 

2594 

2595 elif action == 'stages': 

2596 print(sx.summary_stages()) 

2597 

2598 else: 

2599 parser.print_help() 

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