1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5''' 

6Module to read and write GSE2.0, GSE2.1, and IMS1.0 files. 

7''' 

8 

9import sys 

10import re 

11import logging 

12 

13from . import util 

14from .io_common import FileLoadError, FileSaveError 

15from pyrocko.guts import ( 

16 Object, String, StringChoice, Timestamp, Int, Float, List, Bool, Complex, 

17 ValidationError) 

18 

19try: 

20 range = xrange 

21except NameError: 

22 pass 

23 

24logger = logging.getLogger('pyrocko.io.ims') 

25 

26km = 1000. 

27nm_per_s = 1.0e-9 

28 

29g_versions = ('GSE2.0', 'GSE2.1', 'IMS1.0') 

30g_dialects = ('NOR_NDC', 'USA_DMC') 

31 

32 

33class SerializeError(Exception): 

34 ''' 

35 Raised when serialization of an IMS/GSE2 object fails. 

36 ''' 

37 pass 

38 

39 

40class DeserializeError(Exception): 

41 ''' 

42 Raised when deserialization of an IMS/GSE2 object fails. 

43 ''' 

44 

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

46 Exception.__init__(self, *args) 

47 self._line_number = None 

48 self._line = None 

49 self._position = kwargs.get('position', None) 

50 self._format = kwargs.get('format', None) 

51 self._version_dialect = None 

52 

53 def set_context(self, line_number, line, version_dialect): 

54 self._line_number = line_number 

55 self._line = line 

56 self._version_dialect = version_dialect 

57 

58 def __str__(self): 

59 lst = [Exception.__str__(self)] 

60 if self._version_dialect is not None: 

61 lst.append('format version: %s' % self._version_dialect[0]) 

62 lst.append('dialect: %s' % self._version_dialect[1]) 

63 if self._line_number is not None: 

64 lst.append('line number: %i' % self._line_number) 

65 if self._line is not None: 

66 lst.append('line content:\n%s' % (self._line.decode('ascii') or 

67 '*** line is empty ***')) 

68 

69 if self._position is not None: 

70 if self._position[1] is None: 

71 length = max(1, len(self._line or '') - self._position[0]) 

72 else: 

73 length = self._position[1] 

74 

75 lst.append(' ' * self._position[0] + '^' * length) 

76 

77 if self._format is not None: 

78 i = 0 

79 f = [] 

80 j = 1 

81 for element in self._format: 

82 if element.length != 0: 

83 f.append(' ' * (element.position - i)) 

84 if element.length is not None: 

85 f.append(str(j % 10) * element.length) 

86 i = element.position + element.length 

87 else: 

88 f.append(str(j % 10) + '...') 

89 i = element.position + 4 

90 

91 j += 1 

92 

93 lst.append(''.join(f)) 

94 

95 return '\n'.join(lst) 

96 

97 

98def float_or_none(x): 

99 if x.strip(): 

100 return float(x) 

101 else: 

102 return None 

103 

104 

105def int_or_none(x): 

106 if x.strip(): 

107 return int(x) 

108 else: 

109 return None 

110 

111 

112def float_to_string(fmt): 

113 ef = fmt[0] 

114 assert ef in 'ef' 

115 ln, d = map(int, fmt[1:].split('.')) 

116 pfmts = ['%%%i.%i%s' % (ln, dsub, ef) for dsub in range(d, -1, -1)] 

117 blank = b' ' * ln 

118 

119 def func(v): 

120 if v is None: 

121 return blank 

122 

123 for pfmt in pfmts: 

124 s = pfmt % v 

125 if len(s) == ln: 

126 return s.encode('ascii') 

127 

128 raise SerializeError('format="%s", value=%s' % (pfmt, repr(v))) 

129 

130 return func 

131 

132 

133def int_to_string(fmt): 

134 assert fmt[0] == 'i' 

135 pfmt = '%'+fmt[1:]+'i' 

136 ln = int(fmt[1:]) 

137 blank = b' ' * ln 

138 

139 def func(v): 

140 if v is None: 

141 return blank 

142 

143 s = pfmt % v 

144 if len(s) == ln: 

145 return s.encode('ascii') 

146 else: 

147 raise SerializeError('format="%s", value=%s' % (pfmt, repr(v))) 

148 

149 return func 

150 

151 

152def deserialize_string(fmt): 

153 if fmt.endswith('?'): 

154 def func(s): 

155 if s.strip(): 

156 return str(s.rstrip().decode('ascii')) 

157 else: 

158 return None 

159 else: 

160 def func(s): 

161 return str(s.rstrip().decode('ascii')) 

162 

163 return func 

164 

165 

166def serialize_string(fmt): 

167 if fmt.endswith('+'): 

168 more_ok = True 

169 else: 

170 more_ok = False 

171 

172 fmt = fmt.rstrip('?+') 

173 

174 assert fmt[0] == 'a' 

175 ln = int(fmt[1:]) 

176 

177 def func(v): 

178 if v is None: 

179 v = b'' 

180 else: 

181 v = v.encode('ascii') 

182 

183 s = v.ljust(ln) 

184 if more_ok or len(s) == ln: 

185 return s 

186 else: 

187 raise SerializeError('max string length: %i, value="%s"' % ln, v) 

188 

189 return func 

190 

191 

192def rstrip_string(v): 

193 return v.rstrip() 

194 

195 

196def x_fixed(expect): 

197 def func(): 

198 def parse(s): 

199 if s != expect: 

200 raise DeserializeError( 

201 'expected="%s", value="%s"' % (expect, s)) 

202 return s 

203 

204 def string(s): 

205 return expect 

206 

207 return parse, string 

208 

209 func.width = len(expect) 

210 func.help_type = 'Keyword: %s' % expect 

211 return func 

212 

213 

214def x_scaled(fmt, factor): 

215 def func(): 

216 to_string = float_to_string(fmt) 

217 

218 def parse(s): 

219 x = float_or_none(s) 

220 if x is None: 

221 return None 

222 else: 

223 return x * factor 

224 

225 def string(v): 

226 if v is None: 

227 return to_string(None) 

228 else: 

229 return to_string(v/factor) 

230 

231 return parse, string 

232 

233 func.width = int(fmt[1:].split('.')[0]) 

234 func.help_type = 'float' 

235 return func 

236 

237 

238def x_int_angle(): 

239 def string(v): 

240 if v is None: 

241 return b' ' 

242 else: 

243 return ('%3i' % (int(round(v)) % 360)).encode('ascii') 

244 

245 return float_or_none, string 

246 

247 

248x_int_angle.width = 3 

249x_int_angle.help_type = 'int [0, 360]' 

250 

251 

252def x_substitute(value): 

253 def func(): 

254 def parse(s): 

255 assert s == b'' 

256 return value 

257 

258 def string(s): 

259 return b'' 

260 

261 return parse, string 

262 

263 func.width = 0 

264 func.help_type = 'Not present in this file version.' 

265 return func 

266 

267 

268def fillup_zeros(s, fmt): 

269 s = s.rstrip() 

270 if not s: 

271 return s 

272 

273 if fmt == '%Y/%m/%d %H:%M:%S.3FRAC': 

274 return s + '0000/01/01 00:00:00.000'[len(s):] 

275 elif fmt == '%Y/%m/%d %H:%M:%S.2FRAC': 

276 return s + '0000/01/01 00:00:00.00'[len(s):] 

277 

278 return s 

279 

280 

281def x_date_time(fmt='%Y/%m/%d %H:%M:%S.3FRAC'): 

282 def parse(s): 

283 s = str(s.decode('ascii')) 

284 try: 

285 s = fillup_zeros(s, fmt) 

286 return util.str_to_time(s, format=fmt) 

287 

288 except Exception: 

289 # iris sets this dummy end dates and they don't fit into 32bit 

290 # time stamps 

291 if fmt[:2] == '%Y' and s[:4] in ('2599', '2045'): 

292 return None 

293 

294 elif fmt[6:8] == '%Y' and s[6:10] in ('2599', '2045'): 

295 return None 

296 

297 raise DeserializeError('expected date, value="%s"' % s) 

298 

299 def string(s): 

300 return util.time_to_str(s, format=fmt).encode('ascii') 

301 

302 return parse, string 

303 

304 

305x_date_time.width = 23 

306x_date_time.help_type = 'YYYY/MM/DD HH:MM:SS.FFF' 

307 

308 

309def x_date(): 

310 return x_date_time(fmt='%Y/%m/%d') 

311 

312 

313x_date.width = 10 

314x_date.help_type = 'YYYY/MM/DD' 

315 

316 

317def x_date_iris(): 

318 return x_date_time(fmt='%m/%d/%Y') 

319 

320 

321x_date_iris.width = 10 

322x_date_iris.help_type = 'MM/DD/YYYY' 

323 

324 

325def x_date_time_no_seconds(): 

326 return x_date_time(fmt='%Y/%m/%d %H:%M') 

327 

328 

329x_date_time_no_seconds.width = 16 

330x_date_time_no_seconds.help_type = 'YYYY/MM/DD HH:MM' 

331 

332 

333def x_date_time_2frac(): 

334 return x_date_time(fmt='%Y/%m/%d %H:%M:%S.2FRAC') 

335 

336 

337x_date_time_2frac.width = 22 

338x_date_time_2frac.help_type = 'YYYY/MM/DD HH:MM:SS.FF' 

339 

340 

341def x_yesno(): 

342 def parse(s): 

343 if s == b'y': 

344 return True 

345 elif s == b'n': 

346 return False 

347 else: 

348 raise DeserializeError('"y" on "n" expected') 

349 

350 def string(b): 

351 return [b'n', b'y'][int(b)] 

352 

353 return parse, string 

354 

355 

356x_yesno.width = 1 

357x_yesno.help_type = 'yes/no' 

358 

359 

360def optional(x_func): 

361 

362 def func(): 

363 parse, string = x_func() 

364 

365 def parse_optional(s): 

366 if s.strip(): 

367 return parse(s) 

368 else: 

369 return None 

370 

371 def string_optional(s): 

372 if s is None: 

373 return b' ' * x_func.width 

374 else: 

375 return string(s) 

376 

377 return parse_optional, string_optional 

378 

379 func.width = x_func.width 

380 func.help_type = 'optional %s' % x_func.help_type 

381 

382 return func 

383 

384 

385class E(object): 

386 def __init__(self, begin, end, fmt, dummy=False): 

387 self.advance = 1 

388 if dummy: 

389 self.advance = 0 

390 

391 self.position = begin - 1 

392 if end is not None: 

393 self.length = end - begin + 1 

394 else: 

395 self.length = None 

396 

397 self.end = end 

398 

399 if isinstance(fmt, str): 

400 t = fmt[0] 

401 if t in 'ef': 

402 self.parse = float_or_none 

403 self.string = float_to_string(fmt) 

404 ln = int(fmt[1:].split('.')[0]) 

405 self.help_type = 'float' 

406 elif t == 'a': 

407 self.parse = deserialize_string(fmt) 

408 self.string = serialize_string(fmt) 

409 ln = int(fmt[1:].rstrip('+?')) 

410 self.help_type = 'string' 

411 elif t == 'i': 

412 self.parse = int_or_none 

413 self.string = int_to_string(fmt) 

414 ln = int(fmt[1:]) 

415 self.help_type = 'integer' 

416 else: 

417 assert False, 'invalid format: %s' % t 

418 

419 assert self.length is None or ln == self.length, \ 

420 'inconsistent length for pos=%i, fmt=%s' \ 

421 % (self.position, fmt) 

422 

423 else: 

424 self.parse, self.string = fmt() 

425 self.help_type = fmt.help_type 

426 

427 

428def end_section(line, extra=None): 

429 if line is None: 

430 return True 

431 

432 ul = line.upper() 

433 return ul.startswith(b'DATA_TYPE') or ul.startswith(b'STOP') or \ 

434 (extra is not None and ul.startswith(extra)) 

435 

436 

437class Section(Object): 

438 ''' 

439 Base class for top level sections in IMS/GSE2 files. 

440 

441 Sections as understood by this implementation typically correspond to a 

442 DATA_TYPE section in IMS/GSE2 but for some types a finer granularity has 

443 been chosen. 

444 ''' 

445 

446 handlers = {} # filled after section have been defined below 

447 

448 @classmethod 

449 def read(cls, reader): 

450 datatype = DataType.read(reader) 

451 reader.pushback() 

452 return Section.handlers[ 

453 datatype.type.upper().encode('ascii')].read(reader) 

454 

455 def write_datatype(self, writer): 

456 datatype = DataType( 

457 type=self.keyword.decode('ascii'), 

458 format=writer.version_dialect[0]) 

459 datatype.write(writer) 

460 

461 @classmethod 

462 def read_table(cls, reader, expected_header, block_cls, end=end_section): 

463 

464 header = reader.readline() 

465 if not header.upper().startswith(expected_header.upper()): 

466 raise DeserializeError( 

467 'invalid table header line, expected:\n' 

468 '%s\nfound: %s ' % (expected_header, header)) 

469 

470 while True: 

471 line = reader.readline() 

472 reader.pushback() 

473 if end(line): 

474 break 

475 

476 yield block_cls.read(reader) 

477 

478 def write_table(self, writer, header, blocks): 

479 writer.writeline(header) 

480 for block in blocks: 

481 block.write(writer) 

482 

483 

484def get_versioned(x, version_dialect): 

485 if isinstance(x, dict): 

486 for v in (tuple(version_dialect), version_dialect[0], None): 

487 if v in x: 

488 return x[v] 

489 else: 

490 return x 

491 

492 

493class Block(Object): 

494 ''' 

495 Base class for IMS/GSE2 data blocks / lines. 

496 

497 Blocks as understood by this implementation usually correspond to 

498 individual logical lines in the IMS/GSE2 file. 

499 ''' 

500 

501 def values(self): 

502 return list(self.T.ivals(self)) 

503 

504 @classmethod 

505 def format(cls, version_dialect): 

506 return get_versioned(cls._format, version_dialect) 

507 

508 def serialize(self, version_dialect): 

509 ivalue = 0 

510 out = [] 

511 values = self.values() 

512 for element in self.format(version_dialect): 

513 if element.length != 0: 

514 out.append((element.position, element.string(values[ivalue]))) 

515 ivalue += element.advance 

516 

517 out.sort() 

518 

519 i = 0 

520 slist = [] 

521 for (position, s) in out: 

522 slist.append(b' ' * (position - i)) 

523 slist.append(s) 

524 i = position + len(s) 

525 

526 return b''.join(slist) 

527 

528 @classmethod 

529 def deserialize_values(cls, line, version_dialect): 

530 values = [] 

531 for element in cls.format(version_dialect): 

532 try: 

533 val = element.parse( 

534 line[element.position:element.end]) 

535 

536 if element.advance != 0: 

537 values.append(val) 

538 except Exception: 

539 raise DeserializeError( 

540 'Cannot parse %s' % ( 

541 element.help_type), 

542 position=(element.position, element.length), 

543 version=version_dialect[0], 

544 dialect=version_dialect[1], 

545 format=cls.format(version_dialect)) 

546 

547 return values 

548 

549 @classmethod 

550 def validated(cls, *args, **kwargs): 

551 obj = cls(*args, **kwargs) 

552 try: 

553 obj.validate() 

554 except ValidationError as e: 

555 raise DeserializeError(str(e)) 

556 

557 return obj 

558 

559 @classmethod 

560 def regularized(cls, *args, **kwargs): 

561 obj = cls(*args, **kwargs) 

562 try: 

563 obj.regularize() 

564 except ValidationError as e: 

565 raise DeserializeError(str(e)) 

566 

567 return obj 

568 

569 @classmethod 

570 def deserialize(cls, line, version_dialect): 

571 values = cls.deserialize_values(line, version_dialect) 

572 return cls.validated(**dict(zip(cls.T.propnames, values))) 

573 

574 @classmethod 

575 def read(cls, reader): 

576 line = reader.readline() 

577 return cls.deserialize(line, reader.version_dialect) 

578 

579 def write(self, writer): 

580 s = self.serialize(writer.version_dialect) 

581 writer.writeline(s) 

582 

583 

584class FreeFormatLine(Block): 

585 ''' 

586 Base class for IMS/GSE2 free format lines. 

587 ''' 

588 

589 @classmethod 

590 def deserialize_values(cls, line, version_dialect): 

591 format = cls.format(version_dialect) 

592 values = line.split(None, len(format)-1) 

593 

594 values_weeded = [] 

595 for x, v in zip(format, values): 

596 if isinstance(x, bytes): 

597 if v.upper() != x: 

598 raise DeserializeError( 

599 'expected keyword: %s, found %s' % (x, v.upper())) 

600 

601 else: 

602 if isinstance(x, tuple): 

603 x, (parse, _) = x 

604 v = parse(v) 

605 

606 values_weeded.append((x, v)) 

607 

608 values_weeded.sort() 

609 return [str(xv[1].decode('ascii')) for xv in values_weeded] 

610 

611 @classmethod 

612 def deserialize(cls, line, version_dialect): 

613 values = cls.deserialize_values(line, version_dialect) 

614 propnames = cls.T.propnames 

615 stuff = dict(zip(propnames, values)) 

616 return cls.regularized(**stuff) 

617 

618 def serialize(self, version_dialect): 

619 names = self.T.propnames 

620 props = self.T.properties 

621 out = [] 

622 for x in self.format(version_dialect): 

623 if isinstance(x, bytes): 

624 out.append(x.decode('ascii')) 

625 else: 

626 if isinstance(x, tuple): 

627 x, (_, string) = x 

628 v = string(getattr(self, names[x-1])) 

629 else: 

630 v = getattr(self, names[x-1]) 

631 

632 if v is None: 

633 break 

634 

635 out.append(props[x-1].to_save(v)) 

636 

637 return ' '.join(out).encode('ascii') 

638 

639 

640class DataType(Block): 

641 ''' 

642 Representation of a DATA_TYPE line. 

643 ''' 

644 

645 type = String.T() 

646 subtype = String.T(optional=True) 

647 format = String.T() 

648 subformat = String.T(optional=True) 

649 

650 @classmethod 

651 def deserialize(cls, line, version_dialect): 

652 pat = br'DATA_TYPE +([^ :]+)(:([^ :]+))?( +([^ :]+)(:([^ :]+))?)?' 

653 m = re.match(pat, line) 

654 if not m: 

655 raise DeserializeError('invalid DATA_TYPE line') 

656 

657 return cls.validated( 

658 type=str((m.group(1) or b'').decode('ascii')), 

659 subtype=str((m.group(3) or b'').decode('ascii')), 

660 format=str((m.group(5) or b'').decode('ascii')), 

661 subformat=str((m.group(7) or b'').decode('ascii'))) 

662 

663 def serialize(self, version_dialect): 

664 s = self.type 

665 if self.subtype: 

666 s += ':' + self.subtype 

667 

668 f = self.format 

669 if self.subformat: 

670 f += ':' + self.subformat 

671 

672 return ('DATA_TYPE %s %s' % (s, f)).encode('ascii') 

673 

674 @classmethod 

675 def read(cls, reader): 

676 line = reader.readline() 

677 datatype = cls.deserialize(line, reader.version_dialect) 

678 reader.version_dialect[0] = datatype.format 

679 return datatype 

680 

681 def write(self, writer): 

682 s = self.serialize(writer.version_dialect) 

683 writer.version_dialect[0] = self.format 

684 writer.writeline(s) 

685 

686 

687class FTPFile(FreeFormatLine): 

688 ''' 

689 Representation of an FTP_FILE line. 

690 ''' 

691 

692 _format = [b'FTP_FILE', 1, 2, 3, 4] 

693 

694 net_address = String.T() 

695 login_mode = StringChoice.T(choices=('USER', 'GUEST'), ignore_case=True) 

696 directory = String.T() 

697 file = String.T() 

698 

699 

700class WaveformSubformat(StringChoice): 

701 choices = ['INT', 'CM6', 'CM8', 'AU6', 'AU8'] 

702 ignore_case = True 

703 

704 

705class WID2(Block): 

706 ''' 

707 Representation of a WID2 line. 

708 ''' 

709 

710 _format = [ 

711 E(1, 4, x_fixed(b'WID2'), dummy=True), 

712 E(6, 28, x_date_time), 

713 E(30, 34, 'a5'), 

714 E(36, 38, 'a3'), 

715 E(40, 43, 'a4'), 

716 E(45, 47, 'a3'), 

717 E(49, 56, 'i8'), 

718 E(58, 68, 'f11.6'), 

719 E(70, 79, x_scaled('e10.2', nm_per_s)), 

720 E(81, 87, 'f7.3'), 

721 E(89, 94, 'a6?'), 

722 E(96, 100, 'f5.1'), 

723 E(102, 105, 'f4.1') 

724 ] 

725 

726 time = Timestamp.T() 

727 station = String.T(help='station code (5 characters)') 

728 channel = String.T(help='channel code (3 characters)') 

729 location = String.T( 

730 default='', optional=True, 

731 help='location code (aux_id, 4 characters)') 

732 sub_format = WaveformSubformat.T(default='CM6') 

733 nsamples = Int.T(default=0) 

734 sample_rate = Float.T(default=1.0) 

735 calibration_factor = Float.T( 

736 optional=True, 

737 help='system sensitivity (m/count) at reference period ' 

738 '(calibration_period)') 

739 calibration_period = Float.T( 

740 optional=True, 

741 help='calibration reference period [s]') 

742 instrument_type = String.T( 

743 default='', optional=True, help='instrument type (6 characters)') 

744 horizontal_angle = Float.T( 

745 optional=True, 

746 help='horizontal orientation of sensor, clockwise from north [deg]') 

747 vertical_angle = Float.T( 

748 optional=True, 

749 help='vertical orientation of sensor from vertical [deg]') 

750 

751 

752class OUT2(Block): 

753 ''' 

754 Representation of an OUT2 line. 

755 ''' 

756 

757 _format = [ 

758 E(1, 4, x_fixed(b'OUT2'), dummy=True), 

759 E(6, 28, x_date_time), 

760 E(30, 34, 'a5'), 

761 E(36, 38, 'a3'), 

762 E(40, 43, 'a4'), 

763 E(45, 55, 'f11.3') 

764 ] 

765 

766 time = Timestamp.T() 

767 station = String.T(help='station code (5 characters)') 

768 channel = String.T(help='channel code (3 characters)') 

769 location = String.T( 

770 default='', optional=True, 

771 help='location code (aux_id, 4 characters)') 

772 duration = Float.T() 

773 

774 

775class DLY2(Block): 

776 ''' 

777 Representation of a DLY2 line. 

778 ''' 

779 

780 _format = [ 

781 E(1, 4, x_fixed(b'DLY2'), dummy=True), 

782 E(6, 28, x_date_time), 

783 E(30, 34, 'a5'), 

784 E(36, 38, 'a3'), 

785 E(40, 43, 'a4'), 

786 E(45, 55, 'f11.3') 

787 ] 

788 

789 time = Timestamp.T() 

790 station = String.T(help='station code (5 characters)') 

791 channel = String.T(help='channel code (3 characters)') 

792 location = String.T( 

793 default='', optional=True, 

794 help='location code (aux_id, 4 characters)') 

795 queue_duration = Float.T(help='duration of queue [s]') 

796 

797 

798class DAT2(Block): 

799 ''' 

800 Representation of a DAT2 line. 

801 ''' 

802 

803 _format = [ 

804 E(1, 4, x_fixed(b'DAT2'), dummy=True) 

805 ] 

806 

807 raw_data = List.T(String.T()) 

808 

809 @classmethod 

810 def read(cls, reader): 

811 line = reader.readline() 

812 dat2 = cls.deserialize(line, reader.version_dialect) 

813 while True: 

814 line = reader.readline() 

815 if line.upper().startswith(b'CHK2 '): 

816 reader.pushback() 

817 break 

818 else: 

819 if reader._load_data: 

820 dat2.raw_data.append(line.strip()) 

821 

822 return dat2 

823 

824 def write(self, writer): 

825 Block.write(self, writer) 

826 for line in self.raw_data: 

827 writer.writeline(line) 

828 

829 

830class STA2(Block): 

831 ''' 

832 Representation of a STA2 line. 

833 ''' 

834 

835 _format = [ 

836 E(1, 4, x_fixed(b'STA2'), dummy=True), 

837 E(6, 14, 'a9'), 

838 E(16, 24, 'f9.5'), 

839 E(26, 35, 'f10.5'), 

840 E(37, 48, 'a12'), 

841 E(50, 54, x_scaled('f5.3', km)), 

842 E(56, 60, x_scaled('f5.3', km)) 

843 ] 

844 

845 # the standard requires lat, lon, elevation and depth, we define them as 

846 # optional, however 

847 

848 network = String.T(help='network code (9 characters)') 

849 lat = Float.T(optional=True) 

850 lon = Float.T(optional=True) 

851 coordinate_system = String.T(default='WGS-84') 

852 elevation = Float.T(optional=True, help='elevation [m]') 

853 depth = Float.T(optional=True, help='emplacement depth [m]') 

854 

855 

856class CHK2(Block): 

857 ''' 

858 Representation of a CHK2 line. 

859 ''' 

860 

861 _format = [ 

862 E(1, 4, x_fixed(b'CHK2'), dummy=True), 

863 E(6, 13, 'i8') 

864 ] 

865 

866 checksum = Int.T() 

867 

868 

869class EID2(Block): 

870 ''' 

871 Representation of an EID2 line. 

872 ''' 

873 

874 _format = [ 

875 E(1, 4, x_fixed(b'EID2'), dummy=True), 

876 E(6, 13, 'a8'), 

877 E(15, 23, 'a9'), 

878 ] 

879 

880 event_id = String.T(help='event ID (8 characters)') 

881 bulletin_type = String.T(help='bulletin type (9 characters)') 

882 

883 

884class BEA2(Block): 

885 ''' 

886 Representation of a BEA2 line. 

887 ''' 

888 

889 _format = [ 

890 E(1, 4, x_fixed(b'BEA2'), dummy=True), 

891 E(6, 17, 'a12'), 

892 E(19, 23, 'f5.1'), 

893 E(25, 29, 'f5.1')] 

894 

895 beam_id = String.T(help='beam ID (12 characters)') 

896 azimuth = Float.T() 

897 slowness = Float.T() 

898 

899 

900class Network(Block): 

901 ''' 

902 Representation of an entry in a NETWORK section. 

903 ''' 

904 

905 _format = [ 

906 E(1, 9, 'a9'), 

907 E(11, None, 'a64+')] 

908 

909 network = String.T(help='network code (9 characters)') 

910 description = String.T(help='description') 

911 

912 

913class Station(Block): 

914 ''' 

915 Representation of an entry in a STATION section. 

916 ''' 

917 

918 _format = { 

919 None: [ 

920 E(1, 9, 'a9'), 

921 E(11, 15, 'a5'), 

922 E(17, 20, 'a4'), 

923 E(22, 30, 'f9.5'), 

924 E(32, 41, 'f10.5'), 

925 E(43, 54, 'a12'), 

926 E(56, 60, x_scaled('f5.3', km)), 

927 E(62, 71, x_date), 

928 E(73, 82, optional(x_date)) 

929 ], 

930 'GSE2.0': [ 

931 E(0, -1, x_substitute('')), 

932 E(1, 5, 'a5'), 

933 E(7, 10, 'a4'), 

934 E(12, 20, 'f9.5'), 

935 E(22, 31, 'f10.5'), 

936 E(32, 31, x_substitute('WGS-84')), 

937 E(33, 39, x_scaled('f7.3', km)), 

938 E(41, 50, x_date), 

939 E(52, 61, optional(x_date))]} 

940 

941 _format['IMS1.0', 'USA_DMC'] = list(_format[None]) 

942 _format['IMS1.0', 'USA_DMC'][-2:] = [ 

943 E(62, 71, x_date_iris), 

944 E(73, 82, optional(x_date_iris))] 

945 

946 network = String.T(help='network code (9 characters)') 

947 station = String.T(help='station code (5 characters)') 

948 type = String.T( 

949 help='station type (4 characters) ' 

950 '(1C: single component, 3C: three-component, ' 

951 'hfa: high frequency array, lpa: long period array)') 

952 lat = Float.T() 

953 lon = Float.T() 

954 coordinate_system = String.T(default='WGS-84') 

955 elevation = Float.T(help='elevation [m]') 

956 tmin = Timestamp.T() 

957 tmax = Timestamp.T(optional=True) 

958 

959 

960class Channel(Block): 

961 ''' 

962 Representation of an entry in a CHANNEL section. 

963 ''' 

964 

965 _format = { 

966 None: [ 

967 E(1, 9, 'a9'), 

968 E(11, 15, 'a5'), 

969 E(17, 19, 'a3'), 

970 E(21, 24, 'a4'), 

971 E(26, 34, 'f9.5'), 

972 E(36, 45, 'f10.5'), 

973 E(47, 58, 'a12'), 

974 E(60, 64, x_scaled('f5.3', km)), 

975 E(66, 70, x_scaled('f5.3', km)), 

976 E(72, 77, 'f6.1'), 

977 E(79, 83, 'f5.1'), 

978 E(85, 95, 'f11.6'), 

979 E(97, 102, 'a6'), 

980 E(105, 114, x_date), 

981 E(116, 125, optional(x_date))], 

982 'GSE2.0': [ 

983 E(0, -1, x_substitute('')), 

984 E(1, 5, 'a5'), 

985 E(7, 9, 'a3'), 

986 E(11, 14, 'a4'), 

987 E(16, 24, 'f9.5'), 

988 E(26, 35, 'f10.5'), 

989 E(32, 31, x_substitute('WGS-84')), 

990 E(37, 43, x_scaled('f7.3', km)), 

991 E(45, 50, x_scaled('f6.3', km)), 

992 E(52, 57, 'f6.1'), 

993 E(59, 63, 'f5.1'), 

994 E(65, 75, 'f11.6'), 

995 E(77, 83, 'a7'), 

996 E(85, 94, x_date), 

997 E(96, 105, optional(x_date))]} 

998 

999 # norsar plays its own game... 

1000 _format['GSE2.0', 'NOR_NDC'] = list(_format['GSE2.0']) 

1001 _format['GSE2.0', 'NOR_NDC'][-2:] = [ 

1002 E(85, 100, x_date_time_no_seconds), 

1003 E(102, 117, optional(x_date_time_no_seconds))] 

1004 

1005 # also iris plays its own game... 

1006 _format['IMS1.0', 'USA_DMC'] = list(_format[None]) 

1007 _format['IMS1.0', 'USA_DMC'][-2:] = [ 

1008 E(105, 114, x_date_iris), 

1009 E(116, 125, optional(x_date_iris))] 

1010 

1011 network = String.T(help='network code (9 characters)') 

1012 station = String.T(help='station code (5 characters)') 

1013 channel = String.T(help='channel code (3 characters)') 

1014 location = String.T( 

1015 default='', optional=True, 

1016 help='location code (aux_id, 4 characters)') 

1017 lat = Float.T(optional=True) 

1018 lon = Float.T(optional=True) 

1019 coordinate_system = String.T(default='WGS-84') 

1020 elevation = Float.T(optional=True, help='elevation [m]') 

1021 depth = Float.T(optional=True, help='emplacement depth [m]') 

1022 horizontal_angle = Float.T( 

1023 optional=True, 

1024 help='horizontal orientation of sensor, clockwise from north [deg]') 

1025 vertical_angle = Float.T( 

1026 optional=True, 

1027 help='vertical orientation of sensor from vertical [deg]') 

1028 sample_rate = Float.T() 

1029 instrument_type = String.T( 

1030 default='', optional=True, help='instrument type (6 characters)') 

1031 tmin = Timestamp.T() 

1032 tmax = Timestamp.T(optional=True) 

1033 

1034 

1035class BeamGroup(Block): 

1036 ''' 

1037 Representation of an entry in a BEAM group table. 

1038 ''' 

1039 

1040 _format = [ 

1041 E(1, 8, 'a8'), 

1042 E(10, 14, 'a5'), 

1043 E(16, 18, 'a3'), 

1044 E(20, 23, 'a4'), 

1045 E(25, 27, 'i3'), 

1046 E(29, 37, 'f9.5')] 

1047 

1048 beam_group = String.T(help='beam group (8 characters)') 

1049 station = String.T(help='station code (5 characters)') 

1050 channel = String.T(help='channel code (3 characters)') 

1051 location = String.T( 

1052 default='', optional=True, 

1053 help='location code (aux_id, 4 characters)') 

1054 weight = Int.T( 

1055 optional=True, 

1056 help='weight used for this component when the beam was formed') 

1057 delay = Float.T( 

1058 optional=True, 

1059 help='beam delay for this component [s] ' 

1060 '(used for meabs formed by non-plane waves)') 

1061 

1062 

1063class BeamType(StringChoice): 

1064 choices = ['inc', 'coh'] 

1065 ignore_case = True 

1066 

1067 

1068class FilterType(StringChoice): 

1069 choices = ['BP', 'LP', 'HP', 'BR'] 

1070 ignore_case = True 

1071 

1072 

1073class BeamParameters(Block): 

1074 ''' 

1075 Representation of an entry in a BEAM parameters table. 

1076 ''' 

1077 

1078 _format = [ 

1079 E(1, 12, 'a12'), 

1080 E(14, 21, 'a8'), 

1081 E(23, 25, 'a3'), 

1082 E(27, 27, x_yesno), 

1083 E(29, 33, 'f5.1'), 

1084 E(35, 39, 'f5.3'), # standard says f5.1 -999.0 is vertical beam 

1085 E(41, 48, 'a8'), 

1086 E(50, 55, 'f6.2'), 

1087 E(57, 62, 'f6.2'), 

1088 E(64, 65, 'i2'), 

1089 E(67, 67, x_yesno), 

1090 E(69, 70, 'a2'), 

1091 E(72, 81, x_date), 

1092 E(83, 92, optional(x_date))] 

1093 

1094 beam_id = String.T() 

1095 beam_group = String.T() 

1096 type = BeamType.T() 

1097 is_rotated = Bool.T(help='rotation flag') 

1098 azimuth = Float.T( 

1099 help='azimuth used to steer the beam [deg] (clockwise from North)') 

1100 slowness = Float.T( 

1101 help='slowness used to steer the beam [s/deg]') 

1102 phase = String.T( 

1103 help='phase used to set the beam slowness for origin-based beams ' 

1104 '(8 characters)') 

1105 filter_fmin = Float.T( 

1106 help='low frequency cut-off for the beam filter [Hz]') 

1107 filter_fmax = Float.T( 

1108 help='high frequency cut-off for the beam filter [Hz]') 

1109 filter_order = Int.T( 

1110 help='order of the beam filter') 

1111 filter_is_zero_phase = Bool.T( 

1112 help='flag to indicate zero-phase filtering') 

1113 filter_type = FilterType.T( 

1114 help='type of filtering') 

1115 tmin = Timestamp.T( 

1116 help='start date of beam use') 

1117 tmax = Timestamp.T( 

1118 optional=True, 

1119 help='end date of beam use') 

1120 

1121 

1122class OutageReportPeriod(Block): 

1123 ''' 

1124 Representation of a the report period of an OUTAGE section. 

1125 ''' 

1126 

1127 _format = [ 

1128 E(1, 18, x_fixed(b'Report period from'), dummy=True), 

1129 E(20, 42, x_date_time), 

1130 E(44, 45, x_fixed(b'to'), dummy=True), 

1131 E(47, 69, x_date_time)] 

1132 

1133 tmin = Timestamp.T() 

1134 tmax = Timestamp.T() 

1135 

1136 

1137class Outage(Block): 

1138 ''' 

1139 Representation of an entry in the OUTAGE section table. 

1140 ''' 

1141 _format = [ 

1142 E(1, 9, 'a9'), 

1143 E(11, 15, 'a5'), 

1144 E(17, 19, 'a3'), 

1145 E(21, 24, 'a4'), 

1146 E(26, 48, x_date_time), 

1147 E(50, 72, x_date_time), 

1148 E(74, 83, 'f10.3'), 

1149 E(85, None, 'a48+')] 

1150 

1151 network = String.T(help='network code (9 characters)') 

1152 station = String.T(help='station code (5 characters)') 

1153 channel = String.T(help='channel code (3 characters)') 

1154 location = String.T( 

1155 default='', optional=True, 

1156 help='location code (aux_id, 4 characters)') 

1157 tmin = Timestamp.T() 

1158 tmax = Timestamp.T() 

1159 duration = Float.T() 

1160 comment = String.T() 

1161 

1162 

1163class CAL2(Block): 

1164 ''' 

1165 Representation of a CAL2 line. 

1166 ''' 

1167 

1168 _format = { 

1169 None: [ 

1170 E(1, 4, x_fixed(b'CAL2'), dummy=True), 

1171 E(6, 10, 'a5'), 

1172 E(12, 14, 'a3'), 

1173 E(16, 19, 'a4'), 

1174 E(21, 26, 'a6'), 

1175 E(28, 42, x_scaled('e15.8', nm_per_s)), # standard: e15.2 

1176 E(44, 50, 'f7.3'), 

1177 E(52, 62, 'f11.5'), 

1178 E(64, 79, x_date_time_no_seconds), 

1179 E(81, 96, optional(x_date_time_no_seconds))], 

1180 'GSE2.0': [ 

1181 E(1, 4, x_fixed(b'CAL2'), dummy=True), 

1182 E(6, 10, 'a5'), 

1183 E(12, 14, 'a3'), 

1184 E(16, 19, 'a4'), 

1185 E(21, 26, 'a6'), 

1186 E(28, 37, x_scaled('e10.4', nm_per_s)), 

1187 E(39, 45, 'f7.3'), 

1188 E(47, 56, 'f10.5'), 

1189 E(58, 73, x_date_time_no_seconds), 

1190 E(75, 90, optional(x_date_time_no_seconds))]} 

1191 

1192 station = String.T(help='station code (5 characters)') 

1193 channel = String.T(help='channel code (3 characters)') 

1194 location = String.T( 

1195 default='', optional=True, 

1196 help='location code (aux_id, 4 characters)') 

1197 instrument_type = String.T( 

1198 default='', optional=True, help='instrument type (6 characters)') 

1199 calibration_factor = Float.T( 

1200 help='system sensitivity (m/count) at reference period ' 

1201 '(calibration_period)') 

1202 calibration_period = Float.T(help='calibration reference period [s]') 

1203 sample_rate = Float.T(help='system output sample rate [Hz]') 

1204 tmin = Timestamp.T(help='effective start date and time') 

1205 tmax = Timestamp.T(optional=True, help='effective end date and time') 

1206 comments = List.T(String.T(optional=True)) 

1207 

1208 @classmethod 

1209 def read(cls, reader): 

1210 lstart = reader.current_line_number() 

1211 line = reader.readline() 

1212 obj = cls.deserialize(line, reader.version_dialect) 

1213 while True: 

1214 line = reader.readline() 

1215 # make sure all comments are read 

1216 if line is None or not line.startswith(b' '): 

1217 reader.pushback() 

1218 break 

1219 

1220 obj.append_dataline(line, reader.version_dialect) 

1221 

1222 obj.comments.extend(reader.get_comments_after(lstart)) 

1223 return obj 

1224 

1225 def write(self, writer): 

1226 s = self.serialize(writer.version_dialect) 

1227 writer.writeline(s) 

1228 for c in self.comments: 

1229 writer.writeline((' (%s)' % c).encode('ascii')) 

1230 

1231 

1232class Units(StringChoice): 

1233 choices = ['V', 'A', 'C'] 

1234 ignore_case = True 

1235 

1236 

1237class Stage(Block): 

1238 ''' 

1239 Base class for IMS/GSE2 response stages. 

1240 

1241 Available response stages are :py:class:`PAZ2`, :py:class:`FAP2`, 

1242 :py:class:`GEN2`, :py:class:`DIG2`, and :py:class:`FIR2`. 

1243 

1244 ''' 

1245 

1246 stage_number = Int.T(help='stage sequence number') 

1247 

1248 @classmethod 

1249 def read(cls, reader): 

1250 lstart = reader.current_line_number() 

1251 line = reader.readline() 

1252 obj = cls.deserialize(line, reader.version_dialect) 

1253 

1254 while True: 

1255 line = reader.readline() 

1256 if line is None or not line.startswith(b' '): 

1257 reader.pushback() 

1258 break 

1259 

1260 obj.append_dataline(line, reader.version_dialect) 

1261 

1262 obj.comments.extend(reader.get_comments_after(lstart)) 

1263 

1264 return obj 

1265 

1266 def write(self, writer): 

1267 line = self.serialize(writer.version_dialect) 

1268 writer.writeline(line) 

1269 self.write_datalines(writer) 

1270 for c in self.comments: 

1271 writer.writeline((' (%s)' % c).encode('ascii')) 

1272 

1273 def write_datalines(self, writer): 

1274 pass 

1275 

1276 

1277class PAZ2Data(Block): 

1278 ''' 

1279 Representation of the complex numbers in PAZ2 sections. 

1280 ''' 

1281 

1282 _format = [ 

1283 E(2, 16, 'e15.8'), 

1284 E(18, 32, 'e15.8')] 

1285 

1286 real = Float.T() 

1287 imag = Float.T() 

1288 

1289 

1290class PAZ2(Stage): 

1291 ''' 

1292 Representation of a PAZ2 line. 

1293 ''' 

1294 

1295 _format = { 

1296 None: [ 

1297 E(1, 4, x_fixed(b'PAZ2'), dummy=True), 

1298 E(6, 7, 'i2'), 

1299 E(9, 9, 'a1'), 

1300 E(11, 25, 'e15.8'), 

1301 E(27, 30, 'i4'), 

1302 E(32, 39, 'f8.3'), 

1303 E(41, 43, 'i3'), 

1304 E(45, 47, 'i3'), 

1305 E(49, None, 'a25+')], 

1306 ('IMS1.0', 'USA_DMC'): [ 

1307 E(1, 4, x_fixed(b'PAZ2'), dummy=True), 

1308 E(6, 7, 'i2'), 

1309 E(9, 9, 'a1'), 

1310 E(11, 25, 'e15.8'), 

1311 E(27, 30, 'i4'), 

1312 E(32, 39, 'f8.3'), 

1313 E(40, 42, 'i3'), 

1314 E(44, 46, 'i3'), 

1315 E(48, None, 'a25+')]} 

1316 

1317 output_units = Units.T( 

1318 help='output units code (V=volts, A=amps, C=counts)') 

1319 scale_factor = Float.T(help='scale factor [ouput units/input units]') 

1320 decimation = Int.T(optional=True, help='decimation') 

1321 correction = Float.T(optional=True, help='group correction applied [s]') 

1322 npoles = Int.T(help='number of poles') 

1323 nzeros = Int.T(help='number of zeros') 

1324 description = String.T(default='', optional=True, help='description') 

1325 

1326 poles = List.T(Complex.T()) 

1327 zeros = List.T(Complex.T()) 

1328 

1329 comments = List.T(String.T(optional=True)) 

1330 

1331 def append_dataline(self, line, version_dialect): 

1332 d = PAZ2Data.deserialize(line, version_dialect) 

1333 v = complex(d.real, d.imag) 

1334 i = len(self.poles) + len(self.zeros) 

1335 

1336 if i < self.npoles: 

1337 self.poles.append(v) 

1338 elif i < self.npoles + self.nzeros: 

1339 self.zeros.append(v) 

1340 else: 

1341 raise DeserializeError( 

1342 'more poles and zeros than expected') 

1343 

1344 def write_datalines(self, writer): 

1345 for pole in self.poles: 

1346 PAZ2Data(real=pole.real, imag=pole.imag).write(writer) 

1347 for zero in self.zeros: 

1348 PAZ2Data(real=zero.real, imag=zero.imag).write(writer) 

1349 

1350 

1351class FAP2Data(Block): 

1352 ''' 

1353 Representation of the data tuples in FAP2 section. 

1354 ''' 

1355 

1356 _format = [ 

1357 E(2, 11, 'f10.5'), 

1358 E(13, 27, 'e15.8'), 

1359 E(29, 32, 'i4')] 

1360 

1361 frequency = Float.T() 

1362 amplitude = Float.T() 

1363 phase = Float.T() 

1364 

1365 

1366class FAP2(Stage): 

1367 ''' 

1368 Representation of a FAP2 line. 

1369 ''' 

1370 

1371 _format = [ 

1372 E(1, 4, x_fixed(b'FAP2'), dummy=True), 

1373 E(6, 7, 'i2'), 

1374 E(9, 9, 'a1'), 

1375 E(11, 14, 'i4'), 

1376 E(16, 23, 'f8.3'), 

1377 E(25, 27, 'i3'), 

1378 E(29, 53, 'a25')] 

1379 

1380 output_units = Units.T( 

1381 help='output units code (V=volts, A=amps, C=counts)') 

1382 decimation = Int.T(optional=True, help='decimation') 

1383 correction = Float.T(help='group correction applied [s]') 

1384 ntrip = Int.T(help='number of frequency, amplitude, phase triplets') 

1385 description = String.T(default='', optional=True, help='description') 

1386 

1387 frequencies = List.T(Float.T(), help='frequency [Hz]') 

1388 amplitudes = List.T( 

1389 Float.T(), help='amplitude [input untits/output units]') 

1390 phases = List.T(Float.T(), help='phase delay [degrees]') 

1391 

1392 comments = List.T(String.T(optional=True)) 

1393 

1394 def append_dataline(self, line, version_dialect): 

1395 d = FAP2Data.deserialize(line, version_dialect) 

1396 self.frequencies.append(d.frequency) 

1397 self.amplitudes.append(d.amplitude) 

1398 self.phases.append(d.phase) 

1399 

1400 def write_datalines(self, writer): 

1401 for frequency, amplitude, phase in zip( 

1402 self.frequencies, self.amplitudes, self.phases): 

1403 

1404 FAP2Data( 

1405 frequency=frequency, 

1406 amplitude=amplitude, 

1407 phase=phase).write(writer) 

1408 

1409 

1410class GEN2Data(Block): 

1411 ''' 

1412 Representation of a data tuple in GEN2 section. 

1413 ''' 

1414 

1415 _format = [ 

1416 E(2, 12, 'f11.5'), 

1417 E(14, 19, 'f6.3')] 

1418 

1419 corner = Float.T(help='corner frequency [Hz]') 

1420 slope = Float.T(help='slope above corner [dB/decate]') 

1421 

1422 

1423class GEN2(Stage): 

1424 ''' 

1425 Representation of a GEN2 line. 

1426 ''' 

1427 

1428 _format = [ 

1429 E(1, 4, x_fixed(b'GEN2'), dummy=True), 

1430 E(6, 7, 'i2'), 

1431 E(9, 9, 'a1'), 

1432 E(11, 25, x_scaled('e15.8', nm_per_s)), 

1433 E(27, 33, 'f7.3'), 

1434 E(35, 38, 'i4'), 

1435 E(40, 47, 'f8.3'), 

1436 E(49, 51, 'i3'), 

1437 E(53, 77, 'a25')] 

1438 

1439 output_units = Units.T( 

1440 help='output units code (V=volts, A=amps, C=counts)') 

1441 calibration_factor = Float.T( 

1442 help='system sensitivity (m/count) at reference period ' 

1443 '(calibration_period)') 

1444 calibration_period = Float.T(help='calibration reference period [s]') 

1445 decimation = Int.T(optional=True, help='decimation') 

1446 correction = Float.T(help='group correction applied [s]') 

1447 ncorners = Int.T(help='number of corners') 

1448 description = String.T(default='', optional=True, help='description') 

1449 

1450 corners = List.T(Float.T(), help='corner frequencies [Hz]') 

1451 slopes = List.T(Float.T(), help='slopes above corners [dB/decade]') 

1452 

1453 comments = List.T(String.T(optional=True)) 

1454 

1455 def append_dataline(self, line, version_dialect): 

1456 d = GEN2Data.deserialize(line, version_dialect) 

1457 self.corners.append(d.corner) 

1458 self.slopes.append(d.slope) 

1459 

1460 def write_datalines(self, writer): 

1461 for corner, slope in zip(self.corners, self.slopes): 

1462 GEN2Data(corner=corner, slope=slope).write(writer) 

1463 

1464 

1465class DIG2(Stage): 

1466 ''' 

1467 Representation of a DIG2 line. 

1468 ''' 

1469 

1470 _format = [ 

1471 E(1, 4, x_fixed(b'DIG2'), dummy=True), 

1472 E(6, 7, 'i2'), 

1473 E(9, 23, 'e15.8'), 

1474 E(25, 35, 'f11.5'), 

1475 E(37, None, 'a25+')] 

1476 

1477 sensitivity = Float.T(help='sensitivity [counts/input units]') 

1478 sample_rate = Float.T(help='digitizer sample rate [Hz]') 

1479 description = String.T(default='', optional=True, help='description') 

1480 

1481 comments = List.T(String.T(optional=True)) 

1482 

1483 

1484class SymmetryFlag(StringChoice): 

1485 choices = ['A', 'B', 'C'] 

1486 ignore_case = True 

1487 

1488 

1489class FIR2Data(Block): 

1490 ''' 

1491 Representation of a line of coefficients in a FIR2 section. 

1492 ''' 

1493 

1494 _format = [ 

1495 E(2, 16, 'e15.8'), 

1496 E(18, 32, 'e15.8'), 

1497 E(34, 48, 'e15.8'), 

1498 E(50, 64, 'e15.8'), 

1499 E(66, 80, 'e15.8')] 

1500 

1501 factors = List.T(Float.T()) 

1502 

1503 def values(self): 

1504 return self.factors + [None]*(5-len(self.factors)) 

1505 

1506 @classmethod 

1507 def deserialize(cls, line, version_dialect): 

1508 factors = [v for v in cls.deserialize_values(line, version_dialect) 

1509 if v is not None] 

1510 return cls.validated(factors=factors) 

1511 

1512 

1513class FIR2(Stage): 

1514 ''' 

1515 Representation of a FIR2 line. 

1516 ''' 

1517 

1518 _format = [ 

1519 E(1, 4, x_fixed(b'FIR2'), dummy=True), 

1520 E(6, 7, 'i2'), 

1521 E(9, 18, 'e10.2'), 

1522 E(20, 23, 'i4'), 

1523 E(25, 32, 'f8.3'), 

1524 E(34, 34, 'a1'), 

1525 E(36, 39, 'i4'), 

1526 E(41, None, 'a25+')] 

1527 

1528 gain = Float.T(help='filter gain (relative factor, not in dB)') 

1529 decimation = Int.T(optional=True, help='decimation') 

1530 correction = Float.T(help='group correction applied [s]') 

1531 symmetry = SymmetryFlag.T( 

1532 help='symmetry flag (A=asymmetric, B=symmetric (odd), ' 

1533 'C=symmetric (even))') 

1534 nfactors = Int.T(help='number of factors') 

1535 description = String.T(default='', optional=True, help='description') 

1536 

1537 comments = List.T(String.T(optional=True)) 

1538 

1539 factors = List.T(Float.T()) 

1540 

1541 def append_dataline(self, line, version_dialect): 

1542 d = FIR2Data.deserialize(line, version_dialect) 

1543 self.factors.extend(d.factors) 

1544 

1545 def write_datalines(self, writer): 

1546 i = 0 

1547 while i < len(self.factors): 

1548 FIR2Data(factors=self.factors[i:i+5]).write(writer) 

1549 i += 5 

1550 

1551 

1552class Begin(FreeFormatLine): 

1553 ''' 

1554 Representation of a BEGIN line. 

1555 ''' 

1556 

1557 _format = [b'BEGIN', 1] 

1558 version = String.T(optional=True) 

1559 

1560 @classmethod 

1561 def read(cls, reader): 

1562 line = reader.readline() 

1563 obj = cls.deserialize(line, reader.version_dialect) 

1564 reader.version_dialect[0] = obj.version 

1565 return obj 

1566 

1567 def write(self, writer): 

1568 FreeFormatLine.write(self, writer) 

1569 writer.version_dialect[0] = self.version 

1570 

1571 

1572class MessageType(StringChoice): 

1573 choices = ['REQUEST', 'DATA', 'SUBSCRIPTION'] 

1574 ignore_case = True 

1575 

1576 

1577class MsgType(FreeFormatLine): 

1578 _format = [b'MSG_TYPE', 1] 

1579 type = MessageType.T() 

1580 

1581 

1582class MsgID(FreeFormatLine): 

1583 ''' 

1584 Representation of a MSG_ID line. 

1585 ''' 

1586 

1587 _format = [b'MSG_ID', 1, 2] 

1588 msg_id_string = String.T() 

1589 msg_id_source = String.T(optional=True) 

1590 

1591 @classmethod 

1592 def read(cls, reader): 

1593 line = reader.readline() 

1594 obj = cls.deserialize(line, reader.version_dialect) 

1595 if obj.msg_id_source in g_dialects: 

1596 reader.version_dialect[1] = obj.msg_id_source 

1597 

1598 return obj 

1599 

1600 def write(self, writer): 

1601 FreeFormatLine.write(self, writer) 

1602 if self.msg_id_source in g_dialects: 

1603 writer.version_dialect[1] = self.msg_id_source 

1604 

1605 

1606class RefID(FreeFormatLine): 

1607 ''' 

1608 Representation of a REF_ID line. 

1609 ''' 

1610 

1611 _format = { 

1612 None: [b'REF_ID', 1, 2, 'PART', 3, 'OF', 4], 

1613 'GSE2.0': [b'REF_ID', 1]} 

1614 

1615 msg_id_string = String.T() 

1616 msg_id_source = String.T(optional=True) 

1617 sequence_number = Int.T(optional=True) 

1618 total_number = Int.T(optional=True) 

1619 

1620 def serialize(self, version_dialect): 

1621 out = ['REF_ID', self.msg_id_string] 

1622 if self.msg_id_source: 

1623 out.append(self.msg_id_source) 

1624 i = self.sequence_number 

1625 n = self.total_number 

1626 if i is not None and n is not None: 

1627 out.extend(['PART', str(i), 'OF', str(n)]) 

1628 

1629 return ' '.join(out).encode('ascii') 

1630 

1631 

1632class LogSection(Section): 

1633 ''' 

1634 Representation of a DATA_TYPE LOG section. 

1635 ''' 

1636 

1637 keyword = b'LOG' 

1638 lines = List.T(String.T()) 

1639 

1640 @classmethod 

1641 def read(cls, reader): 

1642 DataType.read(reader) 

1643 lines = [] 

1644 while True: 

1645 line = reader.readline() 

1646 if end_section(line): 

1647 reader.pushback() 

1648 break 

1649 else: 

1650 lines.append(str(line.decode('ascii'))) 

1651 

1652 return cls(lines=lines) 

1653 

1654 def write(self, writer): 

1655 self.write_datatype(writer) 

1656 for line in self.lines: 

1657 ul = line.upper() 

1658 if ul.startswith('DATA_TYPE') or ul.startswith('STOP'): 

1659 line = ' ' + line 

1660 

1661 writer.writeline(str.encode(line)) 

1662 

1663 

1664class ErrorLogSection(LogSection): 

1665 ''' 

1666 Representation of a DATA_TYPE ERROR_LOG section. 

1667 ''' 

1668 

1669 keyword = b'ERROR_LOG' 

1670 

1671 

1672class FTPLogSection(Section): 

1673 ''' 

1674 Representation of a DATA_TYPE FTP_LOG section. 

1675 ''' 

1676 

1677 keyword = b'FTP_LOG' 

1678 ftp_file = FTPFile.T() 

1679 

1680 @classmethod 

1681 def read(cls, reader): 

1682 DataType.read(reader) 

1683 ftp_file = FTPFile.read(reader) 

1684 return cls(ftp_file=ftp_file) 

1685 

1686 def write(self, writer): 

1687 self.write_datatype(writer) 

1688 self.ftp_file.write(writer) 

1689 

1690 

1691class WID2Section(Section): 

1692 ''' 

1693 Representation of a WID2/STA2/EID2/BEA2/DAT2/CHK2 group. 

1694 ''' 

1695 

1696 wid2 = WID2.T() 

1697 sta2 = STA2.T(optional=True) 

1698 eid2s = List.T(EID2.T()) 

1699 bea2 = BEA2.T(optional=True) 

1700 dat2 = DAT2.T() 

1701 chk2 = CHK2.T() 

1702 

1703 @classmethod 

1704 def read(cls, reader): 

1705 blocks = dict(eid2s=[]) 

1706 expect = [(b'WID2 ', WID2, 1)] 

1707 

1708 if reader.version_dialect[0] == 'GSE2.0': 

1709 # should not be there in GSE2.0, but BGR puts it there 

1710 expect.append((b'STA2 ', STA2, 0)) 

1711 else: 

1712 expect.append((b'STA2 ', STA2, 1)) 

1713 

1714 expect.extend([ 

1715 (b'EID2 ', EID2, 0), 

1716 (b'BEA2 ', BEA2, 0), 

1717 (b'DAT2', DAT2, 1), 

1718 (b'CHK2 ', CHK2, 1)]) 

1719 

1720 for k, handler, required in expect: 

1721 line = reader.readline() 

1722 reader.pushback() 

1723 

1724 if line is None: 

1725 raise DeserializeError('incomplete waveform section') 

1726 

1727 if line.upper().startswith(k): 

1728 block = handler.read(reader) 

1729 if k == b'EID2 ': 

1730 blocks['eid2s'].append(block) 

1731 else: 

1732 blocks[str(k.lower().rstrip().decode('ascii'))] = block 

1733 else: 

1734 if required: 

1735 raise DeserializeError('expected %s block' % k) 

1736 else: 

1737 continue 

1738 

1739 return cls(**blocks) 

1740 

1741 def write(self, writer): 

1742 self.wid2.write(writer) 

1743 if self.sta2: 

1744 self.sta2.write(writer) 

1745 for eid2 in self.eid2s: 

1746 eid2.write(writer) 

1747 if self.bea2: 

1748 self.bea2.write(writer) 

1749 

1750 self.dat2.write(writer) 

1751 self.chk2.write(writer) 

1752 

1753 def pyrocko_trace(self, checksum_error='raise'): 

1754 from pyrocko import ims_ext, trace 

1755 assert checksum_error in ('raise', 'warn', 'ignore') 

1756 

1757 raw_data = self.dat2.raw_data 

1758 nsamples = self.wid2.nsamples 

1759 deltat = 1.0 / self.wid2.sample_rate 

1760 tmin = self.wid2.time 

1761 if self.sta2: 

1762 net = self.sta2.network 

1763 else: 

1764 net = '' 

1765 sta = self.wid2.station 

1766 loc = self.wid2.location 

1767 cha = self.wid2.channel 

1768 

1769 if raw_data: 

1770 ydata = ims_ext.decode_cm6(b''.join(raw_data), nsamples) 

1771 if checksum_error != 'ignore': 

1772 if ims_ext.checksum(ydata) != self.chk2.checksum: 

1773 mess = 'computed checksum value differs from stored value' 

1774 if checksum_error == 'raise': 

1775 raise DeserializeError(mess) 

1776 elif checksum_error == 'warn': 

1777 logger.warning(mess) 

1778 

1779 tmax = None 

1780 else: 

1781 tmax = tmin + (nsamples - 1) * deltat 

1782 ydata = None 

1783 

1784 return trace.Trace( 

1785 net, sta, loc, cha, tmin=tmin, tmax=tmax, 

1786 deltat=deltat, 

1787 ydata=ydata) 

1788 

1789 @classmethod 

1790 def from_pyrocko_trace(cls, tr, 

1791 lat=None, lon=None, elevation=None, depth=None): 

1792 

1793 from pyrocko import ims_ext 

1794 ydata = tr.get_ydata() 

1795 raw_data = ims_ext.encode_cm6(ydata) 

1796 return cls( 

1797 wid2=WID2( 

1798 nsamples=tr.data_len(), 

1799 sample_rate=1.0 / tr.deltat, 

1800 time=tr.tmin, 

1801 station=tr.station, 

1802 location=tr.location, 

1803 channel=tr.channel), 

1804 sta2=STA2( 

1805 network=tr.network, 

1806 lat=lat, 

1807 lon=lon, 

1808 elevation=elevation, 

1809 depth=depth), 

1810 dat2=DAT2( 

1811 raw_data=[raw_data[i*80:(i+1)*80] 

1812 for i in range((len(raw_data)-1)//80 + 1)]), 

1813 chk2=CHK2( 

1814 checksum=ims_ext.checksum(ydata))) 

1815 

1816 

1817class OUT2Section(Section): 

1818 ''' 

1819 Representation of a OUT2/STA2 group. 

1820 ''' 

1821 

1822 out2 = OUT2.T() 

1823 sta2 = STA2.T() 

1824 

1825 @classmethod 

1826 def read(cls, reader): 

1827 out2 = OUT2.read(reader) 

1828 line = reader.readline() 

1829 reader.pushback() 

1830 if line.startswith(b'STA2'): 

1831 # the spec sais STA2 is mandatory but in practice, it is not 

1832 # always there... 

1833 sta2 = STA2.read(reader) 

1834 else: 

1835 sta2 = None 

1836 

1837 return cls(out2=out2, sta2=sta2) 

1838 

1839 def write(self, writer): 

1840 self.out2.write(writer) 

1841 if self.sta2 is not None: 

1842 self.sta2.write(writer) 

1843 

1844 

1845class DLY2Section(Section): 

1846 ''' 

1847 Representation of a DLY2/STA2 group. 

1848 ''' 

1849 

1850 dly2 = DLY2.T() 

1851 sta2 = STA2.T() 

1852 

1853 @classmethod 

1854 def read(cls, reader): 

1855 dly2 = DLY2.read(reader) 

1856 sta2 = STA2.read(reader) 

1857 return cls(dly2=dly2, sta2=sta2) 

1858 

1859 def write(self, writer): 

1860 self.dly2.write(writer) 

1861 self.sta2.write(writer) 

1862 

1863 

1864class WaveformSection(Section): 

1865 ''' 

1866 Representation of a DATA_TYPE WAVEFORM line. 

1867 

1868 Any subsequent WID2/OUT2/DLY2 groups are handled as indepenent sections, so 

1869 this type just serves as a dummy to read/write the DATA_TYPE WAVEFORM 

1870 header. 

1871 ''' 

1872 

1873 keyword = b'WAVEFORM' 

1874 

1875 datatype = DataType.T() 

1876 

1877 @classmethod 

1878 def read(cls, reader): 

1879 datatype = DataType.read(reader) 

1880 return cls(datatype=datatype) 

1881 

1882 def write(self, writer): 

1883 self.datatype.write(writer) 

1884 

1885 

1886class TableSection(Section): 

1887 ''' 

1888 Base class for table style sections. 

1889 ''' 

1890 

1891 has_data_type_header = True 

1892 

1893 @classmethod 

1894 def read(cls, reader): 

1895 if cls.has_data_type_header: 

1896 DataType.read(reader) 

1897 

1898 ts = cls.table_setup 

1899 

1900 header = get_versioned(ts['header'], reader.version_dialect) 

1901 blocks = list(cls.read_table( 

1902 reader, header, ts['cls'], end=ts.get('end', end_section))) 

1903 return cls(**{ts['attribute']: blocks}) 

1904 

1905 def write(self, writer): 

1906 if self.has_data_type_header: 

1907 self.write_datatype(writer) 

1908 

1909 ts = self.table_setup 

1910 header = get_versioned(ts['header'], writer.version_dialect) 

1911 self.write_table(writer, header, getattr(self, ts['attribute'])) 

1912 

1913 

1914class NetworkSection(TableSection): 

1915 ''' 

1916 Representation of a DATA_TYPE NETWORK section. 

1917 ''' 

1918 

1919 keyword = b'NETWORK' 

1920 table_setup = dict( 

1921 header=b'Net Description', 

1922 attribute='networks', 

1923 cls=Network) 

1924 

1925 networks = List.T(Network.T()) 

1926 

1927 

1928class StationSection(TableSection): 

1929 ''' 

1930 Representation of a DATA_TYPE STATION section. 

1931 ''' 

1932 

1933 keyword = b'STATION' 

1934 table_setup = dict( 

1935 header={ 

1936 None: ( 

1937 b'Net Sta Type Latitude Longitude Coord ' 

1938 b'Sys Elev On Date Off Date'), 

1939 'GSE2.0': ( 

1940 b'Sta Type Latitude Longitude Elev On Date ' 

1941 b'Off Date')}, 

1942 attribute='stations', 

1943 cls=Station) 

1944 

1945 stations = List.T(Station.T()) 

1946 

1947 

1948class ChannelSection(TableSection): 

1949 ''' 

1950 Representation of a DATA_TYPE CHANNEL section. 

1951 ''' 

1952 

1953 keyword = b'CHANNEL' 

1954 table_setup = dict( 

1955 header={ 

1956 None: ( 

1957 b'Net Sta Chan Aux Latitude Longitude Coord Sys' 

1958 b' Elev Depth Hang Vang Sample Rate Inst ' 

1959 b'On Date Off Date'), 

1960 'GSE2.0': ( 

1961 b'Sta Chan Aux Latitude Longitude ' 

1962 b'Elev Depth Hang Vang Sample_Rate Inst ' 

1963 b'On Date Off Date')}, 

1964 attribute='channels', 

1965 cls=Channel) 

1966 

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

1968 

1969 

1970class BeamSection(Section): 

1971 ''' 

1972 Representation of a DATA_TYPE BEAM section. 

1973 ''' 

1974 

1975 keyword = b'BEAM' 

1976 beam_group_header = b'Bgroup Sta Chan Aux Wgt Delay' 

1977 beam_parameters_header = b'BeamID Bgroup Btype R Azim Slow '\ 

1978 b'Phase Flo Fhi O Z F '\ 

1979 b'On Date Off Date' 

1980 group = List.T(BeamGroup.T()) 

1981 parameters = List.T(BeamParameters.T()) 

1982 

1983 @classmethod 

1984 def read(cls, reader): 

1985 DataType.read(reader) 

1986 

1987 def end(line): 

1988 return line.upper().startswith(b'BEAMID') 

1989 

1990 group = list(cls.read_table(reader, cls.beam_group_header, BeamGroup, 

1991 end)) 

1992 

1993 parameters = list(cls.read_table(reader, cls.beam_parameters_header, 

1994 BeamParameters)) 

1995 

1996 return cls(group=group, parameters=parameters) 

1997 

1998 def write(self, writer): 

1999 self.write_datatype(writer) 

2000 self.write_table(writer, self.beam_group_header, self.group) 

2001 writer.writeline(b'') 

2002 self.write_table(writer, self.beam_parameters_header, self.parameters) 

2003 

2004 

2005class CAL2Section(Section): 

2006 ''' 

2007 Representation of a CAL2 + stages group in a response section. 

2008 ''' 

2009 

2010 cal2 = CAL2.T() 

2011 stages = List.T(Stage.T()) 

2012 

2013 @classmethod 

2014 def read(cls, reader): 

2015 cal2 = CAL2.read(reader) 

2016 stages = [] 

2017 handlers = { 

2018 b'PAZ2': PAZ2, 

2019 b'FAP2': FAP2, 

2020 b'GEN2': GEN2, 

2021 b'DIG2': DIG2, 

2022 b'FIR2': FIR2} 

2023 

2024 while True: 

2025 line = reader.readline() 

2026 reader.pushback() 

2027 if end_section(line, b'CAL2'): 

2028 break 

2029 

2030 k = line[:4].upper() 

2031 if k in handlers: 

2032 stages.append(handlers[k].read(reader)) 

2033 else: 

2034 raise DeserializeError('unexpected line') 

2035 

2036 return cls(cal2=cal2, stages=stages) 

2037 

2038 def write(self, writer): 

2039 self.cal2.write(writer) 

2040 for stage in self.stages: 

2041 stage.write(writer) 

2042 

2043 

2044class ResponseSection(Section): 

2045 ''' 

2046 Representation of a DATA_TYPE RESPONSE line. 

2047 

2048 Any subsequent CAL2+stages groups are handled as indepenent sections, so 

2049 this type just serves as a dummy to read/write the DATA_TYPE RESPONSE 

2050 header. 

2051 ''' 

2052 

2053 keyword = b'RESPONSE' 

2054 

2055 datatype = DataType.T() 

2056 

2057 @classmethod 

2058 def read(cls, reader): 

2059 datatype = DataType.read(reader) 

2060 return cls(datatype=datatype) 

2061 

2062 def write(self, writer): 

2063 self.datatype.write(writer) 

2064 

2065 

2066class OutageSection(Section): 

2067 ''' 

2068 Representation of a DATA_TYPE OUTAGE section. 

2069 ''' 

2070 

2071 keyword = b'OUTAGE' 

2072 outages_header = b'NET Sta Chan Aux Start Date Time'\ 

2073 b' End Date Time Duration Comment' 

2074 report_period = OutageReportPeriod.T() 

2075 outages = List.T(Outage.T()) 

2076 

2077 @classmethod 

2078 def read(cls, reader): 

2079 DataType.read(reader) 

2080 report_period = OutageReportPeriod.read(reader) 

2081 outages = [] 

2082 outages = list(cls.read_table(reader, cls.outages_header, 

2083 Outage)) 

2084 

2085 return cls( 

2086 report_period=report_period, 

2087 outages=outages) 

2088 

2089 def write(self, writer): 

2090 self.write_datatype(writer) 

2091 self.report_period.write(writer) 

2092 self.write_table(writer, self.outages_header, self.outages) 

2093 

2094 

2095class BulletinTitle(Block): 

2096 

2097 _format = [ 

2098 E(1, 136, 'a136')] 

2099 

2100 title = String.T() 

2101 

2102 

2103g_event_types = dict( 

2104 uk='unknown', 

2105 ke='known earthquake', 

2106 se='suspected earthquake', 

2107 kr='known rockburst', 

2108 sr='suspected rockburst', 

2109 ki='known induced event', 

2110 si='suspected induced event', 

2111 km='known mine explosion', 

2112 sm='suspected mine explosion', 

2113 kx='known experimental explosion', 

2114 sx='suspected experimental explosion', 

2115 kn='known nuclear explosion', 

2116 sn='suspected nuclear explosion', 

2117 ls='landslide', 

2118 de='??', 

2119 fe='??',) 

2120 

2121 

2122class Origin(Block): 

2123 _format = [ 

2124 E(1, 22, x_date_time_2frac), 

2125 E(23, 23, 'a1?'), 

2126 E(25, 29, 'f5.2'), 

2127 E(31, 35, 'f5.2'), 

2128 E(37, 44, 'f8.4'), 

2129 E(46, 54, 'f9.4'), 

2130 E(55, 55, 'a1?'), 

2131 E(57, 60, x_scaled('f4.1', km)), 

2132 E(62, 66, x_scaled('f5.1', km)), 

2133 E(68, 70, x_int_angle), 

2134 E(72, 76, x_scaled('f5.1', km)), 

2135 E(77, 77, 'a1?'), 

2136 E(79, 82, x_scaled('f4.1', km)), 

2137 E(84, 87, 'i4'), 

2138 E(89, 92, 'i4'), 

2139 E(94, 96, x_int_angle), 

2140 E(98, 103, 'f6.2'), 

2141 E(105, 110, 'f6.2'), 

2142 E(112, 112, 'a1?'), 

2143 E(114, 114, 'a1?'), 

2144 E(116, 117, 'a2?'), 

2145 E(119, 127, 'a9'), 

2146 E(129, 136, 'a8')] 

2147 

2148 time = Timestamp.T( 

2149 help='epicenter date and time') 

2150 

2151 time_fixed = StringChoice.T( 

2152 choices=['f'], 

2153 optional=True, 

2154 help='fixed flag, ``"f"`` if fixed origin time solution, ' 

2155 '``None`` if not') 

2156 

2157 time_error = Float.T( 

2158 optional=True, 

2159 help='origin time error [seconds], ``None`` if fixed origin time') 

2160 

2161 residual = Float.T( 

2162 optional=True, 

2163 help='root mean square of time residuals [seconds]') 

2164 

2165 lat = Float.T( 

2166 help='latitude') 

2167 

2168 lon = Float.T( 

2169 help='longitude') 

2170 

2171 lat_lon_fixed = StringChoice.T( 

2172 choices=['f'], optional=True, 

2173 help='fixed flag, ``"f"`` if fixed epicenter solution, ' 

2174 '``None`` if not') 

2175 

2176 ellipse_semi_major_axis = Float.T( 

2177 optional=True, 

2178 help='semi-major axis of 90% c. i. ellipse or its estimate [m], ' 

2179 '``None`` if fixed') 

2180 

2181 ellipse_semi_minor_axis = Float.T( 

2182 optional=True, 

2183 help='semi-minor axis of 90% c. i. ellipse or its estimate [m], ' 

2184 '``None`` if fixed') 

2185 

2186 ellipse_strike = Float.T( 

2187 optional=True, 

2188 help='strike of 90% c. i. ellipse [0-360], ``None`` if fixed') 

2189 

2190 depth = Float.T( 

2191 help='depth [m]') 

2192 

2193 depth_fixed = StringChoice.T( 

2194 choices=['f', 'd'], optional=True, 

2195 help='fixed flag, ``"f"`` fixed depth station, "d" depth phases, ' 

2196 '``None`` if not fixed depth') 

2197 

2198 depth_error = Float.T( 

2199 optional=True, 

2200 help='depth error [m], 90% c. i., ``None`` if fixed depth') 

2201 

2202 nphases = Int.T( 

2203 optional=True, 

2204 help='number of defining phases') 

2205 

2206 nstations = Int.T( 

2207 optional=True, 

2208 help='number of defining stations') 

2209 

2210 azimuthal_gap = Float.T( 

2211 optional=True, 

2212 help='gap in azimuth coverage [deg]') 

2213 

2214 distance_min = Float.T( 

2215 optional=True, 

2216 help='distance to closest station [deg]') 

2217 

2218 distance_max = Float.T( 

2219 optional=True, 

2220 help='distance to furthest station [deg]') 

2221 

2222 analysis_type = StringChoice.T( 

2223 optional=True, 

2224 choices=['a', 'm', 'g'], 

2225 help='analysis type, ``"a"`` automatic, ``"m"`` manual, ``"g"`` guess') 

2226 

2227 location_method = StringChoice.T( 

2228 optional=True, 

2229 choices=['i', 'p', 'g', 'o'], 

2230 help='location method, ``"i"`` inversion, ``"p"`` pattern, ' 

2231 '``"g"`` ground truth, ``"o"`` other') 

2232 

2233 event_type = StringChoice.T( 

2234 optional=True, 

2235 choices=sorted(g_event_types.keys()), 

2236 help='event type, ' + ', '.join( 

2237 '``"%s"`` %s' % (k, g_event_types[k]) 

2238 for k in sorted(g_event_types.keys()))) 

2239 

2240 author = String.T(help='author of the origin') 

2241 origin_id = String.T(help='origin identification') 

2242 

2243 

2244class OriginSection(TableSection): 

2245 has_data_type_header = False 

2246 

2247 table_setup = dict( 

2248 header={ 

2249 None: ( 

2250 b' Date Time Err RMS Latitude Longitude ' 

2251 b'Smaj Smin Az Depth Err Ndef Nsta Gap mdist Mdist ' 

2252 b'Qual Author OrigID')}, 

2253 attribute='origins', 

2254 end=lambda line: end_section(line, b'EVENT'), 

2255 cls=Origin) 

2256 

2257 origins = List.T(Origin.T()) 

2258 

2259 

2260class EventTitle(Block): 

2261 _format = [ 

2262 E(1, 5, x_fixed(b'Event'), dummy=True), 

2263 E(7, 14, 'a8'), 

2264 E(16, 80, 'a65')] 

2265 

2266 event_id = String.T() 

2267 region = String.T() 

2268 

2269 

2270class EventSection(Section): 

2271 ''' 

2272 Groups Event, Arrival, ... 

2273 ''' 

2274 

2275 event_title = EventTitle.T() 

2276 origin_section = OriginSection.T() 

2277 

2278 @classmethod 

2279 def read(cls, reader): 

2280 event_title = EventTitle.read(reader) 

2281 origin_section = OriginSection.read(reader) 

2282 return cls( 

2283 event_title=event_title, 

2284 origin_section=origin_section) 

2285 

2286 def write(self, writer): 

2287 self.event_title.write(writer) 

2288 self.origin_section.write(writer) 

2289 

2290 

2291class EventsSection(Section): 

2292 ''' 

2293 Representation of a DATA_TYPE EVENT section. 

2294 ''' 

2295 

2296 keyword = b'EVENT' 

2297 

2298 bulletin_title = BulletinTitle.T() 

2299 event_sections = List.T(EventSection.T()) 

2300 

2301 @classmethod 

2302 def read(cls, reader): 

2303 DataType.read(reader) 

2304 bulletin_title = BulletinTitle.read(reader) 

2305 event_sections = [] 

2306 while True: 

2307 line = reader.readline() 

2308 reader.pushback() 

2309 if end_section(line): 

2310 break 

2311 

2312 if line.upper().startswith(b'EVENT'): 

2313 event_sections.append(EventSection.read(reader)) 

2314 

2315 return cls( 

2316 bulletin_title=bulletin_title, 

2317 event_sections=event_sections, 

2318 ) 

2319 

2320 def write(self, writer): 

2321 self.write_datatype(writer) 

2322 self.bulletin_title.write(writer) 

2323 for event_section in self.event_sections: 

2324 event_section.write(writer) 

2325 

2326 

2327class BulletinSection(EventsSection): 

2328 ''' 

2329 Representation of a DATA_TYPE BULLETIN section. 

2330 ''' 

2331 

2332 keyword = b'BULLETIN' 

2333 

2334 

2335for sec in ( 

2336 LogSection, ErrorLogSection, FTPLogSection, WaveformSection, 

2337 NetworkSection, StationSection, ChannelSection, BeamSection, 

2338 ResponseSection, OutageSection, EventsSection, BulletinSection): 

2339 

2340 Section.handlers[sec.keyword] = sec 

2341 

2342del sec 

2343 

2344 

2345class MessageHeader(Section): 

2346 ''' 

2347 Representation of a BEGIN/MSG_TYPE/MSG_ID/REF_ID group. 

2348 ''' 

2349 

2350 version = String.T() 

2351 type = String.T() 

2352 msg_id = MsgID.T(optional=True) 

2353 ref_id = RefID.T(optional=True) 

2354 

2355 @classmethod 

2356 def read(cls, reader): 

2357 handlers = { 

2358 b'BEGIN': Begin, 

2359 b'MSG_TYPE': MsgType, 

2360 b'MSG_ID': MsgID, 

2361 b'REF_ID': RefID} 

2362 

2363 blocks = {} 

2364 while True: 

2365 line = reader.readline() 

2366 reader.pushback() 

2367 ok = False 

2368 for k in handlers: 

2369 if line.upper().startswith(k): 

2370 blocks[k] = handlers[k].read(reader) 

2371 ok = True 

2372 

2373 if not ok: 

2374 break 

2375 

2376 return MessageHeader( 

2377 type=blocks[b'MSG_TYPE'].type, 

2378 version=blocks[b'BEGIN'].version, 

2379 msg_id=blocks.get(b'MSG_ID', None), 

2380 ref_id=blocks.get(b'REF_ID', None)) 

2381 

2382 def write(self, writer): 

2383 Begin(version=self.version).write(writer) 

2384 MsgType(type=self.type).write(writer) 

2385 if self.msg_id is not None: 

2386 self.msg_id.write(writer) 

2387 if self.ref_id is not None: 

2388 self.ref_id.write(writer) 

2389 

2390 

2391def parse_ff_date_time(s): 

2392 toks = s.split() 

2393 if len(toks) == 2: 

2394 sdate, stime = toks 

2395 else: 

2396 sdate, stime = toks[0], '' 

2397 

2398 stime += '00:00:00.000'[len(stime):] 

2399 return util.str_to_time( 

2400 sdate + ' ' + stime, format='%Y/%m/%d %H:%M:%S.3FRAC') 

2401 

2402 

2403def string_ff_date_time(t): 

2404 return util.time_to_str(t, format='%Y/%m/%d %H:%M:%S.3FRAC') 

2405 

2406 

2407class TimeStamp(FreeFormatLine): 

2408 ''' 

2409 Representation of a TIME_STAMP line. 

2410 ''' 

2411 

2412 _format = [b'TIME_STAMP', 1] 

2413 

2414 value = Timestamp.T() 

2415 

2416 @classmethod 

2417 def deserialize(cls, line, version_dialect): 

2418 (s,) = cls.deserialize_values(line, version_dialect) 

2419 return cls(value=parse_ff_date_time(s)) 

2420 

2421 def serialize(self, line, version_dialect): 

2422 return ( 

2423 'TIME_STAMP %s' % string_ff_date_time(self.value)).encode('ascii') 

2424 

2425 

2426class Stop(FreeFormatLine): 

2427 ''' 

2428 Representation of a STOP line. 

2429 ''' 

2430 

2431 _format = [b'STOP'] 

2432 

2433 dummy = String.T(optional=True) 

2434 

2435 

2436class XW01(FreeFormatLine): 

2437 ''' 

2438 Representation of a XW01 line (which is a relict from GSE1). 

2439 ''' 

2440 

2441 _format = [b'XW01'] 

2442 

2443 dummy = String.T(optional=True) 

2444 

2445 

2446re_comment = re.compile(br'^(%(.+)\s*| \((#?)(.+)\)\s*)$') 

2447re_comment_usa_dmc = re.compile(br'^(%(.+)\s*| ?\((#?)(.+)\)\s*)$') 

2448 

2449 

2450class Reader(object): 

2451 def __init__(self, f, load_data=True, version=None, dialect=None): 

2452 self._f = f 

2453 self._load_data = load_data 

2454 self._current_fpos = None 

2455 self._current_lpos = None # "physical" line number 

2456 self._current_line = None 

2457 self._readline_count = 0 

2458 self._pushed_back = False 

2459 self._handlers = { 

2460 b'DATA_TYPE ': Section, 

2461 b'WID2 ': WID2Section, 

2462 b'OUT2 ': OUT2Section, 

2463 b'DLY2 ': DLY2Section, 

2464 b'CAL2 ': CAL2Section, 

2465 b'BEGIN': MessageHeader, 

2466 b'STOP': Stop, 

2467 b'XW01': XW01, # for compatibility with BGR dialect 

2468 b'HANG:': None, # for compatibility with CNDC 

2469 b'VANG:': None, 

2470 } 

2471 self._comment_lines = [] 

2472 self._time_stamps = [] 

2473 self.version_dialect = [version, dialect] # main version, dialect 

2474 self._in_garbage = True 

2475 

2476 def tell(self): 

2477 return self._current_fpos 

2478 

2479 def current_line_number(self): 

2480 return self._current_lpos - int(self._pushed_back) 

2481 

2482 def readline(self): 

2483 if self._pushed_back: 

2484 self._pushed_back = False 

2485 return self._current_line 

2486 

2487 while True: 

2488 self._current_fpos = self._f.tell() 

2489 self._current_lpos = self._readline_count + 1 

2490 ln = self._f.readline() 

2491 self._readline_count += 1 

2492 if not ln: 

2493 self._current_line = None 

2494 return None 

2495 

2496 lines = [ln.rstrip(b'\n\r')] 

2497 while lines[-1].endswith(b'\\'): 

2498 lines[-1] = lines[-1][:-1] 

2499 ln = self._f.readline() 

2500 self._readline_count += 1 

2501 lines.append(ln.rstrip(b'\n\r')) 

2502 

2503 self._current_line = b''.join(lines) 

2504 

2505 if self.version_dialect[1] == 'USA_DMC': 

2506 m_comment = re_comment_usa_dmc.match(self._current_line) 

2507 else: 

2508 m_comment = re_comment.match(self._current_line) 

2509 

2510 if not self._current_line.strip(): 

2511 pass 

2512 

2513 elif m_comment: 

2514 comment_type = None 

2515 if m_comment.group(3) == b'#': 

2516 comment_type = 'ISF' 

2517 elif m_comment.group(4) is not None: 

2518 comment_type = 'IMS' 

2519 

2520 comment = m_comment.group(2) or m_comment.group(4) 

2521 

2522 self._comment_lines.append( 

2523 (self._current_lpos, comment_type, 

2524 str(comment.decode('ascii')))) 

2525 

2526 elif self._current_line[:10].upper() == b'TIME_STAMP': 

2527 self._time_stamps.append( 

2528 TimeStamp.deserialize( 

2529 self._current_line, self.version_dialect)) 

2530 

2531 else: 

2532 return self._current_line 

2533 

2534 def get_comments_after(self, lpos): 

2535 comments = [] 

2536 i = len(self._comment_lines) - 1 

2537 while i >= 0: 

2538 if self._comment_lines[i][0] <= lpos: 

2539 break 

2540 

2541 comments.append(self._comment_lines[i][-1]) 

2542 i -= 1 

2543 

2544 return comments 

2545 

2546 def pushback(self): 

2547 assert not self._pushed_back 

2548 self._pushed_back = True 

2549 

2550 def __iter__(self): 

2551 return self 

2552 

2553 def next(self): 

2554 return self.__next__() 

2555 

2556 def __next__(self): 

2557 try: 

2558 while True: 

2559 line = self.readline() 

2560 if line is None: 

2561 raise StopIteration() 

2562 

2563 ignore = False 

2564 for k in self._handlers: 

2565 if line.upper().startswith(k): 

2566 if self._handlers[k] is None: 

2567 ignore = True 

2568 break 

2569 

2570 self.pushback() 

2571 sec = self._handlers[k].read(self) 

2572 if isinstance(sec, Stop): 

2573 self._in_garbage = True 

2574 else: 

2575 self._in_garbage = False 

2576 return sec 

2577 

2578 if not self._in_garbage and not ignore: 

2579 raise DeserializeError('unexpected line') 

2580 

2581 except DeserializeError as e: 

2582 e.set_context( 

2583 self._current_lpos, 

2584 self._current_line, 

2585 self.version_dialect) 

2586 raise 

2587 

2588 

2589class Writer(object): 

2590 

2591 def __init__(self, f, version='GSE2.1', dialect=None): 

2592 self._f = f 

2593 self.version_dialect = [version, dialect] 

2594 

2595 def write(self, section): 

2596 section.write(self) 

2597 

2598 def writeline(self, line): 

2599 self._f.write(line.rstrip()) 

2600 self._f.write(b'\n') 

2601 

2602 

2603def write_string(sections): 

2604 from io import BytesIO 

2605 f = BytesIO() 

2606 w = Writer(f) 

2607 for section in sections: 

2608 w.write(section) 

2609 

2610 return f.getvalue() 

2611 

2612 

2613def iload_fh(f, **kwargs): 

2614 ''' 

2615 Load IMS/GSE2 records from open file handle. 

2616 ''' 

2617 try: 

2618 r = Reader(f, **kwargs) 

2619 for section in r: 

2620 yield section 

2621 

2622 except DeserializeError as e: 

2623 raise FileLoadError(e) 

2624 

2625 

2626def iload_string(s, **kwargs): 

2627 ''' 

2628 Read IMS/GSE2 sections from bytes string. 

2629 ''' 

2630 

2631 from io import BytesIO 

2632 f = BytesIO(s) 

2633 return iload_fh(f, **kwargs) 

2634 

2635 

2636iload_filename, iload_dirname, iload_glob, iload = util.make_iload_family( 

2637 iload_fh, 'IMS/GSE2', ':py:class:`Section`') 

2638 

2639 

2640def dump_fh(sections, f): 

2641 ''' 

2642 Dump IMS/GSE2 sections to open file handle. 

2643 ''' 

2644 try: 

2645 w = Writer(f) 

2646 for section in sections: 

2647 w.write(section) 

2648 

2649 except SerializeError as e: 

2650 raise FileSaveError(e) 

2651 

2652 

2653def dump_string(sections): 

2654 ''' 

2655 Write IMS/GSE2 sections to string. 

2656 ''' 

2657 

2658 from io import BytesIO 

2659 f = BytesIO() 

2660 dump_fh(sections, f) 

2661 return f.getvalue() 

2662 

2663 

2664if __name__ == '__main__': 

2665 from optparse import OptionParser 

2666 

2667 usage = 'python -m pyrocko.ims <filenames>' 

2668 

2669 util.setup_logging('pyrocko.ims.__main__', 'warning') 

2670 

2671 description = ''' 

2672 Read and print IMS/GSE2 records. 

2673 ''' 

2674 

2675 parser = OptionParser( 

2676 usage=usage, 

2677 description=description, 

2678 formatter=util.BetterHelpFormatter()) 

2679 

2680 parser.add_option( 

2681 '--version', 

2682 dest='version', 

2683 choices=g_versions, 

2684 help='inial guess for version') 

2685 

2686 parser.add_option( 

2687 '--dialect', 

2688 dest='dialect', 

2689 choices=g_dialects, 

2690 help='inial guess for dialect') 

2691 

2692 parser.add_option( 

2693 '--load-data', 

2694 dest='load_data', 

2695 action='store_true', 

2696 help='unpack data samples') 

2697 

2698 parser.add_option( 

2699 '--out-version', 

2700 dest='out_version', 

2701 choices=g_versions, 

2702 help='output format version') 

2703 

2704 parser.add_option( 

2705 '--out-dialect', 

2706 dest='out_dialect', 

2707 choices=g_dialects, 

2708 help='output format dialect') 

2709 

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

2711 

2712 for fn in args: 

2713 with open(fn, 'rb') as f: 

2714 

2715 r = Reader(f, load_data=options.load_data, 

2716 version=options.version, dialect=options.dialect) 

2717 

2718 w = None 

2719 if options.out_version is not None: 

2720 w = Writer( 

2721 sys.stdout, version=options.out_version, 

2722 dialect=options.out_dialect) 

2723 

2724 for sec in r: 

2725 if not w: 

2726 print(sec) 

2727 

2728 else: 

2729 w.write(sec) 

2730 

2731 if isinstance(sec, WID2Section) and options.load_data: 

2732 tr = sec.pyrocko_trace(checksum_error='warn')