def test_kling_gupta_and_nash_sutcliffe(self): """ Test/verify exposure of the kling_gupta and nash_sutcliffe correlation functions """ def np_nash_sutcliffe(o, p): return 1 - (np.sum((o - p) ** 2)) / (np.sum((o - np.mean(o)) ** 2)) c = api.Calendar() t0 = c.time(2016, 1, 1) dt = api.deltahours(1) n = 240 ta = api.Timeaxis2(t0, dt, n) from math import sin, pi rad_max = 10 * 2 * pi obs_values = api.DoubleVector.from_numpy(np.array([sin(i * rad_max / n) for i in range(n)])) mod_values = api.DoubleVector.from_numpy(np.array([0.1 + sin(pi / 10.0 + i * rad_max / n) for i in range(n)])) obs_ts = api.Timeseries(ta=ta, values=obs_values, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) mod_ts = api.Timeseries(ta=ta, values=mod_values, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) self.assertAlmostEqual(api.kling_gupta(obs_ts, obs_ts, ta, 1.0, 1.0, 1.0), 1.0, None, "1.0 for perfect match") self.assertAlmostEqual(api.nash_sutcliffe(obs_ts, obs_ts, ta), 1.0, None, "1.0 for perfect match") # verify some non trivial cases, and compare to numpy version of ns mod_inv = obs_ts * -1.0 kge_inv = obs_ts.kling_gupta(mod_inv) # also show how to use time-series.method itself to ease use ns_inv = obs_ts.nash_sutcliffe(mod_inv) # similar for nash_sutcliffe, you can reach it directly from a ts ns_inv2 = np_nash_sutcliffe(obs_ts.values.to_numpy(), mod_inv.values.to_numpy()) self.assertLessEqual(kge_inv, 1.0, "should be less than 1") self.assertLessEqual(ns_inv, 1.0, "should be less than 1") self.assertAlmostEqual(ns_inv, ns_inv2, 4, "should equal numpy calculated value") kge_obs_mod = api.kling_gupta(obs_ts, mod_ts, ta, 1.0, 1.0, 1.0) self.assertLessEqual(kge_obs_mod, 1.0) self.assertAlmostEqual(obs_ts.nash_sutcliffe( mod_ts), np_nash_sutcliffe(obs_ts.values.to_numpy(), mod_ts.values.to_numpy()))
def _create_forecast_set(self, n_fc, t0, dt, n_steps, dt_fc, fx): """ Parameters ---------- n_fc : int number of forecasts, e.g. 8 t0 : utctime start of first forecast dt : utctimespan delta t for forecast-ts n_steps : number of steps in one forecast-ts dt_fc : utctimespan delta t between each forecast, like deltahours(6) fx : lambda time_axis: a function returning a DoubleVector with values for the supplied time-axis Returns ------- api.TsVector() """ fc_set = api.TsVector() for i in range(n_fc): ta = api.Timeaxis2(t0 + i * dt_fc, dt, n_steps) ts = api.Timeseries( ta=ta, values=fx(ta), point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) fc_set.append(ts) return fc_set
def test_percentiles(self): c = api.Calendar() t0 = c.time(2016, 1, 1) dt = api.deltahours(1) n = 240 ta = api.Timeaxis(t0, dt, n) timeseries = api.TsVector() for i in range(10): timeseries.append( api.Timeseries(ta=ta, fill_value=i, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE)) wanted_percentiles = api.IntVector([0, 10, 50, -1, 70, 100]) ta_day = api.Timeaxis(t0, dt * 24, n // 24) ta_day2 = api.Timeaxis2(t0, dt * 24, n // 24) percentiles = api.percentiles(timeseries, ta_day, wanted_percentiles) percentiles2 = timeseries.percentiles(ta_day2, wanted_percentiles) # just to verify it works with alt. syntax self.assertEqual(len(percentiles2), len(percentiles)) for i in range(len(ta_day)): self.assertAlmostEqual(0.0, percentiles[0].value(i), 3, " 0-percentile") self.assertAlmostEqual(0.9, percentiles[1].value(i), 3, " 10-percentile") self.assertAlmostEqual(4.5, percentiles[2].value(i), 3, " 50-percentile") self.assertAlmostEqual(4.5, percentiles[3].value(i), 3, " -average") self.assertAlmostEqual(6.3, percentiles[4].value(i), 3, " 70-percentile") self.assertAlmostEqual(9.0, percentiles[5].value(i), 3, "100-percentile")
def _create_geo_ts_grid(self, nx, ny, dx, fx): """Create a geo_ts_grid of TemperatureSources, We just create a terrain model starting at 0 and increasing to max_elevation at max x and y. parameters ---------- nx : int number of grid-cells in x direction ny : int number of grid-cells in y direction dx : float distance in meters for one of the sides in the grid fx : lambda z : 1.0 a function that takes elevation z as input and generates a np.array len(self.ta) of float64 as time-series returns ------- a TemperatureSourceVector() filled with geo-ts representing the grid. """ arome_grid = api.TemperatureSourceVector() for i in range(nx): for j in range(ny): z = self.max_elevation * (i + j) / (nx + ny) ts = api.Timeseries(ta=self.ta, values=fx(z), point_fx=api.point_interpretation_policy. POINT_AVERAGE_VALUE) geo_ts = api.TemperatureSource(api.GeoPoint(i * dx, j * dx, z), ts) arome_grid.append(geo_ts) return arome_grid
def _make_fc_from_obs(self, obs_set, bias): fc_set = api.TemperatureSourceVector() bias_ts = api.Timeseries(self.ta, fill_value=bias) for obs in obs_set: geo_ts = api.TemperatureSource(obs.mid_point(), obs.ts + bias_ts) fc_set.append(geo_ts) return fc_set
def test_timeseries_vector(self): c = api.Calendar() t0 = api.utctime_now() dt = api.deltahours(1) n = 240 ta = api.Timeaxis(t0, dt, n) a = api.Timeseries(ta=ta, fill_value=3.0, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) b = api.Timeseries(ta=ta, fill_value=2.0, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) v = api.TsVector() v.append(a) v.append(b) self.assertEqual(len(v), 2) self.assertAlmostEqual(v[0].value(0), 3.0, "expect first ts to be 3.0") aa = api.Timeseries(ta=a.time_axis, values=a.values, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) # copy construct (really copy the values!) a.fill(1.0) self.assertAlmostEqual(v[0].value(0), 1.0, "expect first ts to be 1.0, because the vector keeps a reference ") self.assertAlmostEqual(aa.value(0), 3.0)
def _create_obs_set(self, geo_points): obs_set = api.TemperatureSourceVector() fx = lambda z: [15 for x in range(self.nt)] ts = api.Timeseries( ta=self.ta, values=fx(self.ta), point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) for gp in geo_points: # Add only one TS per GP, but should be several geo_ts = api.TemperatureSource(gp, ts) obs_set.append(geo_ts) return obs_set
def test_basic_timeseries_math_operations(self): """ Test that timeseries functionality is exposed, and briefly verify correctness of operators (the shyft core do the rest of the test job, not repeated here). """ c = api.Calendar() t0 = api.utctime_now() dt = api.deltahours(1) n = 240 ta = api.Timeaxis2(t0, dt, n) a = api.Timeseries(ta=ta, fill_value=3.0, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) b = api.Timeseries(ta=ta, fill_value=1.0) b.fill(2.0) # demo how to fill a point ts c = a + b * 3.0 - a / 2.0 # operator + * - / d = -a # unary minus e = a.average(ta) # average f = api.max(c, 300.0) g = api.min(c, -300.0) h = a.max(c, 300) k = a.min(c, -300) self.assertEqual(a.size(), n) self.assertEqual(b.size(), n) self.assertEqual(c.size(), n) self.assertAlmostEqual(c.value(0), 3.0 + 2.0 * 3.0 - 3.0 / 2.0) # 7.5 for i in range(n): self.assertAlmostEqual(c.value(i), a.value(i) + b.value(i) * 3.0 - a.value(i) / 2.0, delta=0.0001) self.assertAlmostEqual(d.value(i), - a.value(i), delta=0.0001) self.assertAlmostEqual(e.value(i), a.value(i), delta=0.00001) self.assertAlmostEqual(f.value(i), +300.0, delta=0.00001) self.assertAlmostEqual(h.value(i), +300.0, delta=0.00001) self.assertAlmostEqual(g.value(i), -300.0, delta=0.00001) self.assertAlmostEqual(k.value(i), -300.0, delta=0.00001) # now some more detailed tests for setting values b.set(0, 3.0) self.assertAlmostEqual(b.value(0), 3.0) # 3.0 + 3 * 3 - 3.0/2.0 self.assertAlmostEqual(c.value(1), 7.5, delta=0.0001) # 3 + 3*3 - 1.5 = 10.5 self.assertAlmostEqual(c.value(0), 10.5, delta=0.0001) # 3 + 3*3 - 1.5 = 10.5
def test_accumulate(self): c = api.Calendar() t0 = c.time(2016, 1, 1) dt = api.deltahours(1) n = 240 ta = api.Timeaxis2(t0, dt, n) ts0 = api.Timeseries(ta=ta, fill_value=1.0, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) ts1 = ts0.accumulate(ts0.get_time_axis()) # ok, maybe we should make method that does time-axis implicit ? ts1_values = ts1.values for i in range(n): expected_value = i * dt * 1.0 self.assertAlmostEqual(expected_value, ts1.value(i), 3, "expect integral f(t)*dt") self.assertAlmostEqual(expected_value, ts1_values[i], 3, "expect value vector equal as well")
def _create_geo_precipitation_grid(self, nx, ny, dx, fx): arome_grid = api.PrecipitationSourceVector() for i in range(nx): for j in range(ny): z = self.max_elevation * (i + j) / (nx + ny) ts = api.Timeseries(ta=self.ta, values=fx(z), point_fx=api.point_interpretation_policy. POINT_AVERAGE_VALUE) geo_ts = api.PrecipitationSource( api.GeoPoint(i * dx, j * dx, z), ts) arome_grid.append(geo_ts) return arome_grid
def test_time_shift(self): c = api.Calendar() t0 = c.time(2016, 1, 1) t1 = c.time(2017, 1, 1) dt = api.deltahours(1) n = 240 ta = api.Timeaxis(t0, dt, n) ts0 = api.Timeseries(ta=ta, fill_value=3.0, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) ts1 = api.time_shift(ts0, t1 - t0) ts2 = 2.0 * ts1.time_shift(t0 - t1) # just to verify it still can take part in an expression for i in range(ts0.size()): self.assertAlmostEqual(ts0.value(i), ts1.value(i), 3, "expect values to be equal") self.assertAlmostEqual(ts0.value(i) * 2.0, ts2.value(i), 3, "expect values to be double value") self.assertEqual(ts0.time(i) + (t1 - t0), ts1.time(i), "expect time to be offset delta_t different") self.assertEqual(ts0.time(i), ts2.time(i), "expect time to be equal")
def test_bias_predictor(self): """ Verify that if we feed forecast[n] and observation into the bias-predictor it will create the estimated bias offsets """ f = api.KalmanFilter() bp = api.KalmanBiasPredictor(f) self.assertIsNotNone(bp) self.assertEqual(bp.filter.parameter.n_daily_observations, 8) n_fc = 8 utc = api.Calendar() t0 = utc.time(2016, 1, 1) dt = api.deltahours(1) n_fc_steps = 36 # e.g. like arome 36 hours fc_dt = api.deltahours(6) fc_fx = lambda time_axis: self._create_fc_values( time_axis, 2.0) # just return a constant 2.0 deg C for now fc_set = self._create_geo_forecast_set(n_fc, t0, dt, n_fc_steps, fc_dt, fc_fx) n_obs = 24 obs_ta = api.Timeaxis2(t0, dt, n_obs) obs_ts = api.Timeseries(obs_ta, fill_value=0.0) kalman_dt = api.deltahours( 3) # suitable average for prediction temperature kalman_ta = api.Timeaxis2(t0, kalman_dt, 8) bp.update_with_forecast( fc_set, obs_ts, kalman_ta ) # here we feed in forecast-set and observation into kalman fc_setv = self._create_forecast_set(n_fc, t0, dt, n_fc_steps, fc_dt, fc_fx) bp.update_with_forecast( fc_setv, obs_ts, kalman_ta) # also verify we can feed in a pure TsVector bias_pattern = bp.state.x # the bp.state.x is now the best estimates fo the bias between fc and observation self.assertEqual(len(bias_pattern), 8) for i in range(len(bias_pattern)): self.assertLess(abs(bias_pattern[i] - 2.0), 0.2) # bias should iterate to approx 2.0 degC now.
def test_partition_by(self): """ verify/demo exposure of the .partition_by function that can be used to produce yearly percentiles statistics for long historical time-series """ c = api.Calendar() t0 = c.time(1930, 9, 1) dt = api.deltahours(1) n = c.diff_units(t0, c.time(2016, 9, 1), dt) ta = api.Timeaxis2(t0, dt, n) pattern_values = api.DoubleVector.from_numpy(np.arange(len(ta))) # increasing values src_ts = api.Timeseries(ta=ta, values=pattern_values, point_fx=api.point_interpretation_policy.POINT_AVERAGE_VALUE) partition_t0 = c.time(2016, 9, 1) n_partitions = 80 partition_interval = api.Calendar.YEAR # get back TsVector, # where all TsVector[i].index_of(partition_t0) # is equal to the index ix for which the TsVector[i].value(ix) correspond to start value of that particular partition. ts_partitions = src_ts.partition_by(c, t0, partition_interval, n_partitions, partition_t0) self.assertEqual(len(ts_partitions),n_partitions) ty = t0 for ts in ts_partitions: ix = ts.index_of(partition_t0) vix = ts.value(ix) expected_value = c.diff_units(t0, ty, dt) self.assertEqual(vix, expected_value) ty = c.add(ty, partition_interval, 1) # Now finally, try percentiles on the partitions wanted_percentiles = [0, 10, 25, -1, 50, 75, 90, 100] ta_percentiles = api.Timeaxis2(partition_t0, api.deltahours(24), 365) percentiles = api.percentiles(ts_partitions,ta_percentiles,wanted_percentiles) self.assertEqual(len(percentiles), len(wanted_percentiles))
def test_can_run_bayesian_kriging_from_observation_sites_to_1km_grid(self): """ Somewhat more complex test, first do kriging of 1 timeseries out to grid (expect same values flat) then do kriging of 3 time-series out to the grid (expect different values, no real verification here since this is done elsewhere """ # arrange the test with a btk_parameter, a source grid and a destination grid btk_parameter = api.BTKParameter(temperature_gradient=-0.6, temperature_gradient_sd=0.25, sill=25.0, nugget=0.5, range=20000.0, zscale=20.0) fx = lambda z: api.DoubleVector.from_numpy(np.zeros(self.n)) grid_1km_1 = self._create_geo_point_grid(self.mnx, self.mny, self.dx_model) grid_1km_3 = self._create_geo_point_grid(self.mnx, self.mny, self.dx_model) observation_sites = api.TemperatureSourceVector() ta_obs = api.Timeaxis(self.t, self.d * 3, int(self.n / 3)) ta_grid = api.Timeaxis(self.t, self.d, self.n) ts_site_1 = api.Timeseries( ta_obs, values=api.DoubleVector.from_numpy( (20.0 - 0.6 * 5.0 / 100) + 3.0 * np.sin( np.arange(start=0, stop=ta_obs.size(), step=1) * 2 * np.pi / 8.0 - np.pi / 2.0))) ts_site_2 = api.Timeseries( ta_obs, values=api.DoubleVector.from_numpy( (20.0 - 0.6 * 500.0 / 100) + 3.0 * np.sin( np.arange(start=0, stop=ta_obs.size(), step=1) * 2 * np.pi / 8.0 - np.pi / 2.0))) ts_site_3 = api.Timeseries( ta_obs, values=api.DoubleVector.from_numpy( (20.0 - 0.6 * 1050.0 / 100) + 3.0 * np.sin( np.arange(start=0, stop=ta_obs.size(), step=1) * 2 * np.pi / 8.0 - np.pi / 2.0))) observation_sites.append( api.TemperatureSource(api.GeoPoint(50.0, 50.0, 5.0), ts_site_1)) # act 1: just one time-series put into the system, should give same ts (true-averaged) in all the grid-1km_ts (which can be improved using std.gradient..) grid_1km_1ts = api.bayesian_kriging_temperature( observation_sites, grid_1km_1, ta_grid, btk_parameter) # assert 1: self.assertEqual(len(grid_1km_1ts), self.mnx * self.mny) expected_grid_1ts_values = ts_site_1.average( api.Timeaxis2(ta_grid)).values.to_numpy() for gts in grid_1km_1ts: self.assertEqual(gts.ts.size(), ta_grid.size()) self.assertTrue( np.allclose(expected_grid_1ts_values, gts.ts.values.to_numpy())) observation_sites.append( api.TemperatureSource(api.GeoPoint(9000.0, 500.0, 500), ts_site_2)) observation_sites.append( api.TemperatureSource(api.GeoPoint(9000.0, 12000.0, 1050.0), ts_site_3)) grid_1km_3ts = api.bayesian_kriging_temperature( observation_sites, grid_1km_3, ta_grid, btk_parameter) self.assertEqual(len(grid_1km_3ts), self.mnx * self.mny) for gts in grid_1km_3ts: self.assertEqual(gts.ts.size(), ta_grid.size()) self.assertFalse( np.allclose(expected_grid_1ts_values, gts.ts.values.to_numpy()))