def test__set_time_value_handle_none(self): """Check TypeError exception for private method""" try: ossm = CyTimeseries(timeseries=self.tval) ossm._set_time_value_handle(None) except TypeError: assert True
def test_readfile_constant_wind(self): """ Read contents for a filename that contains a constant wind. This will be just 1 line in the text filename. Test get_time_values method. It gets the time value pair for the constant wind per the data filename. This test just gets the time value pair that was created from the filename. It then invokes get_time_value for that time in the time series and also looks at the velocity 100 sec later. Since wind is constant, the value should be unchanged """ ossmT = CyTimeseries( filename=testdata["timeseries"]["wind_constant"], file_format=ts_format.magnitude_direction ) # Let's see what is stored in the Handle to expected result t_val = ossmT.timeseries actual = np.array(t_val["value"], dtype=velocity_rec) time = np.array(t_val["time"] + (0, 100), dtype=seconds) vel_rec = ossmT.get_time_value(time) tol = 1e-6 msg = "{0} is not within a tolerance of " "{1}".format("get_time_value", tol) for vel in vel_rec: np.testing.assert_allclose(vel["u"], actual["u"], tol, tol, msg, 0) np.testing.assert_allclose(vel["v"], actual["v"], tol, tol, msg, 0)
def test_timeseries(self): """ test setting the timeseries using timeseries property """ ossmT = CyTimeseries(timeseries=self.tval) t_val = ossmT.timeseries print 't_val before:', t_val['value'] # need to learn how to do the following in 1 line of code t_val['value']['u'] += 2 t_val['value']['v'] += 2 print 't_val after:', t_val['value'] ossmT.timeseries = t_val new_val = ossmT.timeseries tol = 1e-10 msg = ('{0} is not within a tolerance of ' '{1}'.format('get_time_value', tol)) np.testing.assert_allclose(t_val['time'], new_val['time'], tol, tol, msg, 0) np.testing.assert_allclose(t_val['value']['u'], new_val['value']['u'], tol, tol, msg, 0) np.testing.assert_allclose(t_val['value']['v'], new_val['value']['v'], tol, tol, msg, 0)
def test_move_out_of_bounds(self): ''' Our wind mover should fail in the prepare_for_model_step() function if our wind time series is out of bounds with respect to the model time we are preparing for, unless our wind time series is configured to extrapolate wind values. ''' # setup a time series that's out of range of our model time time_val = np.zeros((2, ), dtype=time_value_pair) (time_val['time'])[:] = np.add([3600, 7200], self.cm.model_time) (time_val['value']['v'])[:] = [100, 200] # extrapolation should be off by default ossm = CyTimeseries(timeseries=time_val) self.wm.set_ossm(ossm) # this should fail because our time series is not set to extrapolate with raises(OSError): self.wm.prepare_for_model_step(self.cm.model_time, self.cm.time_step) ossm = CyTimeseries(timeseries=time_val, extrapolation_is_allowed=True) self.wm.set_ossm(ossm) # We set our time series to extrapolate, so this should pass self.wm.prepare_for_model_step(self.cm.model_time, self.cm.time_step) # clean up our time series self.wm.set_ossm(self.ossm)
def test_readfile_constant_wind(self): """ Read contents for a filename that contains a constant wind. This will be just 1 line in the text filename. Test get_time_values method. It gets the time value pair for the constant wind per the data filename. This test just gets the time value pair that was created from the filename. It then invokes get_time_value for that time in the time series and also looks at the velocity 100 sec later. Since wind is constant, the value should be unchanged """ ossmT = CyTimeseries(filename=testdata['timeseries']['wind_constant'], file_format=ts_format.magnitude_direction) # Let's see what is stored in the Handle to expected result t_val = ossmT.timeseries actual = np.array(t_val['value'], dtype=velocity_rec) time = np.array(t_val['time'] + (0, 100), dtype=seconds) vel_rec = ossmT.get_time_value(time) tol = 1e-6 msg = ('{0} is not within a tolerance of ' '{1}'.format('get_time_value', tol)) for vel in vel_rec: np.testing.assert_allclose(vel['u'], actual['u'], tol, tol, msg, 0) np.testing.assert_allclose(vel['v'], actual['v'], tol, tol, msg, 0)
def test_get_time_out_of_bounds(self): time_values = np.array([(1, (1, 2)), (2, (2, 3))], dtype=basic_types.time_value_pair) ossm = CyTimeseries(timeseries=time_values) # testing before the start time with pytest.raises(IndexError): _vel_rec, _err = ossm.get_time_value(np.array([0], dtype=seconds)) # testing after the end time with pytest.raises(IndexError): _vel_rec, _err = ossm.get_time_value(np.array([3], dtype=seconds)) # now we allow extrapolation ossm.extrapolation_is_allowed = True begin_value, _err = ossm.get_time_value(np.array([1], dtype=seconds)) end_value, _err = ossm.get_time_value(np.array([2], dtype=seconds)) # before the start time should now return without an exception # and it should return the value associated with the start time. vel_rec, _err = ossm.get_time_value(np.array([0], dtype=seconds)) assert vel_rec == begin_value # after the end time should now return without an exception # and it should return the value associated with the end time. vel_rec, _err = ossm.get_time_value(np.array([3], dtype=seconds)) assert vel_rec == end_value
def test_get_time_value(self): ossm = CyTimeseries(timeseries=self.tval) actual = np.array(self.tval["value"], dtype=velocity_rec) time = np.array(self.tval["time"], dtype=seconds) vel_rec = ossm.get_time_value(time) print vel_rec tol = 1e-6 msg = "{0} is not within a tolerance of " "{1}".format("get_time_value", tol) np.testing.assert_allclose(vel_rec["u"], actual["u"], tol, tol, msg, 0) np.testing.assert_allclose(vel_rec["v"], actual["v"], tol, tol, msg, 0)
def test_get_time_value(self): ossm = CyTimeseries(timeseries=self.tval) actual = np.array(self.tval['value'], dtype=velocity_rec) time = np.array(self.tval['time'], dtype=seconds) vel_rec = ossm.get_time_value(time) print vel_rec tol = 1e-6 msg = ('{0} is not within a tolerance of ' '{1}'.format('get_time_value', tol)) np.testing.assert_allclose(vel_rec['u'], actual['u'], tol, tol, msg, 0) np.testing.assert_allclose(vel_rec['v'], actual['v'], tol, tol, msg, 0)
def test_get_time_value(self): ossm = CyTimeseries(timeseries=self.tval) actual = np.array(self.tval['value'], dtype=velocity_rec) time = np.array(self.tval['time'], dtype=seconds) vel_rec, _err = ossm.get_time_value(time) print vel_rec tol = 1e-6 msg = ('{0} is not within a tolerance of ' '{1}'.format('get_time_value', tol)) np.testing.assert_allclose(vel_rec['u'], actual['u'], tol, tol, msg, 0) np.testing.assert_allclose(vel_rec['v'], actual['v'], tol, tol, msg, 0)
def _obj_to_create(self, filename): """ open file, read a few lines to determine if it is an ossm file or a shio file """ # mode 'U' means universal newline support fh = open(filename, 'rU') lines = [fh.readline() for i in range(4)] if len(lines[1]) == 0: # look for \r for lines instead of \n lines = string.split(lines[0], '\r', 4) if len(lines[1]) == 0: # if this is still 0, then throw an error! raise ValueError('This does not appear to be a valid file format ' 'that can be read by OSSM or Shio to get ' 'tide information') # look for following keywords to determine if it is a Shio or OSSM file shio_file = ['[StationInfo]', 'Type=', 'Name=', 'Latitude='] if all([shio_file[i] == (lines[i])[:len(shio_file[i])] for i in range(4)]): return CyShioTime(filename) elif len(string.split(lines[3], ',')) == 7: # maybe log / display a warning that v=0 for tide file and will be # ignored # if float( string.split(lines[3],',')[-1]) != 0.0: return CyTimeseries(filename, file_format=tsformat('uv')) else: raise ValueError('This does not appear to be a valid file format ' 'that can be read by OSSM or Shio to get ' 'tide information')
class TestObjectSerialization: ''' Test all the serialization and deserialization methods that are available to the CyTimeseries object. ''' ossmT = CyTimeseries(filename=testdata['timeseries']['wind_ts'], file_format=ts_format.magnitude_direction) wm = CyWindMover() wm.set_ossm(ossmT) def test_repr(self): ''' Test that the repr method produces a string capable of reproducing the object. ''' # This works, but in order to properly eval the repr string, we need # the top level gnome module, as well as the numpy 'array' type in the # global name space. # So before we try it, we first need to import like this: import gnome from numpy import array new_wm = eval(repr(self.wm)) assert new_wm == self.wm assert repr(new_wm) == repr(self.wm)
def __init__(self): super(ConstantWindWithOSSM, self).__init__() time_val = np.empty((1, ), dtype=time_value_pair) time_val['time'] = 0 # should not matter time_val['value'] = const_wind self.ossm = CyTimeseries(timeseries=time_val) self.wm.set_ossm(self.ossm)
def test_timeseries(self): """ test setting the timeseries using timeseries property """ ossmT = CyTimeseries(timeseries=self.tval) t_val = ossmT.timeseries print "t_val before:", t_val["value"] # need to learn how to do the following in 1 line of code t_val["value"]["u"] += 2 t_val["value"]["v"] += 2 print "t_val after:", t_val["value"] ossmT.timeseries = t_val new_val = ossmT.timeseries tol = 1e-10 msg = "{0} is not within a tolerance of " "{1}".format("get_time_value", tol) np.testing.assert_allclose(t_val["time"], new_val["time"], tol, tol, msg, 0) np.testing.assert_allclose(t_val["value"]["u"], new_val["value"]["u"], tol, tol, msg, 0) np.testing.assert_allclose(t_val["value"]["v"], new_val["value"]["v"], tol, tol, msg, 0)
def __init__(self, wind=None, timeseries=None, past_hours_to_average=3, **kwargs): """ Initializes a running average object from a wind and past hours to average If no wind is given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) units = 'mps' (note: probably should be an error) All other keywords are optional. Optional parameters (kwargs): :param past_hours_to_average: default is 3 """ self.units = 'mps' self.format = 'uv' self._past_hours_to_average = past_hours_to_average self.wind = wind if (wind is None and timeseries is None): mvg_timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) moving_timeseries = self._convert_to_time_value_pair(mvg_timeseries) else: if wind is not None: moving_timeseries = wind.ossm.create_running_average(self._past_hours_to_average) else: self.wind = Wind(timeseries, units='mps', format='uv') moving_timeseries = self.wind.ossm.create_running_average(self._past_hours_to_average) # print "moving_timeseries" # print moving_timeseries self.ossm = CyTimeseries(timeseries=moving_timeseries) super(RunningAverage, self).__init__(**kwargs)
def test_init_from_timeseries(self): """ Sets the time series in OSSMTimeValue_c equal to the externally supplied numpy array containing time_value_pair data It then reads it back to make sure data was set correctly """ ossm = CyTimeseries(timeseries=self.tval) t_val = ossm.timeseries msg = ('{0}().get_time_value() did not return expected ' 'numpy array').format(ossm.__class__.__name__) np.testing.assert_array_equal(t_val, self.tval, msg, 0) assert ossm.user_units == 'undefined' # meters/second assert ossm.station_location is None assert ossm.station is None assert ossm.filename is None assert ossm.scale_factor == 1.0
class TestVariableWind: """ Uses OSSMTimeValue_c to define a variable wind - variable wind has 'v' component, so movement should only be in 'lat' direction of world point Leave as a class as we may add more methods to it for testing """ wm = CyWindMover() cm = cy_fixtures.CyTestMove() delta = np.zeros((cm.num_le, ), dtype=world_point) time_val = np.zeros((2, ), dtype=time_value_pair) (time_val['time'])[:] = np.add([0, 3600], cm.model_time) # after 1 hour (time_val['value']['v'])[:] = [100, 200] # CyTimeseries needs the same scope as CyWindMover because CyWindMover # uses the C++ pointer defined in CyTimeseries.time_dep. This must be defined # for the scope of CyWindMover ossm = CyTimeseries(timeseries=time_val) wm.set_ossm(ossm) def test_move(self): for x in range(0, 3): vary_time = x * 1800 self.wm.prepare_for_model_step(self.cm.model_time + vary_time, self.cm.time_step) self.wm.get_move(self.cm.model_time + vary_time, self.cm.time_step, self.cm.ref, self.delta, self.cm.windage, self.cm.status, spill_type.forecast) print self.delta assert np.all(self.delta['lat'] != 0) assert np.all(self.delta['long'] == 0) assert np.all(self.delta['z'] == 0)
def __init__(self, wind=None, timeseries=None, past_hours_to_average=3, **kwargs): """ Initializes a running average object from a wind and past hours to average If no wind is given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) units = 'mps' (note: probably should be an error) All other keywords are optional. Optional parameters (kwargs): :param past_hours_to_average: default is 3 """ self.units = 'mps' self.format = 'uv' self._past_hours_to_average = past_hours_to_average self.wind = wind if (wind is None and timeseries is None): mvg_timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) moving_ts = self._convert_to_time_value_pair(mvg_timeseries) elif wind is not None: moving_ts = (wind.ossm .create_running_average(self._past_hours_to_average)) else: self.wind = Wind(timeseries, units='mps', coord_sys='uv') moving_ts = (self.wind.ossm .create_running_average(self._past_hours_to_average)) self.ossm = CyTimeseries(timeseries=moving_ts) super(RunningAverage, self).__init__(**kwargs)
class RunningAverage(Environment): ''' Defines a running average time series for a wind or tide ''' _schema = RunningAverageSchema def __init__(self, wind=None, timeseries=None, past_hours_to_average=3, **kwargs): """ Initializes a running average object from a wind and past hours to average If no wind is given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) units = 'mps' (note: probably should be an error) All other keywords are optional. Optional parameters (kwargs): :param past_hours_to_average: default is 3 """ self.units = 'mps' self.format = 'uv' self._past_hours_to_average = past_hours_to_average self.wind = wind if (wind is None and timeseries is None): mvg_timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) moving_ts = self._convert_to_time_value_pair(mvg_timeseries) elif wind is not None: moving_ts = (wind.ossm .create_running_average(self._past_hours_to_average)) else: self.wind = Wind(timeseries, units='mps', coord_sys='uv') moving_ts = (self.wind.ossm .create_running_average(self._past_hours_to_average)) self.ossm = CyTimeseries(timeseries=moving_ts) super(RunningAverage, self).__init__(**kwargs) def __repr__(self): self_ts = self.timeseries.__repr__() return ('{0.__class__.__module__}.{0.__class__.__name__}(' 'timeseries={1})' .format(self, self_ts)) def __str__(self): return ("Running Average ( " "timeseries=RunningAverage.get_timeseries('uv'), " "format='uv')") @property def past_hours_to_average(self): return self._past_hours_to_average @past_hours_to_average.setter def past_hours_to_average(self, value): """ How many hours for running average """ # may want a check on value self._past_hours_to_average = value @property def timeseries(self): return self.get_timeseries() def _convert_to_time_value_pair(self, datetime_value_2d): ''' fmt datetime_value_2d so it is a numpy array with dtype=basic_types.time_value_pair as the C++ code expects ''' # following fails for 0-d objects so make sure we have a 1-D array # to work with datetime_value_2d = np.asarray(datetime_value_2d, dtype=basic_types.datetime_value_2d) if datetime_value_2d.shape == (): datetime_value_2d = np.asarray([datetime_value_2d], dtype=basic_types.datetime_value_2d) timeval = to_time_value_pair(datetime_value_2d, "uv") return timeval def get_timeseries(self, datetime=None): """ Returns the timeseries in the requested format. If datetime=None, then the original timeseries that was entered is returned. If datetime is a list containing datetime objects, then the wind value for each of those date times is determined by the underlying CyOSSMTime object and the timeseries is returned. The output format is defined by the strings 'r-theta', 'uv' :param datetime: [optional] datetime object or list of datetime objects for which the value is desired :type datetime: datetime object :returns: numpy array containing dtype=basic_types.datetime_value_2d. Contains user specified datetime and the corresponding values in 'm/s' and 'uv' format """ if datetime is None: datetimeval = to_datetime_value_2d(self.ossm.timeseries, 'uv') else: datetime = np.asarray(datetime, dtype='datetime64[s]').reshape(-1) timeval = np.zeros((len(datetime), ), dtype=basic_types.time_value_pair) timeval['time'] = date_to_sec(datetime) timeval['value'] = self.ossm.get_time_value(timeval['time']) datetimeval = to_datetime_value_2d(timeval, 'uv') return datetimeval def prepare_for_model_run(self, model_time): """ Make sure we are up to date with the referenced time series """ if self.wind is None: msg = "wind object not defined for WindMover" raise ReferencedObjectNotSet(msg) model_time = date_to_sec(model_time) self.create_running_average_timeseries(self._past_hours_to_average, model_time) def prepare_for_model_step(self, model_time): """ Make sure we are up to date with the referenced time series """ model_time = date_to_sec(model_time) if self.ossm.check_time_in_range(model_time): return self.create_running_average_timeseries(self._past_hours_to_average, model_time) def create_running_average_timeseries(self, past_hours_to_average, model_time=0): """ Creates the timeseries of the RunningAverage object :param past_hours_to_average: amount of data to use in the averaging """ # first get the time series from the C++ function # self.timeseries = wind.ossm.create_running_average(past_hours) # do we need to dispose of old one here? moving_timeseries = (self.wind.ossm .create_running_average(past_hours_to_average, model_time)) # here should set the timeseries since the CyOSSMTime # should already exist self.ossm.timeseries = moving_timeseries def get_value(self, time): ''' Return the value at specified time and location. Timeseries are independent of location; however, a gridded datafile may require location so this interface may get refactored if it needs to support different types of data. It assumes the data in SI units (m/s) and 'uv' format .. note:: It invokes get_timeseries(..) function ''' if self.ossm.timeseries is None: self.create_running_average_timeseries(self.past_hours_to_average) # if check on time range here: # self.create_running_average_timeseries(self.past_hours, # 'm/s', 'uv') data = self.get_timeseries(time) return tuple(data[0]['value'])
class TestVariableWind: """ Uses OSSMTimeValue_c to define a variable wind - variable wind has 'v' component, so movement should only be in 'lat' direction of world point Leave as a class as we may add more methods to it for testing """ wm = CyWindMover() cm = cy_fixtures.CyTestMove() delta = np.zeros((cm.num_le, ), dtype=world_point) time_val = np.zeros((2, ), dtype=time_value_pair) (time_val['time'])[:] = np.add([0, 3600], cm.model_time) # after 1 hour (time_val['value']['v'])[:] = [100, 200] # CyTimeseries needs the same scope as CyWindMover because CyWindMover # uses the C++ pointer defined in CyTimeseries.time_dep. # This must be defined for the scope of CyWindMover ossm = CyTimeseries(timeseries=time_val) wm.set_ossm(ossm) def test_move(self): for x in range(0, 3): vary_time = x * 1800 self.wm.prepare_for_model_step(self.cm.model_time + vary_time, self.cm.time_step) self.wm.get_move(self.cm.model_time + vary_time, self.cm.time_step, self.cm.ref, self.delta, self.cm.windage, self.cm.status, spill_type.forecast) print self.delta assert np.all(self.delta['lat'] != 0) assert np.all(self.delta['long'] == 0) assert np.all(self.delta['z'] == 0) def test_move_out_of_bounds(self): ''' Our wind mover should fail in the prepare_for_model_step() function if our wind time series is out of bounds with respect to the model time we are preparing for, unless our wind time series is configured to extrapolate wind values. ''' # setup a time series that's out of range of our model time time_val = np.zeros((2, ), dtype=time_value_pair) (time_val['time'])[:] = np.add([3600, 7200], self.cm.model_time) (time_val['value']['v'])[:] = [100, 200] # extrapolation should be off by default ossm = CyTimeseries(timeseries=time_val) self.wm.set_ossm(ossm) # this should fail because our time series is not set to extrapolate with raises(OSError): self.wm.prepare_for_model_step(self.cm.model_time, self.cm.time_step) ossm = CyTimeseries(timeseries=time_val, extrapolation_is_allowed=True) self.wm.set_ossm(ossm) # We set our time series to extrapolate, so this should pass self.wm.prepare_for_model_step(self.cm.model_time, self.cm.time_step) # clean up our time series self.wm.set_ossm(self.ossm)
class RunningAverage(Environment): ''' Defines a running average time series for a wind or tide ''' _schema = RunningAverageSchema def __init__(self, wind=None, timeseries=None, past_hours_to_average=3, **kwargs): """ Initializes a running average object from a wind and past hours to average If no wind is given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) units = 'mps' (note: probably should be an error) All other keywords are optional. Optional parameters (kwargs): :param past_hours_to_average: default is 3 """ self.units = 'mps' self.format = 'uv' self._past_hours_to_average = past_hours_to_average self.wind = wind if (wind is None and timeseries is None): mvg_timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) moving_ts = self._convert_to_time_value_pair(mvg_timeseries) elif wind is not None: moving_ts = (wind.ossm.create_running_average( self._past_hours_to_average)) else: self.wind = Wind(timeseries, units='mps', format='uv') moving_ts = (self.wind.ossm.create_running_average( self._past_hours_to_average)) self.ossm = CyTimeseries(timeseries=moving_ts) super(RunningAverage, self).__init__(**kwargs) def __repr__(self): self_ts = self.timeseries.__repr__() return ('{0.__class__.__module__}.{0.__class__.__name__}(' 'timeseries={1})'.format(self, self_ts)) def __str__(self): return ("Running Average ( " "timeseries=RunningAverage.get_timeseries('uv'), " "format='uv')") @property def past_hours_to_average(self): return self._past_hours_to_average @past_hours_to_average.setter def past_hours_to_average(self, value): """ How many hours for running average """ # may want a check on value self._past_hours_to_average = value @property def timeseries(self): return self.get_timeseries() def _convert_to_time_value_pair(self, datetime_value_2d): ''' fmt datetime_value_2d so it is a numpy array with dtype=basic_types.time_value_pair as the C++ code expects ''' # following fails for 0-d objects so make sure we have a 1-D array # to work with datetime_value_2d = np.asarray(datetime_value_2d, dtype=basic_types.datetime_value_2d) if datetime_value_2d.shape == (): datetime_value_2d = np.asarray([datetime_value_2d], dtype=basic_types.datetime_value_2d) timeval = to_time_value_pair(datetime_value_2d, "uv") return timeval def get_timeseries(self, datetime=None): """ Returns the timeseries in the requested format. If datetime=None, then the original timeseries that was entered is returned. If datetime is a list containing datetime objects, then the wind value for each of those date times is determined by the underlying CyOSSMTime object and the timeseries is returned. The output format is defined by the strings 'r-theta', 'uv' :param datetime: [optional] datetime object or list of datetime objects for which the value is desired :type datetime: datetime object :returns: numpy array containing dtype=basic_types.datetime_value_2d. Contains user specified datetime and the corresponding values in 'm/s' and 'uv' format """ if datetime is None: datetimeval = to_datetime_value_2d(self.ossm.timeseries, 'uv') else: datetime = np.asarray(datetime, dtype='datetime64[s]').reshape(-1) timeval = np.zeros((len(datetime), ), dtype=basic_types.time_value_pair) timeval['time'] = date_to_sec(datetime) timeval['value'] = self.ossm.get_time_value(timeval['time']) datetimeval = to_datetime_value_2d(timeval, 'uv') return datetimeval def prepare_for_model_run(self, model_time): """ Make sure we are up to date with the referenced time series """ if self.wind is None: msg = "wind object not defined for WindMover" raise ReferencedObjectNotSet(msg) model_time = date_to_sec(model_time) self.create_running_average_timeseries(self._past_hours_to_average, model_time) def prepare_for_model_step(self, model_time): """ Make sure we are up to date with the referenced time series """ model_time = date_to_sec(model_time) if self.ossm.check_time_in_range(model_time): return self.create_running_average_timeseries(self._past_hours_to_average, model_time) def create_running_average_timeseries(self, past_hours_to_average, model_time=0): """ Creates the timeseries of the RunningAverage object :param past_hours_to_average: amount of data to use in the averaging """ # first get the time series from the C++ function # self.timeseries = wind.ossm.create_running_average(past_hours) # do we need to dispose of old one here? moving_timeseries = (self.wind.ossm.create_running_average( past_hours_to_average, model_time)) # here should set the timeseries since the CyOSSMTime # should already exist self.ossm.timeseries = moving_timeseries def get_value(self, time): ''' Return the value at specified time and location. Timeseries are independent of location; however, a gridded datafile may require location so this interface may get refactored if it needs to support different types of data. It assumes the data in SI units (m/s) and 'uv' format .. note:: It invokes get_timeseries(..) function ''' if self.ossm.timeseries is None: self.create_running_average_timeseries(self.past_hours_to_average) # if check on time range here: # self.create_running_average_timeseries(self.past_hours, # 'm/s', 'uv') data = self.get_timeseries(time) return tuple(data[0]['value'])
class Timeseries(GnomeId): _schema = ObjTypeSchema def __init__(self, timeseries=None, filename=None, coord_sys='uv', extrapolation_is_allowed=False, **kwargs): """ Initializes a timeseries object from either a timeseries or datafile containing the timeseries. If both timeseries and file are given, it will read data from the file If neither are given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) If user provides timeseries, the default coordinate system is 'uv'. The C++ stores the data in 'uv' coordinates - transformations are done in this Python code (set_timeseries(), get_timeseries()). C++ code only transforms the data from 'r-theta' to 'uv' coordinates if data is read from file. And this happens during initialization because C++ stores data in 'uv' coordinates internally. Units option are not included - let derived classes manage units since the units for CyTimeseries (OSSMTimeValue_c) are limited. No unit conversion is performed when get_timeseries, set_timeseries is invoked. It does, however convert between 'uv' and 'r-theta' depending on the coordinate system specified. Choose coord_sys='uv' if no transformation is desired. .. note:: For the Wind datafiles, the units will get read from the file. These are stored in ossm.user_units. It would be ideal to remove units and unit conversion from here, but can't completely do away with it since C++ file reading uses/sets it. But, managing units is responsibility of derived objects. All other keywords are optional :param timeseries: numpy array containing time_value_pair :type timeseries: numpy.ndarray containing basic_types.datetime_value_2d or basic_types.datetime_value_1d. It gets converted to an array containing basic_types.time_value_pair datatype since that's what the C++ code expects :param filename: path to a timeseries file from which to read data. Datafile must contain either a 3 line or a 5 line header with following info: 1. Station Name: name of the station as a string 2. (long, lat, z): station location as tuple containing floats 3. units: for wind this is knots, meteres per second or miles per hour. For datafile containing something other than velocity, this should be 'undefined' Optional parameters (kwargs): :param coord_sys: (Optional) default timeseries coordinate system is magnitude direction: 'r-theta' :type coord_sys: string 'r-theta' or 'uv'. Default is 'r-theta'. Converts string to integer defined by gnome.basic_types.ts_format.* """ super(Timeseries, self).__init__(**kwargs) if (timeseries is None and filename is None): timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) self._filename = filename if filename is None: # will raise an Exception if it fails self._check_timeseries(timeseries) datetime_value_2d = self._xform_input_timeseries(timeseries) time_value_pair = to_time_value_pair(datetime_value_2d, coord_sys) self.ossm = CyTimeseries(timeseries=time_value_pair) else: ts_format = tsformat(coord_sys) self.ossm = CyTimeseries(filename=self._filename, file_format=ts_format) self.extrapolation_is_allowed = extrapolation_is_allowed def _check_timeseries(self, timeseries): """ Run some checks to make sure timeseries is valid. - We accept numpy arrays - We will also accept a list of timeseries values of the form (datetime(...), (N, N)) - we will also accept a constant single timeseries value. """ if not isinstance(timeseries, np.ndarray): if self._is_timeseries_value(timeseries): return True else: for i in timeseries: if not self._is_timeseries_value(i): raise TimeseriesError('value: {} ' 'is not a timeseries value' .format(i)) return True if not self._timeseries_is_ascending(timeseries): self.logger.error('{0} - timeseries are not in ascending order. ' 'The datetime values in the array must be in ' 'ascending order' .format(self._pid)) raise TimeseriesError("timeseries is not in ascending order") return True def __len__(self): """ length is the number of data points in the timeseries """ return self.ossm.get_num_values() def __eq__(self, o): t1 = super(Timeseries, self).__eq__(o) return t1 and hasattr(self, 'ossm') and np.all(self.ossm.timeseries == o.ossm.timeseries) def get_start_time(self): """ :this will be the real_data_start time (seconds). """ return self.ossm.get_start_time() def get_end_time(self): """ :this will be the real_data_stop time (seconds). """ return (self.ossm.get_end_time()) def _is_timeseries_value(self, value): if not isinstance(value, (list, tuple)): return False if len(value) != 2: return False if not isinstance(value[0], (datetime.datetime, np.datetime64)): return False if len(value[1]) not in (1, 2): return False return True def _timeseries_is_ascending(self, timeseries): """ Check if values are monotonically increasing This should catch both out of order and duplicate values. """ # FixMe: does this ever have to work for a true scalar?? if timeseries.shape == () or timeseries.shape == (1,): # scalar or single value -- must be OK return True if np.any(np.diff(timeseries['time']) <= np.timedelta64(0, 's')): return False else: return True def _xform_input_timeseries(self, timeseries): ''' Ensure input data is numpy array with correct dtype and check timeseries doesn't have invalid data Derived classes can use this before updating timeseries, prior to converting units. ''' # following fails for 0-d objects so make sure we have a 1-D array # to work with timeseries = np.asarray(timeseries, dtype=basic_types.datetime_value_2d) if timeseries.shape == (): timeseries = np.asarray([timeseries], dtype=basic_types.datetime_value_2d) return timeseries def __str__(self): return '{0.__module__}.{0.__class__.__name__}'.format(self) @property def filename(self): return self._filename @property def extrapolation_is_allowed(self): return self.ossm.extrapolation_is_allowed @extrapolation_is_allowed.setter def extrapolation_is_allowed(self, value): self.ossm.extrapolation_is_allowed = value def get_timeseries(self, datetime=None, coord_sys='uv'): """ Returns the timeseries in requested coordinate system. If datetime=None, then the original timeseries that was entered is returned. If datetime is a list containing datetime objects, then the value for each of those date times is determined by the underlying C++ object and the timeseries is returned. The output coordinate system is defined by the strings 'r-theta', 'uv' :param datetime: [optional] datetime object or list of datetime objects for which the value is desired :type datetime: datetime object :param coord_sys: output coordinate system for the times series: either 'r-theta' or 'uv' :type coord_sys: either string or integer value defined by basic_types.ts_format.* (see cy_basic_types.pyx) :returns: numpy array containing dtype=basic_types.datetime_value_2d. Contains user specified datetime and the corresponding values in user specified ts_format """ if datetime is None: datetimeval = to_datetime_value_2d(self.ossm.timeseries, coord_sys) else: datetime = np.asarray(datetime, dtype='datetime64[s]').reshape(-1) timeval = np.zeros((len(datetime), ), dtype=basic_types.time_value_pair) timeval['time'] = date_to_sec(datetime) (timeval['value'], err) = self.ossm.get_time_value(timeval['time']) if err != 0: msg = ('No available data in the time interval that is being ' 'modeled\n' '\tModel time: {}\n' '\tMover: {} of type {}\n' .format(datetime, self.name, self.__class__)) self.logger.error(msg) raise RuntimeError(msg) datetimeval = to_datetime_value_2d(timeval, coord_sys) return np.copy(datetimeval) def set_timeseries(self, datetime_value_2d, coord_sys='uv'): """ Sets the timeseries to the new value given by a numpy array. The coordinate system for the input data defaults to basic_types.format.magnitude_direction but can be changed by the user. Assumes timeseries is valid so _check_timeseries has been invoked and any unit conversions are done. This function simply converts datetime_value_2d to time_value_pair and updates the data in underlying cython/C++ object :param datetime_value_2d: timeseries of wind data defined in a numpy array :type datetime_value_2d: numpy array of dtype basic_types.datetime_value_2d :param coord_sys: output coordinate system for the times series, as defined by basic_types.ts_format. :type coord_sys: either string or integer value defined by basic_types.ts_format.* (see cy_basic_types.pyx) """ datetime_value_2d = self._xform_input_timeseries(datetime_value_2d) timeval = to_time_value_pair(datetime_value_2d, coord_sys) self.ossm.timeseries = timeval
class RunningAverage(Environment, Serializable): ''' Defines a running average time series for a wind or tide ''' _update = [] # used to create new obj or as readonly parameter _create = [] _create.extend(_update) _state = copy.deepcopy(Environment._state) _state += [Field('wind', save=True, update=True, save_reference=True)] _state.add(save=_create, update=_update) _schema = RunningAverageSchema # _state.add_field([serializable.Field('timeseries', save=True, # update=True) # ]) # _state['name'].test_for_eq = False # list of valid velocity units for timeseries # valid_vel_units = _valid_units('Velocity') def __init__(self, wind=None, timeseries=None, past_hours_to_average=3, **kwargs): """ Initializes a running average object from a wind and past hours to average If no wind is given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) units = 'mps' (note: probably should be an error) All other keywords are optional. Optional parameters (kwargs): :param past_hours_to_average: default is 3 """ self.units = 'mps' self.format = 'uv' self._past_hours_to_average = past_hours_to_average self.wind = wind if (wind is None and timeseries is None): mvg_timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) moving_timeseries = self._convert_to_time_value_pair(mvg_timeseries) else: if wind is not None: moving_timeseries = wind.ossm.create_running_average(self._past_hours_to_average) else: self.wind = Wind(timeseries, units='mps', format='uv') moving_timeseries = self.wind.ossm.create_running_average(self._past_hours_to_average) # print "moving_timeseries" # print moving_timeseries self.ossm = CyTimeseries(timeseries=moving_timeseries) super(RunningAverage, self).__init__(**kwargs) def __repr__(self): self_ts = self.timeseries.__repr__() return ('{0.__class__.__module__}.{0.__class__.__name__}(' 'timeseries={1}' ')').format(self, self_ts) def __str__(self): return ("Running Average ( " "timeseries=RunningAverage.get_timeseries('uv'), " "format='uv')") @property def past_hours_to_average(self): return self._past_hours_to_average @past_hours_to_average.setter def past_hours_to_average(self, value): """ How many hours for running average """ # may want a check on value self._past_hours_to_average = value @property def timeseries(self): return self.get_timeseries() def _convert_to_time_value_pair(self, datetime_value_2d): ''' fmt datetime_value_2d so it is a numpy array with dtype=basic_types.time_value_pair as the C++ code expects ''' # following fails for 0-d objects so make sure we have a 1-D array # to work with datetime_value_2d = np.asarray(datetime_value_2d, dtype=basic_types.datetime_value_2d) if datetime_value_2d.shape == (): datetime_value_2d = np.asarray([datetime_value_2d], dtype=basic_types.datetime_value_2d) # self._check_units(units) # self._check_timeseries(datetime_value_2d, units) # datetime_value_2d['value'] = \ # self._convert_units(datetime_value_2d['value'], # fmt, units, 'meter per second') timeval = to_time_value_pair(datetime_value_2d, "uv") return timeval def get_timeseries(self, datetime=None): """ Returns the timeseries in the requested format. If datetime=None, then the original timeseries that was entered is returned. If datetime is a list containing datetime objects, then the wind value for each of those date times is determined by the underlying CyOSSMTime object and the timeseries is returned. The output format is defined by the strings 'r-theta', 'uv' :param datetime: [optional] datetime object or list of datetime objects for which the value is desired :type datetime: datetime object :returns: numpy array containing dtype=basic_types.datetime_value_2d. Contains user specified datetime and the corresponding values in 'm/s' and 'uv' format """ if datetime is None: datetimeval = to_datetime_value_2d(self.ossm.timeseries, 'uv') else: datetime = np.asarray(datetime, dtype='datetime64[s]').reshape(-1) timeval = np.zeros((len(datetime), ), dtype=basic_types.time_value_pair) timeval['time'] = date_to_sec(datetime) timeval['value'] = self.ossm.get_time_value(timeval['time']) datetimeval = to_datetime_value_2d(timeval, 'uv') return datetimeval def prepare_for_model_run(self, model_time): """ Make sure we are up to date with the referenced time series """ if self.wind is None: msg = "wind object not defined for WindMover" raise ReferencedObjectNotSet(msg) model_time = date_to_sec(model_time) self.create_running_average_timeseries(self._past_hours_to_average, model_time) def prepare_for_model_step(self, model_time): """ Make sure we are up to date with the referenced time series """ model_time = date_to_sec(model_time) if self.ossm.check_time_in_range(model_time): return else: if self.wind.ossm.check_time_in_range(model_time): # there is wind data for this time so create # a new running average self.create_running_average_timeseries(self._past_hours_to_average, model_time) self.create_running_average_timeseries(self._past_hours_to_average, model_time) def create_running_average_timeseries(self, past_hours_to_average, model_time=0): """ Creates the timeseries of the RunningAverage object :param past_hours_to_average: amount of data to use in the averaging """ # first get the time series from the C++ function # self.timeseries = wind.ossm.create_running_average(past_hours) # do we need to dispose of old one here? moving_timeseries = self.wind.ossm.create_running_average(past_hours_to_average, model_time) # here should set the timeseries since the CyOSSMTime # should already exist self.ossm.timeseries = moving_timeseries # self.ossm = CyOSSMTime(timeseries=moving_timeseries) def get_value(self, time): ''' Return the value at specified time and location. Timeseries are independent of location; however, a gridded datafile may require location so this interface may get refactored if it needs to support different types of data. It assumes the data in SI units (m/s) and 'uv' format .. note:: It invokes get_timeseries(..) function ''' if self.ossm.timeseries is None: self.create_running_average_timeseries(self.past_hours_to_average) # if check on time range here: # self.create_running_average_timeseries(self.past_hours, # 'm/s', 'uv') data = self.get_timeseries(time) return tuple(data[0]['value']) def serialize(self, json_='webapi'): """ Since 'wind' property is saved as references in save file need to add appropriate node to WindMover schema for 'webapi' """ toserial = self.to_serialize(json_) schema = self.__class__._schema() if json_ == 'webapi': if self.wind: # add wind schema schema.add(WindSchema(name='wind')) serial = schema.serialize(toserial) return serial @classmethod def deserialize(cls, json_): """ append correct schema for wind object """ schema = cls._schema() if 'wind' in json_: schema.add(WindSchema(name='wind')) _to_dict = schema.deserialize(json_) return _to_dict
class Timeseries(GnomeId): def __init__(self, timeseries=None, filename=None, format='uv'): """ Initializes a timeseries object from either a timeseries or datafile containing the timeseries. If both timeseries and file are given, it will read data from the file If neither are given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) If user provides timeseries, the default format is 'uv'. The C++ stores the data in 'uv' format - transformations are done in this Python code (set_timeseries(), get_timeseries()). C++ code only transforms the data from 'r-theta' to 'uv' format if data is read from file. And this happens during initialization because C++ stores data in 'uv' format internally. Units option are not included - let derived classes manage units since the units for CyTimeseries (OSSMTimeValue_c) are limited. No unit conversion is performed when get_timeseries, set_timeseries is invoked. It does, however convert between 'uv' and 'r-theta' depending on format specified. Choose format='uv' if no transformation is desired. .. note:: For the Wind datafiles, the units will get read from the file. These are stored in ossm.user_units. It would be ideal to remove units and unit conversion from here, but can't completely do away with it since C++ file reading uses/sets it. But, managing units is responsibility of derived objects. All other keywords are optional :param timeseries: numpy array containing time_value_pair :type timeseries: numpy.ndarray containing basic_types.datetime_value_2d or basic_types.datetime_value_1d. It gets converted to an array containging basic_types.time_value_pair datatype since that's what the C++ code expects :param filename: path to a timeseries file from which to read data. Datafile must contain either a 3 line or a 5 line header with following info: 1. Station Name: name of the station as a string 2. (long, lat, z): station location as tuple containing floats 3. units: for wind this is knots, meteres per second or miles per hour. For datafile containing something other than velocity, this should be 'undefined' Optional parameters (kwargs): :param format: (Optional) default timeseries format is magnitude direction: 'r-theta' :type format: string 'r-theta' or 'uv'. Default is 'r-theta'. Converts string to integer defined by gnome.basic_types.ts_format.* TODO: 'format' is a python builtin keyword. We should not use it as an argument name """ if (timeseries is None and filename is None): timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) self._filename = filename if filename is None: if self._check_timeseries(timeseries): datetime_value_2d = self._xform_input_timeseries(timeseries) time_value_pair = to_time_value_pair(datetime_value_2d, format) self.ossm = CyTimeseries(timeseries=time_value_pair) else: raise ValueError('Bad timeseries as input') else: ts_format = tsformat(format) self.ossm = CyTimeseries(filename=self._filename, file_format=ts_format) def _check_timeseries(self, timeseries): ''' Run some checks to make sure timeseries is valid. - We accept numpy arrays - We will also accept a list of timeseries values of the form (datetime(...), (N, N)) - we will also accept a constant single timeseries value. Also, make the resolution to minutes as opposed to seconds ''' if not isinstance(timeseries, np.ndarray): if self._is_timeseries_value(timeseries): return True else: for i in timeseries: if not self._is_timeseries_value(i): return False return True if not self._timeseries_is_ascending(timeseries): self.logger.error('{0} - timeseries are not in ascending order. ' 'The datetime values in the array must be in ' 'ascending order' .format(self._pid)) return False if self._timeseries_has_duplicates(timeseries): self.logger.error('{0} - timeseries must contain unique ' 'time entries' .format(self._pid)) return False return True def _is_timeseries_value(self, value): if not isinstance(value, (list, tuple)): return False if len(value) != 2: return False if not isinstance(value[0], (datetime.datetime, np.datetime64)): return False if len(value[1]) not in (1, 2): return False return True def _timeseries_is_ascending(self, timeseries): # we need to have a valid shape to sort if timeseries.shape == (): timeseries = np.asarray([timeseries], dtype=basic_types.datetime_value_2d) if np.any(timeseries['time'][np.argsort(timeseries['time'])] != timeseries['time']): return False else: return True def _timeseries_has_duplicates(self, timeseries): # we need to have a valid shape to sort if timeseries.shape == (): timeseries = np.asarray([timeseries], dtype=basic_types.datetime_value_2d) unique = np.unique(timeseries['time']) if len(unique) != len(timeseries['time']): return True else: return False def _xform_input_timeseries(self, timeseries): ''' Ensure input data is numpy array with correct dtype and check timeseries doesn't have invalid data Derived classes can use this before updating timeseries, prior to converting units. ''' # following fails for 0-d objects so make sure we have a 1-D array # to work with timeseries = np.asarray(timeseries, dtype=basic_types.datetime_value_2d) if timeseries.shape == (): timeseries = np.asarray([timeseries], dtype=basic_types.datetime_value_2d) # Our datetime granularity is in minutes, so zero out the seconds. for ix, tm in enumerate(timeseries['time'].astype(datetime.datetime)): timeseries['time'][ix] = tm.replace(second=0) return timeseries def __str__(self): return '{0.__module__}.{0.__class__.__name__}'.format(self) @property def filename(self): return self._filename def get_timeseries(self, datetime=None, format='uv'): """ Returns the timeseries in requested format. If datetime=None, then the original timeseries that was entered is returned. If datetime is a list containing datetime objects, then the value for each of those date times is determined by the underlying C++ object and the timeseries is returned. The output format is defined by the strings 'r-theta', 'uv' :param datetime: [optional] datetime object or list of datetime objects for which the value is desired :type datetime: datetime object :param format: output format for the times series: either 'r-theta' or 'uv' :type format: either string or integer value defined by basic_types.ts_format.* (see cy_basic_types.pyx) :returns: numpy array containing dtype=basic_types.datetime_value_2d. Contains user specified datetime and the corresponding values in user specified ts_format """ if datetime is None: datetimeval = to_datetime_value_2d(self.ossm.timeseries, format) else: datetime = np.asarray(datetime, dtype='datetime64[s]').reshape(-1) timeval = np.zeros((len(datetime), ), dtype=basic_types.time_value_pair) timeval['time'] = date_to_sec(datetime) timeval['value'] = self.ossm.get_time_value(timeval['time']) datetimeval = to_datetime_value_2d(timeval, format) return datetimeval def set_timeseries(self, datetime_value_2d, format='uv'): """ Sets the timeseries to the new value given by a numpy array. The format for the input data defaults to basic_types.format.magnitude_direction but can be changed by the user Assumes timeseries is valid so _check_timeseries has been invoked and any unit conversions are done. This function simply converts datetime_value_2d to time_value_pair and updates the data in underlying cython/C++ object :param datetime_value_2d: timeseries of wind data defined in a numpy array :type datetime_value_2d: numpy array of dtype basic_types.datetime_value_2d :param format: output format for the times series; as defined by basic_types.format. :type format: either string or integer value defined by basic_types.format.* (see cy_basic_types.pyx) """ datetime_value_2d = self._xform_input_timeseries(datetime_value_2d) timeval = to_time_value_pair(datetime_value_2d, format) self.ossm.timeseries = timeval def __eq__(self, other): ''' only checks the timeseries data is equal in (m/s), in 'uv' format filename is irrelevant after data is loaded checks self.get_timeseries() == other.get_timeseries() Duck typing check - it does not expect type(self) == type(other) ''' self_ts = self.get_timeseries() other_ts = other.get_timeseries() if not np.all(self_ts['time'] == other_ts['time']): return False if not np.allclose(self_ts['value'], other_ts['value']): return False return True def __ne__(self, other): return not self == other
def __init__(self, timeseries=None, filename=None, format='uv'): """ Initializes a timeseries object from either a timeseries or datafile containing the timeseries. If both timeseries and file are given, it will read data from the file If neither are given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) If user provides timeseries, the default format is 'uv'. The C++ stores the data in 'uv' format - transformations are done in this Python code (set_timeseries(), get_timeseries()). C++ code only transforms the data from 'r-theta' to 'uv' format if data is read from file. And this happens during initialization because C++ stores data in 'uv' format internally. Units option are not included - let derived classes manage units since the units for CyTimeseries (OSSMTimeValue_c) are limited. No unit conversion is performed when get_timeseries, set_timeseries is invoked. It does, however convert between 'uv' and 'r-theta' depending on format specified. Choose format='uv' if no transformation is desired. .. note:: For the Wind datafiles, the units will get read from the file. These are stored in ossm.user_units. It would be ideal to remove units and unit conversion from here, but can't completely do away with it since C++ file reading uses/sets it. But, managing units is responsibility of derived objects. All other keywords are optional :param timeseries: numpy array containing time_value_pair :type timeseries: numpy.ndarray containing basic_types.datetime_value_2d or basic_types.datetime_value_1d. It gets converted to an array containging basic_types.time_value_pair datatype since that's what the C++ code expects :param filename: path to a timeseries file from which to read data. Datafile must contain either a 3 line or a 5 line header with following info: 1. Station Name: name of the station as a string 2. (long, lat, z): station location as tuple containing floats 3. units: for wind this is knots, meteres per second or miles per hour. For datafile containing something other than velocity, this should be 'undefined' Optional parameters (kwargs): :param format: (Optional) default timeseries format is magnitude direction: 'r-theta' :type format: string 'r-theta' or 'uv'. Default is 'r-theta'. Converts string to integer defined by gnome.basic_types.ts_format.* TODO: 'format' is a python builtin keyword. We should not use it as an argument name """ if (timeseries is None and filename is None): timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) self._filename = filename if filename is None: if self._check_timeseries(timeseries): datetime_value_2d = self._xform_input_timeseries(timeseries) time_value_pair = to_time_value_pair(datetime_value_2d, format) self.ossm = CyTimeseries(timeseries=time_value_pair) else: raise ValueError('Bad timeseries as input') else: ts_format = tsformat(format) self.ossm = CyTimeseries(filename=self._filename, file_format=ts_format)
def __init__(self, timeseries=None, filename=None, format='uv'): """ Initializes a timeseries object from either a timeseries or datafile containing the timeseries. If both timeseries and file are given, it will read data from the file If neither are given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) If user provides timeseries, the default format is 'uv'. The C++ stores the data in 'uv' format - transformations are done in this Python code (set_timeseries(), get_timeseries()). C++ code only transforms the data from 'r-theta' to 'uv' format if data is read from file. And this happens during initialization because C++ stores data in 'uv' format internally. Units option are not included - let derived classes manage units since the units for CyTimeseries (OSSMTimeValue_c) are limited. No unit conversion is performed when get_timeseries, set_timeseries is invoked. It does, however convert between 'uv' and 'r-theta' depending on format specified. Choose format='uv' if no transformation is desired. .. note:: For the Wind datafiles, the units will get read from the file. These are stored in ossm.user_units. It would be ideal to remove units and unit conversion from here, but can't completely do away with it since C++ file reading uses/sets it. But, managing units is responsibility of derived objects. All other keywords are optional :param timeseries: numpy array containing time_value_pair :type timeseries: numpy.ndarray containing basic_types.datetime_value_2d or basic_types.datetime_value_1d. It gets converted to an array containging basic_types.time_value_pair datatype since that's what the C++ code expects :param filename: path to a timeseries file from which to read data. Datafile must contain either a 3 line or a 5 line header with following info: 1. Station Name: name of the station as a string 2. (long, lat, z): station location as tuple containing floats 3. units: for wind this is knots, meteres per second or miles per hour. For datafile containing something other than velocity, this should be 'undefined' Optional parameters (kwargs): :param format: (Optional) default timeseries format is magnitude direction: 'r-theta' :type format: string 'r-theta' or 'uv'. Default is 'r-theta'. Converts string to integer defined by gnome.basic_types.ts_format.* TODO: 'format' is a python builtin keyword. We should not use it as an argument name """ if (timeseries is None and filename is None): timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) self._filename = filename if filename is None: # will raise an Exception if it fails self._check_timeseries(timeseries) datetime_value_2d = self._xform_input_timeseries(timeseries) time_value_pair = to_time_value_pair(datetime_value_2d, format) self.ossm = CyTimeseries(timeseries=time_value_pair) else: ts_format = tsformat(format) self.ossm = CyTimeseries(filename=self._filename, file_format=ts_format)
class Timeseries(GnomeId): def __init__(self, timeseries=None, filename=None, format='uv'): """ Initializes a timeseries object from either a timeseries or datafile containing the timeseries. If both timeseries and file are given, it will read data from the file If neither are given, timeseries gets initialized as: timeseries = np.zeros((1,), dtype=basic_types.datetime_value_2d) If user provides timeseries, the default format is 'uv'. The C++ stores the data in 'uv' format - transformations are done in this Python code (set_timeseries(), get_timeseries()). C++ code only transforms the data from 'r-theta' to 'uv' format if data is read from file. And this happens during initialization because C++ stores data in 'uv' format internally. Units option are not included - let derived classes manage units since the units for CyTimeseries (OSSMTimeValue_c) are limited. No unit conversion is performed when get_timeseries, set_timeseries is invoked. It does, however convert between 'uv' and 'r-theta' depending on format specified. Choose format='uv' if no transformation is desired. .. note:: For the Wind datafiles, the units will get read from the file. These are stored in ossm.user_units. It would be ideal to remove units and unit conversion from here, but can't completely do away with it since C++ file reading uses/sets it. But, managing units is responsibility of derived objects. All other keywords are optional :param timeseries: numpy array containing time_value_pair :type timeseries: numpy.ndarray containing basic_types.datetime_value_2d or basic_types.datetime_value_1d. It gets converted to an array containging basic_types.time_value_pair datatype since that's what the C++ code expects :param filename: path to a timeseries file from which to read data. Datafile must contain either a 3 line or a 5 line header with following info: 1. Station Name: name of the station as a string 2. (long, lat, z): station location as tuple containing floats 3. units: for wind this is knots, meteres per second or miles per hour. For datafile containing something other than velocity, this should be 'undefined' Optional parameters (kwargs): :param format: (Optional) default timeseries format is magnitude direction: 'r-theta' :type format: string 'r-theta' or 'uv'. Default is 'r-theta'. Converts string to integer defined by gnome.basic_types.ts_format.* TODO: 'format' is a python builtin keyword. We should not use it as an argument name """ if (timeseries is None and filename is None): timeseries = np.array([(sec_to_date(zero_time()), [0.0, 0.0])], dtype=basic_types.datetime_value_2d) self._filename = filename if filename is None: # will raise an Exception if it fails self._check_timeseries(timeseries) datetime_value_2d = self._xform_input_timeseries(timeseries) time_value_pair = to_time_value_pair(datetime_value_2d, format) self.ossm = CyTimeseries(timeseries=time_value_pair) else: ts_format = tsformat(format) self.ossm = CyTimeseries(filename=self._filename, file_format=ts_format) def _check_timeseries(self, timeseries): """ Run some checks to make sure timeseries is valid. - We accept numpy arrays - We will also accept a list of timeseries values of the form (datetime(...), (N, N)) - we will also accept a constant single timeseries value. """ if not isinstance(timeseries, np.ndarray): if self._is_timeseries_value(timeseries): return True else: for i in timeseries: if not self._is_timeseries_value(i): raise TimeseriesError( 'value: {} ' 'is not a timeseries value'.format(i)) return True if not self._timeseries_is_ascending(timeseries): self.logger.error('{0} - timeseries are not in ascending order. ' 'The datetime values in the array must be in ' 'ascending order'.format(self._pid)) raise TimeseriesError("timeseries is not in ascending order") return True def get_start_time(self): """ :this will be the real_data_start time (seconds). """ return (self.ossm.get_start_time()) def get_end_time(self): """ :this will be the real_data_stop time (seconds). """ return (self.ossm.get_end_time()) def _is_timeseries_value(self, value): if not isinstance(value, (list, tuple)): return False if len(value) != 2: return False if not isinstance(value[0], (datetime.datetime, np.datetime64)): return False if len(value[1]) not in (1, 2): return False return True def _timeseries_is_ascending(self, timeseries): """ Check if values are monotonically increasing This should catch both out of order and duplicate values. """ # FixMe: does this ever have to work for a true scalar?? if timeseries.shape == () or timeseries.shape == (1, ): # scalar or single value -- must be OK return True if np.any(np.diff(timeseries['time']) <= np.timedelta64(0, 's')): return False else: return True # not needed -- _timeseries_is_ascending should catch this # def _timeseries_has_duplicates(self, timeseries): # # we need to have a valid shape to sort # if timeseries.shape == (): # timeseries = np.asarray([timeseries], # dtype=basic_types.datetime_value_2d) # unique = np.unique(timeseries['time']) # if len(unique) != len(timeseries['time']): # return True # else: # return False def _xform_input_timeseries(self, timeseries): ''' Ensure input data is numpy array with correct dtype and check timeseries doesn't have invalid data Derived classes can use this before updating timeseries, prior to converting units. ''' # following fails for 0-d objects so make sure we have a 1-D array # to work with timeseries = np.asarray(timeseries, dtype=basic_types.datetime_value_2d) if timeseries.shape == (): timeseries = np.asarray([timeseries], dtype=basic_types.datetime_value_2d) return timeseries def __str__(self): return '{0.__module__}.{0.__class__.__name__}'.format(self) @property def filename(self): return self._filename def get_timeseries(self, datetime=None, format='uv'): """ Returns the timeseries in requested format. If datetime=None, then the original timeseries that was entered is returned. If datetime is a list containing datetime objects, then the value for each of those date times is determined by the underlying C++ object and the timeseries is returned. The output format is defined by the strings 'r-theta', 'uv' :param datetime: [optional] datetime object or list of datetime objects for which the value is desired :type datetime: datetime object :param format: output format for the times series: either 'r-theta' or 'uv' :type format: either string or integer value defined by basic_types.ts_format.* (see cy_basic_types.pyx) :returns: numpy array containing dtype=basic_types.datetime_value_2d. Contains user specified datetime and the corresponding values in user specified ts_format """ if datetime is None: datetimeval = to_datetime_value_2d(self.ossm.timeseries, format) else: datetime = np.asarray(datetime, dtype='datetime64[s]').reshape(-1) timeval = np.zeros((len(datetime), ), dtype=basic_types.time_value_pair) timeval['time'] = date_to_sec(datetime) timeval['value'] = self.ossm.get_time_value(timeval['time']) datetimeval = to_datetime_value_2d(timeval, format) return datetimeval def set_timeseries(self, datetime_value_2d, format='uv'): """ Sets the timeseries to the new value given by a numpy array. The format for the input data defaults to basic_types.format.magnitude_direction but can be changed by the user Assumes timeseries is valid so _check_timeseries has been invoked and any unit conversions are done. This function simply converts datetime_value_2d to time_value_pair and updates the data in underlying cython/C++ object :param datetime_value_2d: timeseries of wind data defined in a numpy array :type datetime_value_2d: numpy array of dtype basic_types.datetime_value_2d :param format: output format for the times series; as defined by basic_types.format. :type format: either string or integer value defined by basic_types.format.* (see cy_basic_types.pyx) """ datetime_value_2d = self._xform_input_timeseries(datetime_value_2d) timeval = to_time_value_pair(datetime_value_2d, format) self.ossm.timeseries = timeval def __eq__(self, other): ''' only checks the timeseries data is equal in (m/s), in 'uv' format filename is irrelevant after data is loaded checks self.get_timeseries() == other.get_timeseries() Duck typing check - it does not expect type(self) == type(other) ''' self_ts = self.get_timeseries() other_ts = other.get_timeseries() if not np.all(self_ts['time'] == other_ts['time']): return False if not np.allclose( self_ts['value'], other_ts['value'], atol=1e-10, rtol=1e-10): return False return True def __ne__(self, other): return not self == other