use crate::errors::RSCMResult;
use crate::spatial::{
FourBoxGrid, FourBoxRegion, HemisphericGrid, HemisphericRegion, ScalarGrid, ScalarRegion,
SpatialGrid,
};
use crate::timeseries::{FloatValue, GridTimeseries, Time, Timeseries};
use crate::timeseries_collection::{TimeseriesData, TimeseriesItem, VariableType};
use ndarray::ArrayView1;
use num::Float;
use std::collections::HashMap;
#[derive(Debug)]
pub struct TimeseriesWindow<'a> {
timeseries: &'a Timeseries<FloatValue>,
current_index: usize,
current_time: Time,
}
impl<'a> TimeseriesWindow<'a> {
pub fn new(
timeseries: &'a Timeseries<FloatValue>,
current_index: usize,
current_time: Time,
) -> Self {
Self {
timeseries,
current_index,
current_time,
}
}
pub fn current(&self) -> FloatValue {
self.timeseries
.at(self.current_index, ScalarRegion::Global)
.expect("Current index out of bounds")
}
pub fn previous(&self) -> Option<FloatValue> {
if self.current_index == 0 {
None
} else {
self.timeseries
.at(self.current_index - 1, ScalarRegion::Global)
}
}
pub fn at_offset(&self, offset: isize) -> Option<FloatValue> {
let index = self.current_index as isize + offset;
if index < 0 || index as usize >= self.timeseries.len() {
None
} else {
self.timeseries.at(index as usize, ScalarRegion::Global)
}
}
pub fn last_n(&self, n: usize) -> ArrayView1<'_, FloatValue> {
assert!(
n <= self.current_index + 1,
"Cannot get {} values when only {} available",
n,
self.current_index + 1
);
let start = self.current_index + 1 - n;
let end = self.current_index + 1;
self.timeseries.values().slice(ndarray::s![start..end, 0])
}
pub fn interpolate(&self, t: Time) -> RSCMResult<FloatValue> {
self.timeseries.at_time(t, ScalarRegion::Global)
}
pub fn time(&self) -> Time {
self.current_time
}
pub fn index(&self) -> usize {
self.current_index
}
pub fn len(&self) -> usize {
self.timeseries.len()
}
pub fn is_empty(&self) -> bool {
self.timeseries.is_empty()
}
}
#[derive(Debug)]
pub struct GridTimeseriesWindow<'a, G>
where
G: SpatialGrid,
{
timeseries: &'a GridTimeseries<FloatValue, G>,
current_index: usize,
current_time: Time,
}
impl<'a, G> GridTimeseriesWindow<'a, G>
where
G: SpatialGrid,
{
pub fn new(
timeseries: &'a GridTimeseries<FloatValue, G>,
current_index: usize,
current_time: Time,
) -> Self {
Self {
timeseries,
current_index,
current_time,
}
}
pub fn current_all(&self) -> Vec<FloatValue> {
self.timeseries
.at_time_index(self.current_index)
.expect("Current index out of bounds")
}
pub fn previous_all(&self) -> Option<Vec<FloatValue>> {
if self.current_index == 0 {
None
} else {
self.timeseries.at_time_index(self.current_index - 1)
}
}
pub fn time(&self) -> Time {
self.current_time
}
pub fn index(&self) -> usize {
self.current_index
}
pub fn grid(&self) -> &G {
self.timeseries.grid()
}
pub fn len(&self) -> usize {
self.timeseries.len()
}
pub fn is_empty(&self) -> bool {
self.timeseries.is_empty()
}
pub fn interpolate_all(&self, t: Time) -> RSCMResult<Vec<FloatValue>> {
self.timeseries.at_time_all(t)
}
}
impl<'a> GridTimeseriesWindow<'a, FourBoxGrid> {
pub fn current(&self, region: FourBoxRegion) -> FloatValue {
self.timeseries
.at(self.current_index, region)
.expect("Current index out of bounds")
}
pub fn previous(&self, region: FourBoxRegion) -> Option<FloatValue> {
if self.current_index == 0 {
None
} else {
self.timeseries.at(self.current_index - 1, region)
}
}
pub fn current_global(&self) -> FloatValue {
let values = self.current_all();
self.timeseries.grid().aggregate_global(&values)
}
pub fn previous_global(&self) -> Option<FloatValue> {
self.previous_all()
.map(|values| self.timeseries.grid().aggregate_global(&values))
}
pub fn interpolate(&self, t: Time, region: FourBoxRegion) -> RSCMResult<FloatValue> {
self.timeseries.at_time(t, region)
}
}
impl<'a> GridTimeseriesWindow<'a, HemisphericGrid> {
pub fn current(&self, region: HemisphericRegion) -> FloatValue {
self.timeseries
.at(self.current_index, region)
.expect("Current index out of bounds")
}
pub fn previous(&self, region: HemisphericRegion) -> Option<FloatValue> {
if self.current_index == 0 {
None
} else {
self.timeseries.at(self.current_index - 1, region)
}
}
pub fn current_global(&self) -> FloatValue {
let values = self.current_all();
self.timeseries.grid().aggregate_global(&values)
}
pub fn previous_global(&self) -> Option<FloatValue> {
self.previous_all()
.map(|values| self.timeseries.grid().aggregate_global(&values))
}
pub fn interpolate(&self, t: Time, region: HemisphericRegion) -> RSCMResult<FloatValue> {
self.timeseries.at_time(t, region)
}
}
impl<'a> GridTimeseriesWindow<'a, ScalarGrid> {
pub fn current(&self) -> FloatValue {
self.timeseries
.at(self.current_index, ScalarRegion::Global)
.expect("Current index out of bounds")
}
pub fn previous(&self) -> Option<FloatValue> {
if self.current_index == 0 {
None
} else {
self.timeseries
.at(self.current_index - 1, ScalarRegion::Global)
}
}
pub fn interpolate(&self, t: Time) -> RSCMResult<FloatValue> {
self.timeseries.at_time(t, ScalarRegion::Global)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FourBoxSlice(pub [FloatValue; 4]);
impl FourBoxSlice {
pub fn new() -> Self {
Self([FloatValue::NAN; 4])
}
pub fn uniform(value: FloatValue) -> Self {
Self([value; 4])
}
pub fn from_array(values: [FloatValue; 4]) -> Self {
Self(values)
}
pub fn with(mut self, region: FourBoxRegion, value: FloatValue) -> Self {
self.0[region as usize] = value;
self
}
pub fn set(&mut self, region: FourBoxRegion, value: FloatValue) {
self.0[region as usize] = value;
}
pub fn get(&self, region: FourBoxRegion) -> FloatValue {
self.0[region as usize]
}
pub fn get_mut(&mut self, region: FourBoxRegion) -> &mut FloatValue {
&mut self.0[region as usize]
}
pub fn as_array(&self) -> &[FloatValue; 4] {
&self.0
}
pub fn as_array_mut(&mut self) -> &mut [FloatValue; 4] {
&mut self.0
}
pub fn to_vec(&self) -> Vec<FloatValue> {
self.0.to_vec()
}
pub fn aggregate_global(&self, grid: &FourBoxGrid) -> FloatValue {
grid.aggregate_global(&self.0)
}
}
impl Default for FourBoxSlice {
fn default() -> Self {
Self::new()
}
}
impl From<[FloatValue; 4]> for FourBoxSlice {
fn from(values: [FloatValue; 4]) -> Self {
Self(values)
}
}
impl From<FourBoxSlice> for [FloatValue; 4] {
fn from(slice: FourBoxSlice) -> Self {
slice.0
}
}
impl From<FourBoxSlice> for Vec<FloatValue> {
fn from(slice: FourBoxSlice) -> Self {
slice.0.to_vec()
}
}
impl std::ops::Index<FourBoxRegion> for FourBoxSlice {
type Output = FloatValue;
fn index(&self, region: FourBoxRegion) -> &Self::Output {
&self.0[region as usize]
}
}
impl std::ops::IndexMut<FourBoxRegion> for FourBoxSlice {
fn index_mut(&mut self, region: FourBoxRegion) -> &mut Self::Output {
&mut self.0[region as usize]
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct HemisphericSlice(pub [FloatValue; 2]);
impl HemisphericSlice {
pub fn new() -> Self {
Self([FloatValue::NAN; 2])
}
pub fn uniform(value: FloatValue) -> Self {
Self([value; 2])
}
pub fn from_array(values: [FloatValue; 2]) -> Self {
Self(values)
}
pub fn with(mut self, region: HemisphericRegion, value: FloatValue) -> Self {
self.0[region as usize] = value;
self
}
pub fn set(&mut self, region: HemisphericRegion, value: FloatValue) {
self.0[region as usize] = value;
}
pub fn get(&self, region: HemisphericRegion) -> FloatValue {
self.0[region as usize]
}
pub fn get_mut(&mut self, region: HemisphericRegion) -> &mut FloatValue {
&mut self.0[region as usize]
}
pub fn as_array(&self) -> &[FloatValue; 2] {
&self.0
}
pub fn as_array_mut(&mut self) -> &mut [FloatValue; 2] {
&mut self.0
}
pub fn to_vec(&self) -> Vec<FloatValue> {
self.0.to_vec()
}
pub fn aggregate_global(&self, grid: &HemisphericGrid) -> FloatValue {
grid.aggregate_global(&self.0)
}
}
impl Default for HemisphericSlice {
fn default() -> Self {
Self::new()
}
}
impl From<[FloatValue; 2]> for HemisphericSlice {
fn from(values: [FloatValue; 2]) -> Self {
Self(values)
}
}
impl From<HemisphericSlice> for [FloatValue; 2] {
fn from(slice: HemisphericSlice) -> Self {
slice.0
}
}
impl From<HemisphericSlice> for Vec<FloatValue> {
fn from(slice: HemisphericSlice) -> Self {
slice.0.to_vec()
}
}
impl std::ops::Index<HemisphericRegion> for HemisphericSlice {
type Output = FloatValue;
fn index(&self, region: HemisphericRegion) -> &Self::Output {
&self.0[region as usize]
}
}
impl std::ops::IndexMut<HemisphericRegion> for HemisphericSlice {
fn index_mut(&mut self, region: HemisphericRegion) -> &mut Self::Output {
&mut self.0[region as usize]
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum StateValue {
Scalar(FloatValue),
FourBox(FourBoxSlice),
Hemispheric(HemisphericSlice),
}
#[derive(Debug, Clone)]
pub struct InputState<'a> {
current_time: Time,
state: Vec<&'a TimeseriesItem>,
}
impl<'a> InputState<'a> {
pub fn build(values: Vec<&'a TimeseriesItem>, current_time: Time) -> Self {
Self {
current_time,
state: values,
}
}
pub fn empty() -> Self {
Self {
current_time: Time::nan(),
state: vec![],
}
}
pub fn get_global(&self, name: &str) -> Option<FloatValue> {
let item = self.iter().find(|item| item.name == name)?;
match &item.data {
TimeseriesData::Scalar(ts) => match item.variable_type {
VariableType::Exogenous => ts.at_time(self.current_time, ScalarRegion::Global).ok(),
VariableType::Endogenous => ts.latest_value(),
},
TimeseriesData::FourBox(ts) => {
let values = match item.variable_type {
VariableType::Exogenous => ts.at_time_all(self.current_time).ok()?,
VariableType::Endogenous => ts.latest_values(),
};
Some(ts.grid().aggregate_global(&values))
}
TimeseriesData::Hemispheric(ts) => {
let values = match item.variable_type {
VariableType::Exogenous => ts.at_time_all(self.current_time).ok()?,
VariableType::Endogenous => ts.latest_values(),
};
Some(ts.grid().aggregate_global(&values))
}
}
}
pub fn has(&self, name: &str) -> bool {
self.state.iter().any(|x| x.name == name)
}
pub fn iter(&self) -> impl Iterator<Item = &&TimeseriesItem> {
self.state.iter()
}
pub fn current_time(&self) -> Time {
self.current_time
}
pub fn get_scalar_window(&self, name: &str) -> TimeseriesWindow<'_> {
let item = self
.iter()
.find(|item| item.name == name)
.unwrap_or_else(|| panic!("Variable '{}' not found in input state", name));
let ts = item
.data
.as_scalar()
.unwrap_or_else(|| panic!("Variable '{}' is not a scalar timeseries", name));
let current_index = ts
.time_axis()
.index_of(self.current_time)
.unwrap_or_else(|| {
panic!(
"Time {} not found in timeseries '{}' time axis",
self.current_time, name
)
});
TimeseriesWindow::new(ts, current_index, self.current_time)
}
pub fn get_four_box_window(&self, name: &str) -> GridTimeseriesWindow<'_, FourBoxGrid> {
let item = self
.iter()
.find(|item| item.name == name)
.unwrap_or_else(|| panic!("Variable '{}' not found in input state", name));
let ts = item
.data
.as_four_box()
.unwrap_or_else(|| panic!("Variable '{}' is not a FourBox timeseries", name));
let current_index = ts
.time_axis()
.index_of(self.current_time)
.unwrap_or_else(|| {
panic!(
"Time {} not found in timeseries '{}' time axis",
self.current_time, name
)
});
GridTimeseriesWindow::new(ts, current_index, self.current_time)
}
pub fn get_hemispheric_window(&self, name: &str) -> GridTimeseriesWindow<'_, HemisphericGrid> {
let item = self
.iter()
.find(|item| item.name == name)
.unwrap_or_else(|| panic!("Variable '{}' not found in input state", name));
let ts = item
.data
.as_hemispheric()
.unwrap_or_else(|| panic!("Variable '{}' is not a Hemispheric timeseries", name));
let current_index = ts
.time_axis()
.index_of(self.current_time)
.unwrap_or_else(|| {
panic!(
"Time {} not found in timeseries '{}' time axis",
self.current_time, name
)
});
GridTimeseriesWindow::new(ts, current_index, self.current_time)
}
pub fn to_hashmap(self) -> HashMap<String, FloatValue> {
HashMap::from_iter(self.state.into_iter().map(|item| {
let value = match &item.data {
TimeseriesData::Scalar(ts) => ts.latest_value().unwrap(),
TimeseriesData::FourBox(ts) => ts.grid().aggregate_global(&ts.latest_values()),
TimeseriesData::Hemispheric(ts) => ts.grid().aggregate_global(&ts.latest_values()),
};
(item.name.clone(), value)
}))
}
}
impl<'a> IntoIterator for InputState<'a> {
type Item = &'a TimeseriesItem;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.state.into_iter()
}
}
pub type OutputState = HashMap<String, StateValue>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_value_scalar() {
let sv = StateValue::Scalar(42.0);
assert!(sv.is_scalar());
assert!(!sv.is_four_box());
assert!(!sv.is_hemispheric());
assert_eq!(sv.as_scalar(), Some(42.0));
assert_eq!(sv.as_four_box(), None);
assert_eq!(sv.as_hemispheric(), None);
assert_eq!(sv.to_scalar(), 42.0);
}
#[test]
fn test_state_value_four_box() {
let slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
let sv = StateValue::FourBox(slice);
assert!(!sv.is_scalar());
assert!(sv.is_four_box());
assert!(!sv.is_hemispheric());
assert_eq!(sv.as_scalar(), None);
assert_eq!(sv.as_four_box(), Some(&slice));
assert_eq!(sv.as_hemispheric(), None);
assert_eq!(sv.to_scalar(), 2.5); }
#[test]
fn test_state_value_hemispheric() {
let slice = HemisphericSlice::from_array([10.0, 20.0]);
let sv = StateValue::Hemispheric(slice);
assert!(!sv.is_scalar());
assert!(!sv.is_four_box());
assert!(sv.is_hemispheric());
assert_eq!(sv.as_scalar(), None);
assert_eq!(sv.as_four_box(), None);
assert_eq!(sv.as_hemispheric(), Some(&slice));
assert_eq!(sv.to_scalar(), 15.0); }
#[test]
fn test_state_value_from_impls() {
let sv: StateValue = 42.0.into();
assert!(sv.is_scalar());
assert_eq!(sv.as_scalar(), Some(42.0));
let slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
let sv: StateValue = slice.into();
assert!(sv.is_four_box());
let slice = HemisphericSlice::from_array([10.0, 20.0]);
let sv: StateValue = slice.into();
assert!(sv.is_hemispheric());
}
#[test]
fn test_input_state_get_global() {
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::timeseries::{TimeAxis, Timeseries};
use numpy::array;
use numpy::ndarray::Axis;
use std::sync::Arc;
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values = array![280.0, 285.0].insert_axis(Axis(1));
let ts = Timeseries::new(
values,
time_axis,
crate::spatial::ScalarGrid,
"ppm".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
let item = TimeseriesItem {
data: TimeseriesData::Scalar(ts),
name: "CO2".to_string(),
variable_type: VariableType::Endogenous,
};
let state = InputState::build(vec![&item], 2001.0);
assert_eq!(state.get_global("CO2"), Some(285.0));
assert_eq!(state.get_scalar_window("CO2").current(), 285.0);
}
#[test]
fn test_input_state_grid_values() {
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::spatial::FourBoxGrid;
use crate::timeseries::{GridTimeseries, TimeAxis};
use numpy::array;
use numpy::ndarray::Array2;
use std::sync::Arc;
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 item = TimeseriesItem {
data: TimeseriesData::FourBox(ts),
name: "Temperature".to_string(),
variable_type: VariableType::Endogenous,
};
let state = InputState::build(vec![&item], 2001.0);
let window = state.get_four_box_window("Temperature");
let values = window.current_all();
assert_eq!(values, [16.0, 15.0, 11.0, 10.0]);
let global = state.get_global("Temperature").unwrap();
assert_eq!(global, 13.0); }
#[test]
fn test_input_state_to_hashmap_with_grid() {
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::spatial::FourBoxGrid;
use crate::timeseries::{GridTimeseries, TimeAxis};
use numpy::array;
use numpy::ndarray::Array2;
use std::sync::Arc;
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 item = TimeseriesItem {
data: TimeseriesData::FourBox(ts),
name: "Temperature".to_string(),
variable_type: VariableType::Endogenous,
};
let state = InputState::build(vec![&item], 2000.5);
let hashmap = state.to_hashmap();
assert_eq!(hashmap.get("Temperature"), Some(&13.0));
}
}
impl StateValue {
pub fn to_scalar(&self) -> FloatValue {
match self {
StateValue::Scalar(v) => *v,
StateValue::FourBox(slice) => {
let values = slice.as_array();
values.iter().sum::<FloatValue>() / 4.0
}
StateValue::Hemispheric(slice) => {
let values = slice.as_array();
values.iter().sum::<FloatValue>() / 2.0
}
}
}
pub fn is_scalar(&self) -> bool {
matches!(self, StateValue::Scalar(_))
}
pub fn is_four_box(&self) -> bool {
matches!(self, StateValue::FourBox(_))
}
pub fn is_hemispheric(&self) -> bool {
matches!(self, StateValue::Hemispheric(_))
}
pub fn as_scalar(&self) -> Option<FloatValue> {
match self {
StateValue::Scalar(v) => Some(*v),
_ => None,
}
}
pub fn as_four_box(&self) -> Option<&FourBoxSlice> {
match self {
StateValue::FourBox(slice) => Some(slice),
_ => None,
}
}
pub fn as_hemispheric(&self) -> Option<&HemisphericSlice> {
match self {
StateValue::Hemispheric(slice) => Some(slice),
_ => None,
}
}
}
impl From<FloatValue> for StateValue {
fn from(value: FloatValue) -> Self {
StateValue::Scalar(value)
}
}
impl From<FourBoxSlice> for StateValue {
fn from(slice: FourBoxSlice) -> Self {
StateValue::FourBox(slice)
}
}
impl From<HemisphericSlice> for StateValue {
fn from(slice: HemisphericSlice) -> Self {
StateValue::Hemispheric(slice)
}
}
#[cfg(test)]
mod timeseries_window_tests {
use super::*;
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::timeseries::TimeAxis;
use numpy::array;
use numpy::ndarray::{Array, Axis};
use std::sync::Arc;
fn create_scalar_timeseries() -> Timeseries<FloatValue> {
let values = array![1.0, 2.0, 3.0, 4.0, 5.0].insert_axis(Axis(1));
let time_axis = Arc::new(TimeAxis::from_values(Array::range(2000.0, 2005.0, 1.0)));
GridTimeseries::new(
values,
time_axis,
ScalarGrid,
"test".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
)
}
#[test]
fn test_timeseries_window_current() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 2, 2002.0);
assert_eq!(window.current(), 3.0);
assert_eq!(window.time(), 2002.0);
assert_eq!(window.index(), 2);
}
#[test]
fn test_timeseries_window_previous() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 2, 2002.0);
assert_eq!(window.previous(), Some(2.0));
let window_start = TimeseriesWindow::new(&ts, 0, 2000.0);
assert_eq!(window_start.previous(), None);
}
#[test]
fn test_timeseries_window_at_offset() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 2, 2002.0);
assert_eq!(window.at_offset(0), Some(3.0)); assert_eq!(window.at_offset(-1), Some(2.0)); assert_eq!(window.at_offset(-2), Some(1.0)); assert_eq!(window.at_offset(1), Some(4.0)); assert_eq!(window.at_offset(2), Some(5.0)); assert_eq!(window.at_offset(-3), None); assert_eq!(window.at_offset(3), None); }
#[test]
fn test_timeseries_window_last_n() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 4, 2004.0);
let last_3 = window.last_n(3);
assert_eq!(last_3.len(), 3);
assert_eq!(last_3[0], 3.0);
assert_eq!(last_3[1], 4.0);
assert_eq!(last_3[2], 5.0);
let last_1 = window.last_n(1);
assert_eq!(last_1[0], 5.0);
let all = window.last_n(5);
assert_eq!(all.len(), 5);
}
#[test]
#[should_panic(expected = "Cannot get 6 values when only 5 available")]
fn test_timeseries_window_last_n_panic() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 4, 2004.0);
let _ = window.last_n(6); }
#[test]
fn test_timeseries_window_interpolate() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 2, 2002.0);
let mid = window.interpolate(2001.5).unwrap();
assert_eq!(mid, 2.5); }
#[test]
fn test_timeseries_window_len() {
let ts = create_scalar_timeseries();
let window = TimeseriesWindow::new(&ts, 2, 2002.0);
assert_eq!(window.len(), 5);
assert!(!window.is_empty());
}
}
#[cfg(test)]
mod grid_timeseries_window_tests {
use super::*;
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::spatial::FourBoxGrid;
use crate::timeseries::TimeAxis;
use numpy::array;
use numpy::ndarray::Array2;
use std::sync::Arc;
fn create_four_box_timeseries() -> GridTimeseries<FloatValue, FourBoxGrid> {
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, 16.0, 15.0, 11.0, 10.0, 17.0, 16.0, 12.0, 11.0, ],
)
.unwrap();
GridTimeseries::new(
values,
time_axis,
grid,
"C".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
)
}
#[test]
fn test_grid_window_current() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
assert_eq!(window.current(FourBoxRegion::NorthernOcean), 16.0);
assert_eq!(window.current(FourBoxRegion::NorthernLand), 15.0);
assert_eq!(window.current(FourBoxRegion::SouthernOcean), 11.0);
assert_eq!(window.current(FourBoxRegion::SouthernLand), 10.0);
}
#[test]
fn test_grid_window_current_all() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
let all = window.current_all();
assert_eq!(all, vec![16.0, 15.0, 11.0, 10.0]);
}
#[test]
fn test_grid_window_previous() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
assert_eq!(window.previous(FourBoxRegion::NorthernOcean), Some(15.0));
assert_eq!(window.previous_all(), Some(vec![15.0, 14.0, 10.0, 9.0]));
let window_start = GridTimeseriesWindow::new(&ts, 0, 2000.0);
assert_eq!(window_start.previous(FourBoxRegion::NorthernOcean), None);
assert_eq!(window_start.previous_all(), None);
}
#[test]
fn test_grid_window_current_global() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
assert_eq!(window.current_global(), 13.0);
}
#[test]
fn test_grid_window_previous_global() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
assert_eq!(window.previous_global(), Some(12.0));
let window_start = GridTimeseriesWindow::new(&ts, 0, 2000.0);
assert_eq!(window_start.previous_global(), None);
}
#[test]
fn test_grid_window_interpolate() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
let mid = window
.interpolate(2000.5, FourBoxRegion::NorthernOcean)
.unwrap();
assert_eq!(mid, 15.5); }
#[test]
fn test_grid_window_interpolate_all() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
let mid = window.interpolate_all(2000.5).unwrap();
assert_eq!(mid, vec![15.5, 14.5, 10.5, 9.5]);
}
#[test]
fn test_grid_window_metadata() {
let ts = create_four_box_timeseries();
let window = GridTimeseriesWindow::new(&ts, 1, 2001.0);
assert_eq!(window.time(), 2001.0);
assert_eq!(window.index(), 1);
assert_eq!(window.len(), 3);
assert!(!window.is_empty());
assert_eq!(window.grid().size(), 4);
}
}
#[cfg(test)]
mod typed_slice_tests {
use super::*;
use crate::spatial::{FourBoxGrid, HemisphericGrid};
#[test]
fn test_four_box_slice_new() {
let slice = FourBoxSlice::new();
assert!(slice.get(FourBoxRegion::NorthernOcean).is_nan());
assert!(slice.get(FourBoxRegion::SouthernLand).is_nan());
}
#[test]
fn test_four_box_slice_uniform() {
let slice = FourBoxSlice::uniform(15.0);
assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 15.0);
assert_eq!(slice.get(FourBoxRegion::SouthernLand), 15.0);
}
#[test]
fn test_four_box_slice_builder() {
let slice = FourBoxSlice::new()
.with(FourBoxRegion::NorthernOcean, 16.0)
.with(FourBoxRegion::NorthernLand, 15.0)
.with(FourBoxRegion::SouthernOcean, 11.0)
.with(FourBoxRegion::SouthernLand, 10.0);
assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 16.0);
assert_eq!(slice.get(FourBoxRegion::NorthernLand), 15.0);
assert_eq!(slice.get(FourBoxRegion::SouthernOcean), 11.0);
assert_eq!(slice.get(FourBoxRegion::SouthernLand), 10.0);
}
#[test]
fn test_four_box_slice_mutate() {
let mut slice = FourBoxSlice::uniform(0.0);
slice.set(FourBoxRegion::NorthernOcean, 42.0);
assert_eq!(slice.get(FourBoxRegion::NorthernOcean), 42.0);
*slice.get_mut(FourBoxRegion::SouthernLand) = 7.0;
assert_eq!(slice.get(FourBoxRegion::SouthernLand), 7.0);
}
#[test]
fn test_four_box_slice_index() {
let mut slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
assert_eq!(slice[FourBoxRegion::NorthernOcean], 1.0);
assert_eq!(slice[FourBoxRegion::NorthernLand], 2.0);
slice[FourBoxRegion::SouthernOcean] = 99.0;
assert_eq!(slice[FourBoxRegion::SouthernOcean], 99.0);
}
#[test]
fn test_four_box_slice_conversions() {
let slice = FourBoxSlice::from_array([1.0, 2.0, 3.0, 4.0]);
let vec: Vec<FloatValue> = slice.into();
assert_eq!(vec, vec![1.0, 2.0, 3.0, 4.0]);
let slice2: FourBoxSlice = [5.0, 6.0, 7.0, 8.0].into();
let arr: [FloatValue; 4] = slice2.into();
assert_eq!(arr, [5.0, 6.0, 7.0, 8.0]);
}
#[test]
fn test_four_box_slice_aggregate_global() {
let slice = FourBoxSlice::from_array([16.0, 14.0, 12.0, 10.0]);
let grid = FourBoxGrid::magicc_standard();
let global = slice.aggregate_global(&grid);
assert_eq!(global, 13.0);
}
#[test]
fn test_hemispheric_slice_new() {
let slice = HemisphericSlice::new();
assert!(slice.get(HemisphericRegion::Northern).is_nan());
assert!(slice.get(HemisphericRegion::Southern).is_nan());
}
#[test]
fn test_hemispheric_slice_builder() {
let slice = HemisphericSlice::new()
.with(HemisphericRegion::Northern, 15.0)
.with(HemisphericRegion::Southern, 10.0);
assert_eq!(slice.get(HemisphericRegion::Northern), 15.0);
assert_eq!(slice.get(HemisphericRegion::Southern), 10.0);
}
#[test]
fn test_hemispheric_slice_index() {
let mut slice = HemisphericSlice::from_array([15.0, 10.0]);
assert_eq!(slice[HemisphericRegion::Northern], 15.0);
assert_eq!(slice[HemisphericRegion::Southern], 10.0);
slice[HemisphericRegion::Northern] = 20.0;
assert_eq!(slice[HemisphericRegion::Northern], 20.0);
}
#[test]
fn test_hemispheric_slice_aggregate_global() {
let slice = HemisphericSlice::from_array([15.0, 10.0]);
let grid = HemisphericGrid::equal_weights();
let global = slice.aggregate_global(&grid);
assert_eq!(global, 12.5);
}
#[test]
fn test_slice_default() {
let four_box = FourBoxSlice::default();
assert!(four_box.get(FourBoxRegion::NorthernOcean).is_nan());
let hemispheric = HemisphericSlice::default();
assert!(hemispheric.get(HemisphericRegion::Northern).is_nan());
}
}
#[cfg(test)]
mod input_state_window_tests {
use super::*;
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::spatial::{FourBoxGrid, HemisphericGrid};
use crate::timeseries::{GridTimeseries, TimeAxis, Timeseries};
use numpy::array;
use numpy::ndarray::{Array2, Axis};
use std::sync::Arc;
fn create_scalar_item(name: &str, values: Vec<FloatValue>) -> TimeseriesItem {
let n = values.len();
let time_vals: Vec<f64> = (0..n).map(|i| 2000.0 + i as f64).collect();
let time_axis = Arc::new(TimeAxis::from_values(ndarray::Array1::from_vec(time_vals)));
let values_arr = ndarray::Array1::from_vec(values).insert_axis(Axis(1));
let ts = Timeseries::new(
values_arr,
time_axis,
ScalarGrid,
"unit".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
TimeseriesItem {
data: TimeseriesData::Scalar(ts),
name: name.to_string(),
variable_type: VariableType::Endogenous,
}
}
fn create_four_box_item(name: &str) -> TimeseriesItem {
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, 16.0, 15.0, 11.0, 10.0, 17.0, 16.0, 12.0, 11.0, ],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"C".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
TimeseriesItem {
data: TimeseriesData::FourBox(ts),
name: name.to_string(),
variable_type: VariableType::Endogenous,
}
}
fn create_hemispheric_item(name: &str) -> TimeseriesItem {
let grid = HemisphericGrid::equal_weights();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let values = Array2::from_shape_vec(
(2, 2),
vec![
1000.0, 500.0, 1100.0, 550.0, ],
)
.unwrap();
let ts = GridTimeseries::new(
values,
time_axis,
grid,
"mm/yr".to_string(),
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
TimeseriesItem {
data: TimeseriesData::Hemispheric(ts),
name: name.to_string(),
variable_type: VariableType::Endogenous,
}
}
#[test]
fn test_get_scalar_window() {
let item = create_scalar_item("CO2", vec![280.0, 285.0, 290.0, 295.0, 300.0]);
let state = InputState::build(vec![&item], 2002.0);
let window = state.get_scalar_window("CO2");
assert_eq!(window.current(), 290.0);
assert_eq!(window.previous(), Some(285.0));
assert_eq!(window.len(), 5);
}
#[test]
fn test_get_four_box_window() {
let item = create_four_box_item("Temperature");
let state = InputState::build(vec![&item], 2001.0);
let window = state.get_four_box_window("Temperature");
assert_eq!(window.current(FourBoxRegion::NorthernOcean), 16.0);
assert_eq!(window.current(FourBoxRegion::SouthernLand), 10.0);
assert_eq!(window.current_all(), vec![16.0, 15.0, 11.0, 10.0]);
assert_eq!(window.previous_all(), Some(vec![15.0, 14.0, 10.0, 9.0]));
}
#[test]
fn test_get_hemispheric_window() {
let item = create_hemispheric_item("Precipitation");
let state = InputState::build(vec![&item], 2001.0);
let window = state.get_hemispheric_window("Precipitation");
assert_eq!(window.current(HemisphericRegion::Northern), 1100.0);
assert_eq!(window.current(HemisphericRegion::Southern), 550.0);
assert_eq!(window.current_global(), 825.0); }
#[test]
#[should_panic(expected = "Variable 'NonExistent' not found")]
fn test_get_scalar_window_missing_variable() {
let item = create_scalar_item("CO2", vec![280.0, 285.0]);
let state = InputState::build(vec![&item], 2000.0);
let _ = state.get_scalar_window("NonExistent");
}
#[test]
#[should_panic(expected = "not a scalar timeseries")]
fn test_get_scalar_window_wrong_type() {
let item = create_four_box_item("Temperature");
let state = InputState::build(vec![&item], 2000.0);
let _ = state.get_scalar_window("Temperature");
}
#[test]
#[should_panic(expected = "not a FourBox timeseries")]
fn test_get_four_box_window_wrong_type() {
let item = create_scalar_item("CO2", vec![280.0, 285.0]);
let state = InputState::build(vec![&item], 2000.0);
let _ = state.get_four_box_window("CO2");
}
#[test]
fn test_current_time_accessor() {
let item = create_scalar_item("CO2", vec![280.0, 285.0]);
let state = InputState::build(vec![&item], 2023.5);
assert_eq!(state.current_time(), 2023.5);
}
}