Source code for pyrocko.io.mseed

# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------

'''
Read/write MiniSEED files (wraps `libmseed
<https://github.com/EarthScope/libmseed>`_).
'''


from collections import defaultdict
from struct import unpack
import os
import re
import math
import logging

from pyrocko import trace
from pyrocko.util import reuse, ensuredirs
from .io_common import FileLoadError, FileSaveError

logger = logging.getLogger('pyrocko.io.mseed')

MSEED_HEADER_BYTES = 64
VALID_RECORD_LENGTHS = tuple(2**exp for exp in range(8, 20))

g_bytes_read = 0


[docs] class CodeTooLong(FileSaveError): ''' Raised when a code of a trace is too long to be stored in Mini-SEED. ''' pass
[docs] class ExtraCodeNotSupported(FileSaveError): ''' Raised when trying to store a trace with an extra code in Mini-SEED. ''' pass
[docs] def check_codes(tr): ''' Check that the codes of a trace can be stored in Mini-SEED. :param tr: Trace to check. :type tr: :py:class:`~pyrocko.trace.Trace` :raises: :py:exc:`CodeTooLong` if a code is too long, :py:exc:`ExtraCodeNotSupported` if the trace has an extra code. ''' for code, maxlen, val in zip( ['network', 'station', 'location', 'channel'], [2, 5, 2, 3], tr.nslc_id): if len(val) > maxlen: raise CodeTooLong( '%s code too long to be stored in MSeed file: %s' % (code, val)) if tr.extra: raise ExtraCodeNotSupported( 'Mini-SEED cannot store the extra code of trace %s. Remove it ' 'before saving, e.g. with `squirrel jackseis --rename-extra ' '\'\'`, or use another file format.' % '.'.join(tr.codes))
def iload(filename, load_data=True, offset=0, segment_size=0, nsegments=0, tmin=0.0, tmax=0.0): from pyrocko import mseed_ext global g_bytes_read try: isegment = 0 while isegment < nsegments or nsegments == 0: tr_tuples = mseed_ext.get_traces( filename, load_data, offset, segment_size, tmin=int(tmin*float(mseed_ext.HPTMODULUS)), tmax=int(tmax*float(mseed_ext.HPTMODULUS))) if not tr_tuples: break offset_end = tr_tuples[0][9] assert all(tr_tuple[9] == offset_end for tr_tuple in tr_tuples) g_bytes_read += offset_end - offset for tr_tuple in tr_tuples: network, station, location, channel = tr_tuple[1:5] _tmin = float(tr_tuple[5])/float(mseed_ext.HPTMODULUS) _tmax = float(tr_tuple[6])/float(mseed_ext.HPTMODULUS) try: deltat = reuse(1.0/float(tr_tuple[7])) except ZeroDivisionError: deltat = 0.0 ydata = tr_tuple[8] tr = trace.Trace( network.strip(), station.strip(), location.strip(), channel.strip(), _tmin, _tmax, deltat, ydata) tr.meta = { 'offset_start': offset, 'offset_end': offset_end, 'last': tr_tuple[10], 'segment_size': segment_size } if tmin or tmax: try: tr.chop(tmin or tr.tmin, tmax or tr.tmax, include_last=False) except trace.NoData: pass yield tr if tr_tuple[10]: break offset = tr_tuple[9] isegment += 1 except (OSError, mseed_ext.MSeedError) as e: raise FileLoadError(str(e)+' (file: %s)' % filename) def as_tuple(tr, dataquality='D'): from pyrocko import mseed_ext itmin = int(round(tr.tmin*mseed_ext.HPTMODULUS)) itmax = int(round(tr.tmax*mseed_ext.HPTMODULUS)) srate = 1.0/tr.deltat return (tr.network, tr.station, tr.location, tr.channel, itmin, itmax, srate, dataquality, tr.get_ydata()) def save( traces, filename_template, additional={}, overwrite=True, dataquality='D', record_length=4096, append=False, check_append=False, check_append_merge=False, check_append_hook=None, check_overlaps=True, steim=1): from pyrocko import mseed_ext assert record_length in VALID_RECORD_LENGTHS assert dataquality in ('D', 'E', 'C', 'O', 'T', 'L'), 'invalid dataquality' # nifty logic for overwrite, append, check_append_hook(fn): # file exists: # overwrite, append # 0, 0 => fail # 0, 1, None => append # 0, 1, 0 => fail # 0, 1, 1 => append # 1, 0 => truncate # 1, 1, None => append # 1, 1, 0 => truncate, append # 1, 1, 1 => append if not append: check_append = False check_append_hook = None fn_tr = defaultdict(list) for tr in traces: check_codes(tr) fn = tr.fill_template(filename_template, **additional) if os.path.exists(fn): if not overwrite: if not append or ( append and check_append_hook and not check_append_hook(fn)): raise FileSaveError('File exists: %s' % fn) else: if not append or ( append and check_append_hook and not check_append_hook(fn)): os.unlink(fn) fn_tr[fn].append(tr) for fn, traces_thisfile in fn_tr.items(): if check_overlaps: try: trace.check_overlaps( traces_thisfile, message='Traces to be stored have overlaps.\n File: %s' % fn) except trace.OverlappingTraces as e: raise FileSaveError(str(e)) from e append_ = append if check_append: if os.path.exists(fn): traces_infile = list(iload(fn, load_data=False)) try: trace.check_overlaps( traces_thisfile, traces_infile, message='Trace to be stored would overlap with ' 'trace already stored in file.\n File: %s' % fn) except trace.OverlappingTraces as e: if check_append_merge: traces_thisfile_merge = list(iload(fn, load_data=True)) traces_thisfile_merge.extend( tr.copy() for tr in traces_thisfile) traces_thisfile = trace.degapper( sorted( traces_thisfile_merge, key=lambda tr: tr.full_id)) append_ = False else: raise FileSaveError(str(e)) from e trtups = [] traces_thisfile.sort(key=lambda a: a.full_id) for tr in traces_thisfile: trtups.append(as_tuple(tr, dataquality)) ensuredirs(fn) try: mseed_ext.store_traces(trtups, fn, record_length, append_, steim) except mseed_ext.MSeedError as e: raise FileSaveError( str(e) + " (while storing traces to file '%s')" % fn) return list(fn_tr.keys()) tcs = {} def get_bytes(traces, dataquality='D', record_length=4096, steim=1): from pyrocko import mseed_ext assert record_length in VALID_RECORD_LENGTHS assert dataquality in ('D', 'E', 'C', 'O', 'T', 'L'), 'invalid dataquality' nbytes_approx = 0 rl = record_length trtups = [] for tr in traces: check_codes(tr) nbytes_approx += math.ceil( tr.ydata.nbytes / (rl-MSEED_HEADER_BYTES)) * rl trtups.append(as_tuple(tr, dataquality)) return mseed_ext.mseed_bytes(trtups, nbytes_approx, record_length, steim) def detect(first512): if len(first512) < 256: return False rec = first512 try: sequence_number = int(rec[:6]) except Exception: return False if sequence_number < 0: return False type_code = rec[6:7] if type_code in b'DRQM': bads = [] for sex in '<>': bad = False fmt = sex + '6s1s1s5s2s3s2s10sH2h4Bl2H' vals = unpack(fmt, rec[:48]) fmt_btime = sex + 'HHBBBBH' tvals = unpack(fmt_btime, vals[7]) if tvals[1] < 1 or tvals[1] > 367 or tvals[2] > 23 or \ tvals[3] > 59 or tvals[4] > 60 or tvals[6] > 9999: bad = True bads.append(bad) if all(bads): return False else: if type_code not in b'VAST': return False continuation_code = rec[7:8] if continuation_code != b' ': return False blockette_type = rec[8:8+3].decode() if not re.match(r'^\d\d\d$', blockette_type): return False try: blockette_length = int(rec[11:11+4]) except Exception: return False if blockette_length < 7: return False return True