diff --git a/geomagio/Controller.py b/geomagio/Controller.py index fcce9de087bf25a145d7fefea453a0202bc0322c..6cd3ae81d76c06c4fa73ad422f9aef2908a1d6e4 100644 --- a/geomagio/Controller.py +++ b/geomagio/Controller.py @@ -3,11 +3,9 @@ import argparse import sys -from obspy.core import UTCDateTime +from obspy.core import Stream, UTCDateTime from algorithm import algorithms import TimeseriesUtility -from TimeseriesFactoryException import TimeseriesFactoryException -from Util import ObjectView import edge import iaga2002 @@ -43,6 +41,97 @@ class Controller(object): self._algorithm = algorithm self._outputFactory = outputFactory + def _get_input_timeseries(self, observatory, channels, starttime, endtime): + """Get timeseries from the input factory for requested options. + + Parameters + ---------- + observatory : array_like + observatories to request. + channels : array_like + channels to request. + starttime : obspy.core.UTCDateTime + time of first sample to request. + endtime : obspy.core.UTCDateTime + time of last sample to request. + renames : array_like + list of channels to rename + each list item should be array_like: + the first element is the channel to rename, + the last element is the new channel name + + Returns + ------- + timeseries : obspy.core.Stream + """ + timeseries = Stream() + for obs in list(observatory): + # get input interval for observatory + # do this per observatory in case an + # algorithm needs different amounts of data + input_start, input_end = self._algorithm.get_input_interval( + start=starttime, + end=endtime, + observatory=obs, + channel=channels) + timeseries += self._inputFactory.get_timeseries( + observatory=obs, + starttime=input_start, + endtime=input_end, + channels=channels) + return timeseries + + def _rename_channels(self, timeseries, renames): + """Rename trace channel names. + + Parameters + ---------- + timeseries : obspy.core.Stream + stream with channels to rename + renames : array_like + list of channels to rename + each list item should be array_like: + the first element is the channel to rename, + the last element is the new channel name + + Returns + ------- + timeseries : obspy.core.Stream + """ + for r in renames: + from_name, to_name = r[0], r[-1] + for t in timeseries.select(channel=from_name): + t.stats.channel = to_name + return timeseries + + def _get_output_timeseries(self, observatory, channels, starttime, + endtime): + """Get timeseries from the output factory for requested options. + + Parameters + ---------- + observatory : array_like + observatories to request. + channels : array_like + channels to request. + starttime : obspy.core.UTCDateTime + time of first sample to request. + endtime : obspy.core.UTCDateTime + time of last sample to request. + + Returns + ------- + timeseries : obspy.core.Stream + """ + timeseries = Stream() + for obs in list(observatory): + timeseries += self._outputFactory.get_timeseries( + observatory=obs, + starttime=starttime, + endtime=endtime, + channels=channels) + return timeseries + def run(self, options): """run controller Parameters @@ -52,22 +141,28 @@ class Controller(object): contain other options passed in by the controller. """ algorithm = self._algorithm - input_channels = algorithm.get_input_channels() - output_channels = self._get_output_channels( - algorithm.get_output_channels(), - options.outchannels) - # get input - start, end = self._algorithm.get_input_interval( - start=options.starttime, - end=options.endtime) - timeseries = self._inputFactory.get_timeseries( - starttime=start, - endtime=end, + input_channels = options.inchannels or \ + algorithm.get_input_channels() + output_channels = options.outchannels or \ + algorithm.get_output_channels() + # input + timeseries = self._get_input_timeseries( + observatory=options.observatory, + starttime=options.starttime, + endtime=options.endtime, channels=input_channels) if timeseries.count() == 0: return # process + if options.rename_input_channel: + timeseries = self._rename_channels( + timeseries=timeseries, + renames=options.rename_input_channel) processed = algorithm.process(timeseries) + if options.rename_output_channel: + processed = self._rename_channels( + timeseries=processed, + renames=options.rename_output_channel) # output self._outputFactory.put_timeseries( timeseries=processed, @@ -95,12 +190,13 @@ class Controller(object): period, oldest to newest. """ algorithm = self._algorithm - input_channels = algorithm.get_input_channels() - output_channels = self._get_output_channels( - algorithm.get_output_channels(), - options.outchannels) + input_channels = options.inchannels or \ + algorithm.get_input_channels() + output_channels = options.outchannels or \ + algorithm.get_output_channels() # request output to see what has already been generated - output_timeseries = self._outputFactory.get_timeseries( + output_timeseries = self._get_output_timeseries( + observatory=options.observatory, starttime=options.starttime, endtime=options.endtime, channels=output_channels) @@ -109,12 +205,10 @@ class Controller(object): output_gaps = TimeseriesUtility.get_merged_gaps( TimeseriesUtility.get_stream_gaps(output_timeseries)) for output_gap in output_gaps: - input_start, input_end = algorithm.get_input_interval( - start=output_gap[0], - end=output_gap[1]) - input_timeseries = self._inputFactory.get_timeseries( - starttime=input_start, - endtime=input_end, + input_timeseries = self._get_input_timeseries( + observatory=options.observatory, + starttime=output_gap[0], + endtime=output_gap[1], channels=input_channels) if not algorithm.can_produce_data( starttime=output_gap[0], @@ -125,40 +219,13 @@ class Controller(object): if output_gap[0] == options.starttime: # found fillable gap at start, recurse to previous interval interval = options.endtime - options.starttime - self.run_as_update(ObjectView({ - 'outchannels': options.outchannels, - 'starttime': options.starttime - interval - delta, - 'endtime': options.starttime - delta - })) + options.starttime = options.starttime - interval - delta + options.endtime = options.starttime - delta + self.run_as_update(options) # fill gap - self.run(ObjectView({ - 'outchannels': options.outchannels, - 'starttime': output_gap[0], - 'endtime': output_gap[1] - })) - - def _get_output_channels(self, algorithm_channels, commandline_channels): - """get output channels - - Parameters - ---------- - algorithm_channels: array_like - list of channels required by the algorithm - commandline_channels: array_like - list of channels requested by the user - Notes - ----- - We want to return the channels requested by the user, but we require - that they be in the list of channels for the algorithm. - """ - if commandline_channels is not None: - for channel in commandline_channels: - if channel not in algorithm_channels: - raise TimeseriesFactoryException( - 'Output "%s" Channel not in Algorithm' - % channel) - return commandline_channels - return algorithm_channels + options.starttime = output_gap[0] + options.endtime = output_gap[1] + self.run(options) def main(args): @@ -347,7 +414,11 @@ def parse_args(args): help='UTC date YYYY-MM-DD HH:MM:SS') parser.add_argument('--observatory', - help='Observatory code ie BOU, CMO, etc') + help='Observatory code ie BOU, CMO, etc.' + + ' CAUTION: Using multiple observatories is not' + + ' recommended in most cases; especially with' + + ' single observatory formats like IAGA and PCDCP.', + nargs='*') parser.add_argument('--inchannels', nargs='*', help='Channels H, E, Z, etc') @@ -358,6 +429,16 @@ def parse_args(args): parser.add_argument('--type', default='variation', choices=['variation', 'quasi-definitive', 'definitive']) + parser.add_argument('--rename-input-channel', + action='append', + help='Rename an input channel after it is read', + metavar=('FROM', 'TO'), + nargs=2) + parser.add_argument('--rename-output-channel', + action='append', + help='Rename an output channel before it is written', + metavar=('FROM', 'TO'), + nargs=2) parser.add_argument('--locationcode', choices=['R0', 'R1', 'RM', 'Q0', 'D0', 'C0']) parser.add_argument('--outlocationcode', diff --git a/geomagio/TimeseriesUtility.py b/geomagio/TimeseriesUtility.py index 20aa4974bbb5d94db587a74030278a2ef063b20e..e547e63c9578b2ff053ad45ddb5e12841dddd3fa 100644 --- a/geomagio/TimeseriesUtility.py +++ b/geomagio/TimeseriesUtility.py @@ -110,3 +110,22 @@ def get_merged_gaps(gaps): if merged_gap is not None: merged_gaps.append(merged_gap) return merged_gaps + + +def get_channels(stream): + """Get a list of channels in a stream. + + Parameters + ---------- + stream : obspy.core.Stream + + Returns + ------- + channels : array_like + """ + channels = {} + for trace in stream: + channel = trace.stats.channel + if channel: + channels[channel] = True + return [ch for ch in channels] diff --git a/geomagio/algorithm/Algorithm.py b/geomagio/algorithm/Algorithm.py index e3fdc3f471bd90e7160c21a70884d9504f33cea6..1aa1b7b6c2ea523fca1879e635088733402599f5 100644 --- a/geomagio/algorithm/Algorithm.py +++ b/geomagio/algorithm/Algorithm.py @@ -56,18 +56,24 @@ class Algorithm(object): """ return self._outchannels - def get_input_interval(self, start, end): + def get_input_interval(self, start, end, observatory=None, channels=None): """Get Input Interval start : UTCDateTime - start time of requested output + start time of requested output. end : UTCDateTime - end time of requested output + end time of requested output. + observatory : string + observatory code. + channels : string + input channels. Returns ------- - tuple : (input_start, input_end) - start and end of required input to generate requested output. + input_start : UTCDateTime + start of input required to generate requested output + input_end : UTCDateTime + end of input required to generate requested output. """ return (start, end) diff --git a/geomagio/edge/EdgeFactory.py b/geomagio/edge/EdgeFactory.py index 7b92a33ea51715e3a2702c0acf8d1d00995c82e8..307275a9cd826e1a22b8d876030a89419ee31341 100644 --- a/geomagio/edge/EdgeFactory.py +++ b/geomagio/edge/EdgeFactory.py @@ -17,7 +17,7 @@ import obspy.core from datetime import datetime from obspy import earthworm from obspy.core import UTCDateTime -from .. import ChannelConverter +from .. import ChannelConverter, TimeseriesUtility from ..TimeseriesFactory import TimeseriesFactory from ..TimeseriesFactoryException import TimeseriesFactoryException from ..ObservatoryMetadata import ObservatoryMetadata @@ -183,12 +183,11 @@ class EdgeFactory(TimeseriesFactory): if (starttime is None or endtime is None): starttime, endtime = self._get_stream_start_end_times(timeseries) - for channel in channels: if timeseries.select(channel=channel).count() == 0: raise TimeseriesFactoryException( - 'Missing channel "%s" for output' % channel) - + 'Missing channel "%s" for output, available channels %s' % + (channel, str(TimeseriesUtility.get_channels(timeseries)))) for channel in channels: self._put_channel(timeseries, observatory, channel, type, interval, starttime, endtime) diff --git a/geomagio/iaga2002/IAGA2002Writer.py b/geomagio/iaga2002/IAGA2002Writer.py index 56c510f3d0dbc273eea1dc9e06cb8723fb5da909..cd070075a3c5e046044ea54586301c142d857793 100644 --- a/geomagio/iaga2002/IAGA2002Writer.py +++ b/geomagio/iaga2002/IAGA2002Writer.py @@ -3,7 +3,7 @@ from cStringIO import StringIO from datetime import datetime import numpy import textwrap -from .. import ChannelConverter +from .. import ChannelConverter, TimeseriesUtility from ..TimeseriesFactoryException import TimeseriesFactoryException from ..Util import create_empty_trace import IAGA2002Parser @@ -30,6 +30,11 @@ class IAGA2002Writer(object): 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 if len(channels) != 4: self._pad_to_four_channels(timeseries, channels) diff --git a/geomagio/pcdcp/PCDCPWriter.py b/geomagio/pcdcp/PCDCPWriter.py index e26c29075ae4b3320619ac617ce5190babe4f767..3042df76e476185449be5b5cf288cc8aa20fa280 100644 --- a/geomagio/pcdcp/PCDCPWriter.py +++ b/geomagio/pcdcp/PCDCPWriter.py @@ -3,7 +3,8 @@ import numpy import PCDCPParser from cStringIO import StringIO from datetime import datetime -from geomagio import ChannelConverter +from .. import ChannelConverter, TimeseriesUtility +from ..TimeseriesFactoryException import TimeseriesFactoryException from obspy.core import Stream @@ -26,6 +27,11 @@ class PCDCPWriter(object): 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)) out.write(self._format_data(timeseries, channels))