use crate::errors::RSCMResult;
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::interpolate::Interp1d;
use crate::spatial::SpatialGrid;
use crate::state::{FourBoxSlice, HemisphericSlice};
use num::Float;
use numpy::ndarray::prelude::*;
use numpy::ndarray::{Array, Array1, Array2, Axis};
use serde::{Deserialize, Serialize};
use std::iter::zip;
use std::sync::Arc;
pub type Time = f64;
pub type FloatValue = f64;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TimeAxis {
bounds: Array1<Time>,
}
fn check_monotonic_increasing(arr: &Array1<Time>) -> bool {
let mut zipped_arr = zip(arr.slice(s![0..arr.len() - 1]), arr.slice(s![1..]));
zipped_arr.all(|(&a, &b)| b > a)
}
impl TimeAxis {
fn new(bounds: Array1<Time>) -> Self {
let is_monotonic = check_monotonic_increasing(&bounds);
assert!(is_monotonic);
Self { bounds }
}
pub fn from_values(values: Array1<Time>) -> Self {
assert!(values.len() >= 2);
let step = values[values.len() - 1] - values[values.len() - 2];
let mut bounds = Array::zeros(values.len() + 1);
bounds.slice_mut(s![..values.len()]).assign(&values);
let last_idx = bounds.len() - 1;
bounds[last_idx] = bounds[last_idx - 1] + step;
Self::new(bounds)
}
pub fn from_bounds(bounds: Array1<Time>) -> Self {
assert!(bounds.len() > 1);
Self::new(bounds)
}
pub fn values(&self) -> ArrayView1<'_, Time> {
self.bounds.slice(s![0..self.len()])
}
pub fn bounds(&self) -> ArrayView1<'_, Time> {
self.bounds.view()
}
pub fn len(&self) -> usize {
self.bounds.len() - 1
}
pub fn is_empty(&self) -> bool {
false
}
pub fn len_bounds(&self) -> usize {
self.bounds.len()
}
pub fn first(&self) -> &Time {
self.bounds.first().unwrap()
}
pub fn last(&self) -> &Time {
self.bounds.get(self.len()).unwrap()
}
pub fn at(&self, index: usize) -> Option<Time> {
if index < self.len() {
Option::from(self.bounds[index])
} else {
None
}
}
pub fn at_bounds(&self, index: usize) -> Option<(Time, Time)> {
if index < self.len() {
let bound: (Time, Time) = (self.bounds[index], self.bounds[index + 1]);
Option::from(bound)
} else {
None
}
}
pub fn get_index(&self, time: Time) -> usize {
self.bounds
.as_slice()
.unwrap()
.binary_search_by(|v| v.partial_cmp(&time).expect("Couldn't compare values"))
.unwrap()
}
pub fn contains(&self, value: Time) -> bool {
let mut found = false;
for v in self.values().iter() {
if value == *v {
found = true;
break;
}
}
found
}
pub fn index_of(&self, value: Time) -> Option<usize> {
for (i, v) in self.values().iter().enumerate() {
if (*v - value).abs() < 1e-10 {
return Some(i);
}
}
None
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GridTimeseries<T, G>
where
T: Float,
G: SpatialGrid,
{
#[serde(default, skip)]
grid: G,
values: Array2<T>,
time_axis: Arc<TimeAxis>,
units: String,
latest: usize,
interpolation_strategy: InterpolationStrategy,
}
impl<T, G> GridTimeseries<T, G>
where
T: Float + From<Time>,
G: SpatialGrid,
{
pub fn new(
values: Array2<T>,
time_axis: Arc<TimeAxis>,
grid: G,
units: String,
interpolation_strategy: InterpolationStrategy,
) -> Self {
assert_eq!(
values.nrows(),
time_axis.len(),
"Time dimension must match time axis length"
);
assert_eq!(
values.ncols(),
grid.size(),
"Space dimension must match grid size"
);
let mut latest = 0;
for (t, row) in values.rows().into_iter().enumerate() {
if row.iter().all(|v| !v.is_nan()) {
latest = t;
}
}
Self {
grid,
values,
time_axis,
units,
latest,
interpolation_strategy,
}
}
pub fn new_empty(
time_axis: Arc<TimeAxis>,
grid: G,
units: String,
interpolation_strategy: InterpolationStrategy,
) -> Self {
let mut values = Array2::zeros((time_axis.len(), grid.size()));
values.fill(T::nan());
Self::new(values, time_axis, grid, units, interpolation_strategy)
}
pub fn len(&self) -> usize {
self.values.nrows()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn grid(&self) -> &G {
&self.grid
}
pub fn time_axis(&self) -> Arc<TimeAxis> {
self.time_axis.clone()
}
pub fn units(&self) -> &str {
&self.units
}
pub fn latest(&self) -> usize {
self.latest
}
pub fn at_index(&self, time_index: usize, region_index: usize) -> Option<T> {
self.values.get((time_index, region_index)).copied()
}
pub fn set_index(&mut self, time_index: usize, region_index: usize, value: T) {
self.values[(time_index, region_index)] = value;
if time_index >= self.latest && !value.is_nan() {
let row = self.values.row(time_index);
if row.iter().all(|v| !v.is_nan()) {
self.latest = time_index;
}
}
}
pub fn set_all(&mut self, time_index: usize, values: &[T]) {
assert_eq!(
values.len(),
self.grid.size(),
"Values length ({}) must match grid size ({})",
values.len(),
self.grid.size()
);
for (region_index, &value) in values.iter().enumerate() {
self.values[(time_index, region_index)] = value;
}
if time_index >= self.latest {
let row = self.values.row(time_index);
if row.iter().all(|v| !v.is_nan()) {
self.latest = time_index;
}
}
}
pub fn at_time_index(&self, time_index: usize) -> Option<Vec<T>> {
if time_index < self.len() {
Some(self.values.row(time_index).to_vec())
} else {
None
}
}
pub fn latest_values(&self) -> Vec<T> {
self.values.row(self.latest).to_vec()
}
pub fn at_time_all(&self, time: Time) -> RSCMResult<Vec<T>> {
let mut result = Vec::with_capacity(self.grid.size());
for region_idx in 0..self.grid.size() {
let region_values = self.values.column(region_idx);
let interp = Interp1d::new(
self.time_axis.values(),
region_values.view(),
self.interpolation_strategy.clone(),
);
result.push(interp.interpolate(time)?);
}
Ok(result)
}
pub fn latest_global(&self) -> T {
let values_f64: Vec<FloatValue> = self
.latest_values()
.iter()
.map(|v| v.to_f64().unwrap())
.collect();
<T as From<Time>>::from(self.grid.aggregate_global(&values_f64))
}
pub fn aggregate_global(&self) -> GridTimeseries<T, crate::spatial::ScalarGrid> {
use crate::spatial::ScalarGrid;
let mut global_values = Array2::zeros((self.len(), 1));
for (t, row) in self.values.rows().into_iter().enumerate() {
let values_f64: Vec<FloatValue> = row.iter().map(|v| v.to_f64().unwrap()).collect();
global_values[(t, 0)] =
<T as From<Time>>::from(self.grid.aggregate_global(&values_f64));
}
GridTimeseries::new(
global_values,
self.time_axis.clone(),
ScalarGrid,
self.units.clone(),
self.interpolation_strategy.clone(),
)
}
pub fn transform_to<G2: SpatialGrid>(
&self,
target_grid: G2,
) -> RSCMResult<GridTimeseries<T, G2>> {
let target_size = target_grid.size();
let mut transformed_values = Array2::zeros((self.len(), target_size));
for (t, row) in self.values.rows().into_iter().enumerate() {
let values_f64: Vec<FloatValue> = row.iter().map(|v| v.to_f64().unwrap()).collect();
let transformed = self.grid.transform_to(&values_f64, &target_grid)?;
for (r, &val) in transformed.iter().enumerate() {
transformed_values[(t, r)] = <T as From<Time>>::from(val);
}
}
Ok(GridTimeseries::new(
transformed_values,
self.time_axis.clone(),
target_grid,
self.units.clone(),
self.interpolation_strategy.clone(),
))
}
pub fn region(&self, region_index: usize) -> GridTimeseries<T, crate::spatial::ScalarGrid> {
use crate::spatial::ScalarGrid;
assert!(
region_index < self.grid.size(),
"Region index out of bounds"
);
let mut region_values = Array2::zeros((self.len(), 1));
for t in 0..self.len() {
region_values[(t, 0)] = self.values[(t, region_index)];
}
GridTimeseries::new(
region_values,
self.time_axis.clone(),
ScalarGrid,
self.units.clone(),
self.interpolation_strategy.clone(),
)
}
pub fn region_by_name(
&self,
name: &str,
) -> Option<GridTimeseries<T, crate::spatial::ScalarGrid>> {
self.grid
.region_names()
.iter()
.position(|n| n == name)
.map(|idx| self.region(idx))
}
pub fn values(&self) -> &Array2<T> {
&self.values
}
pub fn with_interpolation_strategy(
&mut self,
interpolation_strategy: InterpolationStrategy,
) -> &Self {
self.interpolation_strategy = interpolation_strategy;
self
}
pub fn interpolate_into(self, new_time_axis: Arc<TimeAxis>) -> Self {
let mut new_values = Array2::zeros((new_time_axis.len(), self.grid.size()));
for region_idx in 0..self.grid.size() {
let region_values = self.values.column(region_idx);
let interp = Interp1d::new(
self.time_axis.values(),
region_values.view(),
self.interpolation_strategy.clone(),
);
for (t, &time) in new_time_axis.values().iter().enumerate() {
new_values[(t, region_idx)] = interp.interpolate(time).unwrap();
}
}
Self::new(
new_values,
new_time_axis,
self.grid,
self.units,
self.interpolation_strategy,
)
}
}
impl<T> GridTimeseries<T, crate::spatial::ScalarGrid>
where
T: Float + From<Time>,
{
pub fn from_values(values: Array1<T>, time: Array1<Time>) -> Self {
use crate::spatial::ScalarGrid;
let values_2d = values.insert_axis(Axis(1));
Self::new(
values_2d,
Arc::new(TimeAxis::from_values(time)),
ScalarGrid,
"".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
)
}
pub fn new_empty_scalar(
time_axis: Arc<TimeAxis>,
units: String,
interpolation_strategy: InterpolationStrategy,
) -> Self {
use crate::spatial::ScalarGrid;
Self::new_empty(time_axis, ScalarGrid, units, interpolation_strategy)
}
pub fn at_scalar(&self, index: usize) -> Option<T> {
self.at(index, crate::spatial::ScalarRegion::Global)
}
pub fn set_scalar(&mut self, time_index: usize, value: T) {
self.set(time_index, crate::spatial::ScalarRegion::Global, value);
}
pub fn latest_ref(&self) -> &usize {
&self.latest
}
pub fn latest_value(&self) -> Option<T> {
self.at_scalar(self.latest)
}
}
impl<T> GridTimeseries<T, crate::spatial::ScalarGrid>
where
T: Float + From<Time>,
{
pub fn at(&self, time_index: usize, region: crate::spatial::ScalarRegion) -> Option<T> {
GridTimeseries::at_index(self, time_index, region as usize)
}
pub fn set(&mut self, time_index: usize, region: crate::spatial::ScalarRegion, value: T) {
GridTimeseries::set_index(self, time_index, region as usize, value);
}
pub fn at_time(&self, time: Time, region: crate::spatial::ScalarRegion) -> RSCMResult<T> {
let result = self.at_time_all(time)?;
Ok(result[region as usize])
}
}
impl<T> GridTimeseries<T, crate::spatial::FourBoxGrid>
where
T: Float + From<Time>,
{
pub fn at(&self, time_index: usize, region: crate::spatial::FourBoxRegion) -> Option<T> {
GridTimeseries::at_index(self, time_index, region as usize)
}
pub fn set(&mut self, time_index: usize, region: crate::spatial::FourBoxRegion, value: T) {
GridTimeseries::set_index(self, time_index, region as usize, value);
}
pub fn set_from_slice(&mut self, time_index: usize, slice: &FourBoxSlice) {
let values: Vec<T> = slice
.as_array()
.iter()
.map(|&v| <T as From<Time>>::from(v))
.collect();
self.set_all(time_index, &values);
}
pub fn at_time(&self, time: Time, region: crate::spatial::FourBoxRegion) -> RSCMResult<T> {
let result = self.at_time_all(time)?;
Ok(result[region as usize])
}
}
impl<T> GridTimeseries<T, crate::spatial::HemisphericGrid>
where
T: Float + From<Time>,
{
pub fn at(&self, time_index: usize, region: crate::spatial::HemisphericRegion) -> Option<T> {
GridTimeseries::at_index(self, time_index, region as usize)
}
pub fn set(&mut self, time_index: usize, region: crate::spatial::HemisphericRegion, value: T) {
GridTimeseries::set_index(self, time_index, region as usize, value);
}
pub fn set_from_slice(&mut self, time_index: usize, slice: &HemisphericSlice) {
let values: Vec<T> = slice
.as_array()
.iter()
.map(|&v| <T as From<Time>>::from(v))
.collect();
self.set_all(time_index, &values);
}
pub fn at_time(&self, time: Time, region: crate::spatial::HemisphericRegion) -> RSCMResult<T> {
let result = self.at_time_all(time)?;
Ok(result[region as usize])
}
}
pub type Timeseries<T> = GridTimeseries<T, crate::spatial::ScalarGrid>;
#[cfg(test)]
mod tests {
use super::*;
use crate::interpolate::strategies::{InterpolationStrategy, PreviousStrategy};
use crate::spatial::{FourBoxRegion, HemisphericRegion, ScalarRegion};
use is_close::is_close;
#[test]
#[should_panic]
fn check_monotonic_values() {
Timeseries::from_values(array![1.0, 2.0, 3.0], array![2020.0, 1.0, 2021.0,]);
}
#[test]
fn get_value() {
let mut result = Timeseries::from_values(
array![1.0, 2.0, 3.0, 4.0, 5.0],
Array::range(2020.0, 2025.0, 1.0),
);
result.with_interpolation_strategy(InterpolationStrategy::from(LinearSplineStrategy::new(
false,
)));
assert_eq!(result.at_time(2020.0, ScalarRegion::Global).unwrap(), 1.0);
assert_eq!(result.at_time(2020.5, ScalarRegion::Global).unwrap(), 1.5);
assert_eq!(result.at_time(2021.0, ScalarRegion::Global).unwrap(), 2.0);
assert!(result.at_time(2026.0, ScalarRegion::Global).is_err());
}
#[test]
fn custom_interpolator() {
let data = array![1.0, 1.5, 2.0];
let years = Array::range(2020.0, 2023.0, 1.0);
let query = 2024.0;
let mut timeseries = Timeseries::from_values(data, years);
let result = timeseries.at_time(query, ScalarRegion::Global).unwrap();
assert_eq!(result, 3.0);
timeseries
.with_interpolation_strategy(InterpolationStrategy::from(PreviousStrategy::new(true)));
let result = timeseries.at_time(query, ScalarRegion::Global).unwrap();
assert_eq!(result, 2.0);
}
#[test]
fn serialise_and_deserialise_json() {
let data = array![1.0, 1.5, 2.0];
let years = Array::range(2020.0, 2023.0, 1.0);
let timeseries = Timeseries::from_values(data, years);
let serialised = serde_json::to_string(×eries).unwrap();
assert_eq!(
serialised,
r#"{"values":{"v":1,"dim":[3,1],"data":[1.0,1.5,2.0]},"time_axis":{"bounds":{"v":1,"dim":[4],"data":[2020.0,2021.0,2022.0,2023.0]}},"units":"","latest":2,"interpolation_strategy":"Linear"}"#
);
let deserialised = serde_json::from_str::<Timeseries<f64>>(&serialised).unwrap();
assert_eq!(timeseries.values(), deserialised.values());
}
#[test]
#[should_panic]
fn serialise_and_deserialise_with_nan_json() {
let data = array![1.0, 1.5, FloatValue::nan()];
let years = Array::range(2020.0, 2023.0, 1.0);
let timeseries = Timeseries::from_values(data, years);
let serialised = serde_json::to_string(×eries).unwrap();
assert_eq!(
serialised,
r#"{"units":"","values":{"v":1,"dim":[3],"data":[1.0,1.5,null]},"time_axis":{"bounds":{"v":1,"dim":[4],"data":[2020.0,2021.0,2022.0,2023.0]}},"latest":2,"interpolation_strategy":"Linear"}"#
);
serde_json::from_str::<Timeseries<f64>>(&serialised).unwrap();
}
#[test]
fn serialise_and_deserialise_with_nan_toml() {
let data = array![1.0, 1.5, FloatValue::nan()];
let years = Array::range(2020.0, 2023.0, 1.0);
let timeseries = Timeseries::from_values(data, years);
let serialised = toml::to_string(×eries).unwrap();
let expected = "units = \"\"
latest = 1
interpolation_strategy = \"Linear\"
[values]
v = 1
dim = [3, 1]
data = [1.0, 1.5, nan]
[time_axis.bounds]
v = 1
dim = [4]
data = [2020.0, 2021.0, 2022.0, 2023.0]
";
assert_eq!(serialised, expected);
let deserialised = toml::from_str::<Timeseries<f64>>(&serialised).unwrap();
assert!(zip(timeseries.values(), deserialised.values())
.all(|(x0, x1)| { is_close!(*x0, *x1) || (x0.is_nan() && x0.is_nan()) }))
}
mod grid_timeseries_tests {
use super::*;
use crate::spatial::{FourBoxGrid, HemisphericGrid};
#[test]
fn create_grid_timeseries() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
let values = Array2::from_shape_vec(
(3, 4),
vec![
15.0, 14.0, 10.0, 9.0, 15.5, 14.5, 10.5, 9.5, 16.0, 15.0, 11.0, 10.0, ],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
assert_eq!(ts.len(), 3);
assert_eq!(ts.grid().size(), 4);
assert_eq!(ts.latest(), 2);
}
#[test]
fn grid_timeseries_access() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values = Array2::from_shape_vec(
(2, 4),
vec![
15.0, 14.0, 10.0, 9.0, 16.0, 15.0, 11.0, 10.0, ],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
assert_eq!(ts.at(0, FourBoxRegion::NorthernOcean), Some(15.0));
assert_eq!(ts.at(0, FourBoxRegion::NorthernLand), Some(14.0));
assert_eq!(ts.at(1, FourBoxRegion::SouthernOcean), Some(11.0));
let row0 = ts.at_time_index(0).unwrap();
assert_eq!(row0, vec![15.0, 14.0, 10.0, 9.0]);
let latest = ts.latest_values();
assert_eq!(latest, vec![16.0, 15.0, 11.0, 10.0]);
}
#[test]
fn grid_timeseries_set() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values = Array2::from_shape_vec(
(2, 4),
vec![
15.0,
14.0,
10.0,
9.0,
FloatValue::nan(),
FloatValue::nan(),
FloatValue::nan(),
FloatValue::nan(),
],
)
.unwrap();
let mut ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
assert_eq!(ts.latest(), 0);
ts.set_index(1, 0, 16.0);
ts.set_index(1, 1, 15.0);
ts.set_index(1, 2, 11.0);
ts.set_index(1, 3, 10.0);
assert_eq!(ts.latest(), 1);
assert_eq!(ts.at(1, FourBoxRegion::NorthernOcean), Some(16.0));
}
#[test]
fn grid_timeseries_aggregate_global() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values = Array2::from_shape_vec(
(2, 4),
vec![
15.0, 14.0, 10.0, 9.0, 16.0, 15.0, 11.0, 10.0, ],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let global = ts.aggregate_global();
assert_eq!(global.grid().size(), 1);
assert_eq!(global.len(), 2);
assert_eq!(global.at(0, ScalarRegion::Global), Some(12.0));
assert_eq!(global.at(1, ScalarRegion::Global), Some(13.0));
}
#[test]
fn grid_timeseries_transform_four_box_to_hemispheric() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values =
Array2::from_shape_vec((2, 4), vec![16.0, 14.0, 12.0, 8.0, 17.0, 15.0, 13.0, 9.0])
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let hemispheric = ts.transform_to(HemisphericGrid::equal_weights()).unwrap();
assert_eq!(hemispheric.grid().size(), 2);
assert_eq!(hemispheric.at(0, HemisphericRegion::Northern), Some(15.0));
assert_eq!(hemispheric.at(0, HemisphericRegion::Southern), Some(10.0));
}
#[test]
fn grid_timeseries_region_extraction() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values =
Array2::from_shape_vec((2, 4), vec![15.0, 14.0, 10.0, 9.0, 16.0, 15.0, 11.0, 10.0])
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let northern_ocean = ts.region(FourBoxRegion::NorthernOcean as usize);
assert_eq!(northern_ocean.grid().size(), 1);
assert_eq!(northern_ocean.at(0, ScalarRegion::Global), Some(15.0));
assert_eq!(northern_ocean.at(1, ScalarRegion::Global), Some(16.0));
let northern_land = ts.region_by_name("Northern Land").unwrap();
assert_eq!(northern_land.at(0, ScalarRegion::Global), Some(14.0));
assert_eq!(northern_land.at(1, ScalarRegion::Global), Some(15.0));
}
#[test]
fn grid_timeseries_interpolation() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2002.0]));
let values = Array2::from_shape_vec(
(2, 4),
vec![10.0, 10.0, 10.0, 10.0, 20.0, 20.0, 20.0, 20.0],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let values_2001 = ts.at_time_all(2001.0).unwrap();
assert_eq!(values_2001.len(), 4);
for val in values_2001 {
assert_eq!(val, 15.0); }
}
#[test]
fn grid_timeseries_interpolate_into() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2002.0]));
let values = Array2::from_shape_vec(
(2, 4),
vec![10.0, 10.0, 10.0, 10.0, 20.0, 20.0, 20.0, 20.0],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let new_time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0, 2002.0]));
let resampled = ts.interpolate_into(new_time_axis);
assert_eq!(resampled.len(), 3);
assert_eq!(resampled.at(0, FourBoxRegion::NorthernOcean), Some(10.0));
assert_eq!(resampled.at(1, FourBoxRegion::NorthernOcean), Some(15.0));
assert_eq!(resampled.at(2, FourBoxRegion::NorthernOcean), Some(20.0));
}
#[test]
fn grid_timeseries_serialization_json() {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values =
Array2::from_shape_vec((2, 4), vec![15.0, 14.0, 10.0, 9.0, 16.0, 15.0, 11.0, 10.0])
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"degC".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let serialized = serde_json::to_string(&ts).unwrap();
let deserialized: GridTimeseries<FloatValue, FourBoxGrid> =
serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.len(), ts.len());
assert_eq!(deserialized.grid().size(), ts.grid().size());
assert_eq!(
deserialized.at(0, FourBoxRegion::NorthernOcean),
ts.at(0, FourBoxRegion::NorthernOcean)
);
}
}
}