1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import os 

7import sys 

8import subprocess 

9import tempfile 

10import calendar 

11import time 

12import re 

13import logging 

14import shutil 

15 

16from pyrocko import ExternalProgramMissing 

17from pyrocko import pile, model, util, response 

18from pyrocko.io import eventdata 

19 

20pjoin = os.path.join 

21logger = logging.getLogger('pyrocko.io.rdseed') 

22 

23 

24def cmp(a, b): 

25 return (a > b) - (a < b) 

26 

27 

28def read_station_header_file(fn): 

29 

30 def m(i, *args): 

31 if len(ltoks) >= i + len(args) \ 

32 and (tuple(ltoks[i:i+len(args)]) == args): 

33 return True 

34 return False 

35 

36 def f(i): 

37 return float(toks[i]) 

38 

39 def s(i): 

40 if len(toks) > i: 

41 return toks[i] 

42 else: 

43 return '' 

44 

45 with open(fn, 'r') as fi: 

46 

47 stations = [] 

48 atsec, station, channel = None, None, None 

49 

50 for line in fi: 

51 toks = line.split() 

52 ltoks = line.lower().split() 

53 if m(2, 'station', 'header'): 

54 atsec = 'station' 

55 station = {'channels': []} 

56 stations.append(station) 

57 continue 

58 

59 if m(2, 'station') and m(5, 'channel'): 

60 atsec = 'channel' 

61 channel = {} 

62 station['channels'].append(channel) 

63 continue 

64 

65 if atsec == 'station': 

66 if m(1, 'station', 'code:'): 

67 station['station'] = s(3) 

68 

69 elif m(1, 'network', 'code:'): 

70 station['network'] = s(3) 

71 

72 elif m(1, 'name:'): 

73 station['name'] = ' '.join(toks[2:]) 

74 

75 if atsec == 'channel': 

76 if m(1, 'channel:'): 

77 channel['channel'] = s(2) 

78 

79 elif m(1, 'location:'): 

80 channel['location'] = s(2) 

81 

82 elif m(1, 'latitude:'): 

83 station['lat'] = f(2) 

84 

85 elif m(1, 'longitude:'): 

86 station['lon'] = f(2) 

87 

88 elif m(1, 'elevation:'): 

89 station['elevation'] = f(2) 

90 

91 elif m(1, 'local', 'depth:'): 

92 channel['depth'] = f(3) 

93 

94 elif m(1, 'azimuth:'): 

95 channel['azimuth'] = f(2) 

96 

97 elif m(1, 'dip:'): 

98 channel['dip'] = f(2) 

99 

100 nsl_stations = {} 

101 for station in stations: 

102 for channel in station['channels']: 

103 def cs(k, default=None): 

104 return channel.get(k, station.get(k, default)) 

105 

106 nsl = station['network'], station['station'], channel['location'] 

107 if nsl not in nsl_stations: 

108 nsl_stations[nsl] = model.Station( 

109 network=station['network'], 

110 station=station['station'], 

111 location=channel['location'], 

112 lat=cs('lat'), 

113 lon=cs('lon'), 

114 elevation=cs('elevation'), 

115 depth=cs('depth', None), 

116 name=station['name']) 

117 

118 nsl_stations[nsl].add_channel(model.Channel( 

119 channel['channel'], 

120 azimuth=channel['azimuth'], 

121 dip=channel['dip'])) 

122 

123 return list(nsl_stations.values()) 

124 

125 

126def cmp_version(a, b): 

127 ai = [int(x) for x in a.split('.')] 

128 bi = [int(x) for x in b.split('.')] 

129 return cmp(ai, bi) 

130 

131 

132def dumb_parser(data): 

133 

134 (in_ws, in_kw, in_str) = (1, 2, 3) 

135 

136 state = in_ws 

137 

138 rows = [] 

139 cols = [] 

140 accu = '' 

141 for c in data: 

142 if state == in_ws: 

143 if c == '"': 

144 new_state = in_str 

145 

146 elif c not in (' ', '\t', '\n', '\r'): 

147 new_state = in_kw 

148 

149 if state == in_kw: 

150 if c in (' ', '\t', '\n', '\r'): 

151 cols.append(accu) 

152 accu = '' 

153 if c in ('\n', '\r'): 

154 rows.append(cols) 

155 cols = [] 

156 new_state = in_ws 

157 

158 if state == in_str: 

159 if c == '"': 

160 accu += c 

161 cols.append(accu[1:-1]) 

162 accu = '' 

163 if c in ('\n', '\r'): 

164 rows.append(cols) 

165 cols = [] 

166 new_state = in_ws 

167 

168 state = new_state 

169 

170 if state in (in_kw, in_str): 

171 accu += c 

172 

173 if len(cols) != 0: 

174 rows.append(cols) 

175 

176 return rows 

177 

178 

179class Programs(object): 

180 rdseed = 'rdseed' 

181 avail = None 

182 

183 @staticmethod 

184 def check(): 

185 if Programs.avail is not None: 

186 return Programs.avail 

187 

188 else: 

189 try: 

190 rdseed_proc = subprocess.Popen( 

191 [Programs.rdseed], 

192 stdin=subprocess.PIPE, 

193 stdout=subprocess.PIPE, 

194 stderr=subprocess.PIPE) 

195 

196 (out, err) = rdseed_proc.communicate() 

197 

198 except OSError as e: 

199 if e.errno == 2: 

200 reason = "Could not find executable: '%s'." \ 

201 % Programs.rdseed 

202 else: 

203 reason = str(e) 

204 

205 logging.debug('Failed to run rdseed program. %s' % reason) 

206 Programs.avail = False 

207 return Programs.avail 

208 

209 ms = [re.search( 

210 r'Release (\d+(\.\d+(\.\d+)?)?)', s.decode()) 

211 for s in (err, out)] 

212 ms = list(filter(bool, ms)) 

213 if not ms: 

214 logger.error('Cannot determine rdseed version number.') 

215 else: 

216 version = ms[0].group(1) 

217 if cmp_version('4.7.5', version) == 1 \ 

218 or cmp_version(version, '5.1') == 1: 

219 

220 logger.warning( 

221 'Module pyrocko.rdseed has not been tested with ' 

222 'version %s of rdseed.' % version) 

223 

224 Programs.avail = True 

225 return Programs.avail 

226 

227 

228class SeedVolumeNotFound(Exception): 

229 pass 

230 

231 

232class SeedVolumeAccess(eventdata.EventDataAccess): 

233 

234 def __init__(self, seedvolume, datapile=None): 

235 

236 ''' 

237 Create new SEED Volume access object. 

238 

239 :param seedvolume: filename of seed volume 

240 :param datapile: if not ``None``, this should be a 

241 :py:class:`pile.Pile` object with data traces which are then used 

242 instead of the data provided by the SEED volume. 

243 (This is useful for dataless SEED volumes.) 

244 ''' 

245 

246 eventdata.EventDataAccess.__init__(self, datapile=datapile) 

247 self.tempdir = None 

248 Programs.check() 

249 

250 self.tempdir = None 

251 self.seedvolume = seedvolume 

252 if not os.path.isfile(self.seedvolume): 

253 raise SeedVolumeNotFound() 

254 

255 self.tempdir = tempfile.mkdtemp('', 'SeedVolumeAccess-') 

256 self.station_headers_file = os.path.join( 

257 self.tempdir, 'station_header_infos') 

258 self._unpack() 

259 self.shutil = shutil 

260 

261 def __del__(self): 

262 if self.tempdir: 

263 self.shutil.rmtree(self.tempdir) 

264 

265 def get_pile(self): 

266 if self._pile is None: 

267 fns = util.select_files([self.tempdir], include=r'\.SAC$') 

268 self._pile = pile.Pile() 

269 self._pile.load_files(fns, fileformat='sac') 

270 

271 return self._pile 

272 

273 def get_resp_file(self, tr): 

274 respfile = pjoin(self.tempdir, 'RESP.%s.%s.%s.%s' % tr.nslc_id) 

275 if not os.path.exists(respfile): 

276 raise eventdata.NoRestitution( 

277 'no response information available for trace %s.%s.%s.%s' 

278 % tr.nslc_id) 

279 

280 return respfile 

281 

282 def get_stationxml(self): 

283 stations = self.get_pyrocko_stations() 

284 respfiles = [] 

285 for station in stations: 

286 for channel in station.get_channels(): 

287 nslc = station.nsl() + (channel.name,) 

288 respfile = pjoin(self.tempdir, 'RESP.%s.%s.%s.%s' % nslc) 

289 respfiles.append(respfile) 

290 

291 from pyrocko.fdsn import resp 

292 sxml = resp.make_stationxml(stations, resp.iload(respfiles)) 

293 return sxml 

294 

295 def get_restitution(self, tr, allowed_methods): 

296 if 'evalresp' in allowed_methods: 

297 respfile = pjoin(self.tempdir, 'RESP.%s.%s.%s.%s' % tr.nslc_id) 

298 if not os.path.exists(respfile): 

299 raise eventdata.NoRestitution( 

300 'no response information available for trace %s.%s.%s.%s' 

301 % tr.nslc_id) 

302 

303 trans = response.InverseEvalresp(respfile, tr) 

304 return trans 

305 else: 

306 raise eventdata.NoRestitution( 

307 'no response information available for trace %s.%s.%s.%s' 

308 % tr.nslc_id) 

309 

310 def get_pyrocko_response(self, tr, target): 

311 ''' 

312 Extract the frequency response as :py:class:`response.EvalResp` 

313 instance for *tr*. 

314 

315 :param tr: :py:class:`trace.Trace` instance 

316 ''' 

317 respfile = self.get_resp_file(tr) 

318 return response.Evalresp(respfile, tr, target) 

319 

320 def _unpack(self): 

321 input_fn = self.seedvolume 

322 output_dir = self.tempdir 

323 

324 def strerr(s): 

325 return '\n'.join(['rdseed: '+line.decode() 

326 for line in s.splitlines()]) 

327 try: 

328 

329 # seismograms: 

330 if self._pile is None: 

331 rdseed_proc = subprocess.Popen( 

332 [Programs.rdseed, '-f', input_fn, '-d', '-z', '3', '-o', 

333 '1', '-p', '-R', '-q', output_dir], 

334 stdin=subprocess.PIPE, 

335 stdout=subprocess.PIPE, 

336 stderr=subprocess.PIPE) 

337 

338 (out, err) = rdseed_proc.communicate() 

339 logging.info(strerr(err)) 

340 else: 

341 rdseed_proc = subprocess.Popen( 

342 [Programs.rdseed, '-f', input_fn, '-z', '3', '-p', '-R', 

343 '-q', output_dir], 

344 stdin=subprocess.PIPE, 

345 stdout=subprocess.PIPE, 

346 stderr=subprocess.PIPE) 

347 

348 (out, err) = rdseed_proc.communicate() 

349 logging.info(strerr(err)) 

350 

351 # event data: 

352 rdseed_proc = subprocess.Popen( 

353 [Programs.rdseed, '-f', input_fn, '-e', '-q', output_dir], 

354 stdin=subprocess.PIPE, 

355 stdout=subprocess.PIPE, 

356 stderr=subprocess.PIPE) 

357 

358 (out, err) = rdseed_proc.communicate() 

359 logging.info(strerr(err)) 

360 

361 # station summary information: 

362 rdseed_proc = subprocess.Popen( 

363 [Programs.rdseed, '-f', input_fn, '-S', '-q', output_dir], 

364 stdin=subprocess.PIPE, 

365 stdout=subprocess.PIPE, 

366 stderr=subprocess.PIPE) 

367 

368 (out, err) = rdseed_proc.communicate() 

369 logging.info(strerr(err)) 

370 

371 # station headers: 

372 rdseed_proc = subprocess.Popen( 

373 [Programs.rdseed, '-f', input_fn, '-s', '-q', output_dir], 

374 stdin=subprocess.PIPE, 

375 stdout=subprocess.PIPE, 

376 stderr=subprocess.PIPE) 

377 

378 (out, err) = rdseed_proc.communicate() 

379 with open(self.station_headers_file, 'w') as fout: 

380 fout.write(out.decode()) 

381 logging.info(strerr(err)) 

382 

383 except OSError as e: 

384 if e.errno == 2: 

385 reason = "Could not find executable: '%s'." % Programs.rdseed 

386 else: 

387 reason = str(e) 

388 

389 logging.fatal('Failed to unpack SEED volume. %s' % reason) 

390 sys.exit(1) 

391 

392 def _get_events_from_file(self): 

393 rdseed_event_file = os.path.join(self.tempdir, 'rdseed.events') 

394 if not os.path.isfile(rdseed_event_file): 

395 return [] 

396 

397 with open(rdseed_event_file, 'r') as f: 

398 events = [] 

399 for line in f: 

400 toks = line.split(', ') 

401 if len(toks) == 9: 

402 datetime = toks[1].split('.')[0] 

403 format = '%Y/%m/%d %H:%M:%S' 

404 secs = util.to_time_float(calendar.timegm( 

405 time.strptime(datetime, format))) 

406 e = model.Event( 

407 lat=float(toks[2]), 

408 lon=float(toks[3]), 

409 depth=float(toks[4])*1000., 

410 magnitude=float(toks[8]), 

411 time=secs) 

412 

413 events.append(e) 

414 else: 

415 raise Exception('Event description in unrecognized format') 

416 

417 return events 

418 

419 def _get_stations_from_file(self): 

420 stations = read_station_header_file(self.station_headers_file) 

421 return stations 

422 

423 def _insert_channel_descriptions(self, stations): 

424 # this is done beforehand in this class 

425 pass 

426 

427 

428def check_have_rdseed(): 

429 if not Programs.check(): 

430 raise ExternalProgramMissing( 

431 'rdseed is not installed or cannot be found') 

432 

433 

434if __name__ == '__main__': 

435 print(SeedVolumeAccess(sys.argv[1]).get_stationxml().dump_xml())