diff --git a/geomagio/Controller.py b/geomagio/Controller.py index df3b841951608a4da5310954bbf88b82b57b0851..370170a9b4eb8a9809d3577c8e900f26958d0dc2 100644 --- a/geomagio/Controller.py +++ b/geomagio/Controller.py @@ -17,6 +17,7 @@ import imfv283 # factories for new filetypes import temperature import vbf +import binlog class Controller(object): @@ -406,13 +407,16 @@ def get_output_factory(args): elif output_url is not None: output_factory = temperature.TEMPFactory( **output_factory_args) - elif output_type == 'vbf' or output_type == 'binlog': + elif output_type == 'vbf': if output_stream is not None: output_factory = vbf.StreamVBFFactory( - output=output_type, + **output_factory_args) + elif output_type == 'binlog': + if output_stream is not None: + output_factory = binlog.StreamBinLogFactory( **output_factory_args) elif output_url is not None: - output_factory = vbf.VBFFactory( + output_factory = binlog.BinLogFactory( output=output_type, **output_factory_args) return output_factory diff --git a/geomagio/binlog/BinLogFactory.py b/geomagio/binlog/BinLogFactory.py new file mode 100644 index 0000000000000000000000000000000000000000..ee46eba857e2039a185367c3ed5f8a4fed8fe970 --- /dev/null +++ b/geomagio/binlog/BinLogFactory.py @@ -0,0 +1,36 @@ +"""Factory that creates BinLog Files.""" + +from ..TimeseriesFactory import TimeseriesFactory +from BinLogWriter import BinLogWriter + + +class BinLogFactory(TimeseriesFactory): + """TimeseriesFactory for BinLog formatted files. + + Parameters + ---------- + output : bin-change report. + + All other named parameters passed to TimeseriesFactory. + + See Also + -------- + TimeseriesFactory + """ + + def __init__(self, **kwargs): + TimeseriesFactory.__init__(self, **kwargs) + + def write_file(self, fh, timeseries, channels): + """Write timeseries data to the given file object. + + Parameters + ---------- + fh : writable + file handle where data is written. + timeseries : obspy.core.Stream + stream containing traces to store. + channels : list + list of channels to store. + """ + BinLogWriter().write(fh, timeseries, channels) diff --git a/geomagio/binlog/BinLogWriter.py b/geomagio/binlog/BinLogWriter.py new file mode 100644 index 0000000000000000000000000000000000000000..ced87b800ce74ff3623f3b6abcca1b79be84e61d --- /dev/null +++ b/geomagio/binlog/BinLogWriter.py @@ -0,0 +1,215 @@ + +import numpy +from cStringIO import StringIO +from datetime import datetime +from .. import ChannelConverter, TimeseriesUtility +from ..TimeseriesFactoryException import TimeseriesFactoryException +from obspy.core import Stream + + +# For binlog, need to track previous volt/bin values. +h_prev = [99.999999, 999] +e_prev = [99.999999, 999] +z_prev = [99.999999, 999] +# Use seperate HEZ buffers to group binlog output by component. +Hbuf = [] +Ebuf = [] +Zbuf = [] + + +class BinLogWriter(object): + """BinLog writer. + """ + + def __init__(self): + return + + def write(self, out, timeseries, channels): + """Write parsed timeseries info to binlog file. + + Parameters + ---------- + out : file object + File object to be written to. Could be stdout. + timeseries : obspy.core.stream + Timeseries object with data to be written. + channels : array_like + Channels to be written from timeseries object. + """ + for channel in channels: + if timeseries.select(channel=channel).count() == 0: + raise TimeseriesFactoryException( + 'Missing channel "%s" for output, available channels %s' % + (channel, str(TimeseriesUtility.get_channels(timeseries)))) + stats = timeseries[0].stats + + out.write(self._format_header(stats)) + + self._format_data(timeseries, channels) + + if (len(Hbuf) + len(Ebuf) + len(Zbuf)) > 0: + out.write(' C Date Time DaySec Bin change' + ' Voltage change\n') + out.write(''.join(Hbuf)) + out.write('\n') + out.write(''.join(Ebuf)) + out.write('\n') + out.write(''.join(Zbuf)) + else: + out.write('*** No Bin Changes Found ***\n') + + def _format_header(self, stats): + """format headers for BinLog file + + Parameters + ---------- + stats : List + An object with the header values to be written. + + Returns + ------- + str + A string formatted to be a single header line in a BinLog file. + """ + buf = [] + + observatory = stats.station + sttdate = stats.starttime.strftime("%d-%b-%y") + enddate = stats.endtime.strftime("%d-%b-%y") + + buf.append('Bin Change Report: ' + observatory + ' Start Day: ' + + sttdate + ' End Day: ' + enddate + '\n\n') + + return ''.join(buf) + + def _format_data(self, timeseries, channels): + """Format all data lines. + + Parameters + ---------- + timeseries : obspy.core.Stream + Stream containing traces with channel listed in channels + channels : sequence + List and order of channel values to output. + + Returns + ------- + str + A string formatted to be the data lines in a BinLog file. + """ + + # create new stream + timeseriesLocal = Stream() + # Use a copy of the trace so that we don't modify the original. + for trace in timeseries: + traceLocal = trace.copy() + if traceLocal.stats.channel == 'D': + traceLocal.data = \ + ChannelConverter.get_minutes_from_radians(traceLocal.data) + + # TODO - we should look into multiplying the trace all at once + # like this, but this gives an error on Windows at the moment. + # traceLocal.data = \ + # numpy.round(numpy.multiply(traceLocal.data, 100)).astype(int) + + timeseriesLocal.append(traceLocal) + + traces = [timeseriesLocal.select(channel=c)[0] for c in channels] + starttime = float(traces[0].stats.starttime) + delta = traces[0].stats.delta + + for i in xrange(len(traces[0].data)): + self._format_values( + datetime.utcfromtimestamp(starttime + i * delta), + (t.data[i] for t in traces)) + + return + + def _format_values(self, time, values): + """Format one line of data values. + + Parameters + ---------- + time : datetime + Timestamp for values. + values : sequence + List and order of channel values to output. + + Returns + ------- + unicode + Formatted line containing values. + """ + + tt = time.timetuple() + totalMinutes = int(tt.tm_hour * 3600 + tt.tm_min * 60 + tt.tm_sec) + + timestr = '{0.tm_year:0>4d}-{0.tm_mon:0>2d}-{0.tm_mday:0>2d} ' \ + '{0.tm_hour:0>2d}:{0.tm_min:0>2d}:{0.tm_sec:0>2d}' \ + ' ({1:0>5d})'. \ + format(tt, totalMinutes) + + # init volt/bin vals to dead + vdead = 99.999999 + bdead = 999 + vblist = [vdead, bdead, vdead, bdead, vdead, bdead] + + # now "un-dead" the non-nans, format volts as float, bins as int + for idx, valx in enumerate(values): + if ~numpy.isnan(valx): + if idx == 0 or idx == 2 or idx == 4: + vblist[idx] = valx / 1000. + else: + vblist[idx] = int(valx) + + if vblist[1] != 999 and h_prev[1] != 999 and vblist[1] != h_prev[1]: + Hbuf.append('{0: >3s} {1:>s} ' + '{2: >4d} to {3: >4d} {4: >10.6f} to {5: >10.6f}\n'. + format('(H)', timestr, h_prev[1], + vblist[1], h_prev[0], vblist[0])) + + if vblist[3] != 999 and e_prev[1] != 999 and vblist[3] != e_prev[1]: + Ebuf.append('{0: >3s} {1:>s} ' + '{2: >4d} to {3: >4d} {4: >10.6f} to {5: >10.6f}\n'. + format('(E)', timestr, e_prev[1], + vblist[3], e_prev[0], vblist[2])) + + if vblist[5] != 999 and z_prev[1] != 999 and vblist[5] != z_prev[1]: + Zbuf.append('{0: >3s} {1:>s} ' + '{2: >4d} to {3: >4d} {4: >10.6f} to {5: >10.6f}\n'. + format('(Z)', timestr, z_prev[1], + vblist[5], z_prev[0], vblist[4])) + + h_prev[0] = vblist[0] + h_prev[1] = vblist[1] + + e_prev[0] = vblist[2] + e_prev[1] = vblist[3] + + z_prev[0] = vblist[4] + z_prev[1] = vblist[5] + + return + + @classmethod + def format(self, timeseries, channels): + """Get a BinLog formatted string. + + Calls write() with a StringIO, and returns the output. + + Parameters + ---------- + timeseries : obspy.core.Stream + Stream containing traces with channel listed in channels + channels : sequence + List and order of channel values to output. + + Returns + ------- + unicode + BinLog formatted string. + """ + out = StringIO() + writer = BinLogWriter() + writer.write(out, timeseries, channels) + return out.getvalue() diff --git a/geomagio/binlog/StreamBinLogFactory.py b/geomagio/binlog/StreamBinLogFactory.py new file mode 100644 index 0000000000000000000000000000000000000000..6fda2de572692126a23cdadb8ad9f89668b821e9 --- /dev/null +++ b/geomagio/binlog/StreamBinLogFactory.py @@ -0,0 +1,34 @@ +"""Factory to load BinLog files from an input StreamBinLogFactory.""" + +from BinLogFactory import BinLogFactory + + +class StreamBinLogFactory(BinLogFactory): + """Timeseries Factory for BinLog files loaded via a stream. + normally either a single file, or stdio. + + Parameters + ---------- + stream: file object + io stream, normally either a file, or stdio + + See Also + -------- + BinLogFactory + Timeseriesfactory + """ + + def __init__(self, stream, **kwargs): + BinLogFactory.__init__(self, **kwargs) + self._stream = stream + + def put_timeseries(self, timeseries, starttime=None, endtime=None, + channels=None, type=None, interval=None): + """Implements put_timeseries + + Notes: Calls BinLogFactory.write_file in place of + BinLogFactory.put_timeseries. This can result in a + non-standard BinLog file, specifically one of longer than + expected length. + """ + BinLogFactory.write_file(self, self._stream, timeseries, channels) diff --git a/geomagio/binlog/__init__.py b/geomagio/binlog/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..533042e79b16c99b066e94973962c869be95bbda --- /dev/null +++ b/geomagio/binlog/__init__.py @@ -0,0 +1,13 @@ +"""IO Module for BinLog Format +""" + +from BinLogFactory import BinLogFactory +from StreamBinLogFactory import StreamBinLogFactory +from BinLogWriter import BinLogWriter + + +__all__ = [ + 'BinLogFactory', + 'StreamBinLogFactory', + 'BinLogWriter' +] diff --git a/geomagio/vbf/StreamVBFFactory.py b/geomagio/vbf/StreamVBFFactory.py index eba950fa2eb176615c3c5b495f050e631c51e6c9..100150b6e98383d77d003e07d33cd1b4c7fb9aba 100644 --- a/geomagio/vbf/StreamVBFFactory.py +++ b/geomagio/vbf/StreamVBFFactory.py @@ -18,7 +18,6 @@ class StreamVBFFactory(VBFFactory): Timeseriesfactory """ - # Flag "output" used for vbf file versus bin-change log. def __init__(self, stream, **kwargs): VBFFactory.__init__(self, **kwargs) self._stream = stream diff --git a/geomagio/vbf/VBFFactory.py b/geomagio/vbf/VBFFactory.py index e2b91ce0add44ebe942904351550b423514e4dfa..4c4d821eaf0426d12394e0c45f33eee818d2b237 100644 --- a/geomagio/vbf/VBFFactory.py +++ b/geomagio/vbf/VBFFactory.py @@ -13,8 +13,7 @@ class VBFFactory(TimeseriesFactory): Parameters ---------- - output : {'binlog', 'vbf'} - bin-change or vbf style output. + output : vbf style output. All other named parameters passed to TimeseriesFactory. @@ -23,9 +22,8 @@ class VBFFactory(TimeseriesFactory): TimeseriesFactory """ - def __init__(self, output='vbf', **kwargs): + def __init__(self, **kwargs): TimeseriesFactory.__init__(self, **kwargs) - self.output = output def write_file(self, fh, timeseries, channels): """Write timeseries data to the given file object. @@ -39,7 +37,4 @@ class VBFFactory(TimeseriesFactory): channels : list list of channels to store. """ - if self.output == 'binlog': - VBFWriter().write_change_log(fh, timeseries, channels) - else: - VBFWriter().write(fh, timeseries, channels) + VBFWriter().write(fh, timeseries, channels) diff --git a/geomagio/vbf/VBFWriter.py b/geomagio/vbf/VBFWriter.py index 9b3cfbff3eec6d2ad2bcd6ffe59ca08a3ff213ff..45d9cc2786fbefb75efa2f8d13a92b8f28648833 100644 --- a/geomagio/vbf/VBFWriter.py +++ b/geomagio/vbf/VBFWriter.py @@ -7,16 +7,6 @@ from ..TimeseriesFactoryException import TimeseriesFactoryException from obspy.core import Stream -# For a binlog, need to save previous time and volts. -h_prev = [99.999999, 999] -e_prev = [99.999999, 999] -z_prev = [99.999999, 999] -# Use seperate HEZ buffers to group binlog output by component. -Hbuf = [] -Ebuf = [] -Zbuf = [] - - class VBFWriter(object): """VBF writer. """ @@ -151,180 +141,6 @@ class VBFWriter(object): return '{0:0>5d} {1: >10.6f} {2: >4d} {3: >10.6f} {4: >4d} ' \ '{5: >10.6f} {6: >4d}\n'.format(totalMinutes, *vblist) - # =============================================== - # CODE BELOW IS FOR MAKING A BIN CHANGE LOG. - # VBFFactory calls the '_change_' version of the - # procedures rather than the "usual" procedures - # =============================================== - - def write_change_log(self, out, timeseries, channels): - """Write timeseries to vbf file. - - Parameters - ---------- - out : file object - File object to be written to. Could be stdout. - timeseries : obspy.core.stream - Timeseries object with data to be written. - channels : array_like - Channels to be written from timeseries object. - """ - for channel in channels: - if timeseries.select(channel=channel).count() == 0: - raise TimeseriesFactoryException( - 'Missing channel "%s" for output, available channels %s' % - (channel, str(TimeseriesUtility.get_channels(timeseries)))) - stats = timeseries[0].stats - - out.write(self._format_change_header(stats)) - - self._format_change_data(timeseries, channels) - - if (len(Hbuf) + len(Ebuf) + len(Zbuf)) > 0: - out.write(' C Date Time DaySec Bin change' - ' Voltage change\n') - out.write(''.join(Hbuf)) - out.write('\n') - out.write(''.join(Ebuf)) - out.write('\n') - out.write(''.join(Zbuf)) - else: - out.write('*** No Bin Changes Found ***\n') - - def _format_change_header(self, stats): - """format headers for VBF file - - Parameters - ---------- - stats : List - An object with the header values to be written. - - Returns - ------- - str - A string formatted to be a single header line in a VBF file. - """ - buf = [] - - observatory = stats.station - sttdate = stats.starttime.strftime("%d-%b-%y") - enddate = stats.endtime.strftime("%d-%b-%y") - - buf.append('Bin Change Report: ' + observatory + ' Start Day: ' + - sttdate + ' End Day: ' + enddate + '\n\n') - - return ''.join(buf) - - def _format_change_data(self, timeseries, channels): - """Format all data lines. - - Parameters - ---------- - timeseries : obspy.core.Stream - Stream containing traces with channel listed in channels - channels : sequence - List and order of channel values to output. - - Returns - ------- - str - A string formatted to be the data lines in a VBF file. - """ - - # create new stream - timeseriesLocal = Stream() - # Use a copy of the trace so that we don't modify the original. - for trace in timeseries: - traceLocal = trace.copy() - if traceLocal.stats.channel == 'D': - traceLocal.data = \ - ChannelConverter.get_minutes_from_radians(traceLocal.data) - - # TODO - we should look into multiplying the trace all at once - # like this, but this gives an error on Windows at the moment. - # traceLocal.data = \ - # numpy.round(numpy.multiply(traceLocal.data, 100)).astype(int) - - timeseriesLocal.append(traceLocal) - - traces = [timeseriesLocal.select(channel=c)[0] for c in channels] - starttime = float(traces[0].stats.starttime) - delta = traces[0].stats.delta - - for i in xrange(len(traces[0].data)): - self._format_change_values( - datetime.utcfromtimestamp(starttime + i * delta), - (t.data[i] for t in traces)) - - return - - def _format_change_values(self, time, values): - """Format one line of data values. - - Parameters - ---------- - time : datetime - Timestamp for values. - values : sequence - List and order of channel values to output. - If value is NaN, self.empty_value is output in its place. - - Returns - ------- - unicode - Formatted line containing values. - """ - - tt = time.timetuple() - totalMinutes = int(tt.tm_hour * 3600 + tt.tm_min * 60 + tt.tm_sec) - - timestr = '{0.tm_year:0>4d}-{0.tm_mon:0>2d}-{0.tm_mday:0>2d} ' \ - '{0.tm_hour:0>2d}:{0.tm_min:0>2d}:{0.tm_sec:0>2d}' \ - ' ({1:0>5d})'. \ - format(tt, totalMinutes) - - # init volt/bin vals to dead - vdead = 99.999999 - bdead = 999 - vblist = [vdead, bdead, vdead, bdead, vdead, bdead] - - # now "un-dead" the non-nans, format volts as float, bins as int - for idx, valx in enumerate(values): - if ~numpy.isnan(valx): - if idx == 0 or idx == 2 or idx == 4: - vblist[idx] = valx / 1000. - else: - vblist[idx] = int(valx) - - if vblist[1] != 999 and h_prev[1] != 999 and vblist[1] != h_prev[1]: - Hbuf.append('{0: >3s} {1:>s} ' - '{2: >4d} to {3: >4d} {4: >10.6f} to {5: >10.6f}\n'. - format('(H)', timestr, h_prev[1], - vblist[1], h_prev[0], vblist[0])) - - if vblist[3] != 999 and e_prev[1] != 999 and vblist[3] != e_prev[1]: - Ebuf.append('{0: >3s} {1:>s} ' - '{2: >4d} to {3: >4d} {4: >10.6f} to {5: >10.6f}\n'. - format('(E)', timestr, e_prev[1], - vblist[3], e_prev[0], vblist[2])) - - if vblist[5] != 999 and z_prev[1] != 999 and vblist[5] != z_prev[1]: - Zbuf.append('{0: >3s} {1:>s} ' - '{2: >4d} to {3: >4d} {4: >10.6f} to {5: >10.6f}\n'. - format('(Z)', timestr, z_prev[1], - vblist[5], z_prev[0], vblist[4])) - - h_prev[0] = vblist[0] - h_prev[1] = vblist[1] - - e_prev[0] = vblist[2] - e_prev[1] = vblist[3] - - z_prev[0] = vblist[4] - z_prev[1] = vblist[5] - - return - @classmethod def format(self, timeseries, channels): """Get an VBF formatted string.