use crate::component::{
Component, GridType, InputState, OutputState, RequirementDefinition, RequirementType,
};
use crate::errors::RSCMResult;
use crate::spatial::{FourBoxGrid, HemisphericGrid, SpatialGrid};
use crate::timeseries::Time;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FourBoxToScalarTransform {
variable_name: String,
unit: String,
grid: FourBoxGrid,
}
impl FourBoxToScalarTransform {
pub fn new(variable_name: &str, unit: &str, grid: FourBoxGrid) -> Self {
Self {
variable_name: variable_name.to_string(),
unit: unit.to_string(),
grid,
}
}
pub fn with_standard_weights(variable_name: &str, unit: &str) -> Self {
Self::new(variable_name, unit, FourBoxGrid::magicc_standard())
}
fn input_name(&self) -> String {
format!("{}|FourBox", self.variable_name)
}
}
#[typetag::serde]
impl Component for FourBoxToScalarTransform {
fn definitions(&self) -> Vec<RequirementDefinition> {
vec![
RequirementDefinition::with_grid(
&self.input_name(),
&self.unit,
RequirementType::Input,
GridType::FourBox,
),
RequirementDefinition::with_grid(
&self.variable_name,
&self.unit,
RequirementType::Output,
GridType::Scalar,
),
]
}
fn solve(
&self,
_t_current: Time,
_t_next: Time,
input_state: &InputState,
) -> RSCMResult<OutputState> {
let window = input_state.get_four_box_window(&self.input_name());
let values = window.current_all();
let global = self.grid.aggregate_global(&values);
let mut output = HashMap::new();
output.insert(
self.variable_name.clone(),
crate::state::StateValue::Scalar(global),
);
Ok(output)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FourBoxToHemisphericTransform {
variable_name: String,
unit: String,
grid: FourBoxGrid,
}
impl FourBoxToHemisphericTransform {
pub fn new(variable_name: &str, unit: &str, grid: FourBoxGrid) -> Self {
Self {
variable_name: variable_name.to_string(),
unit: unit.to_string(),
grid,
}
}
pub fn with_standard_weights(variable_name: &str, unit: &str) -> Self {
Self::new(variable_name, unit, FourBoxGrid::magicc_standard())
}
fn input_name(&self) -> String {
format!("{}|FourBox", self.variable_name)
}
fn output_name(&self) -> String {
format!("{}|Hemispheric", self.variable_name)
}
}
#[typetag::serde]
impl Component for FourBoxToHemisphericTransform {
fn definitions(&self) -> Vec<RequirementDefinition> {
vec![
RequirementDefinition::with_grid(
&self.input_name(),
&self.unit,
RequirementType::Input,
GridType::FourBox,
),
RequirementDefinition::with_grid(
&self.output_name(),
&self.unit,
RequirementType::Output,
GridType::Hemispheric,
),
]
}
fn solve(
&self,
_t_current: Time,
_t_next: Time,
input_state: &InputState,
) -> RSCMResult<OutputState> {
let window = input_state.get_four_box_window(&self.input_name());
let values = window.current_all();
let target = HemisphericGrid::equal_weights();
let hemispheric = self.grid.transform_to(&values, &target)?;
let mut output = HashMap::new();
output.insert(
self.output_name(),
crate::state::StateValue::Hemispheric(crate::state::HemisphericSlice::from_array([
hemispheric[0],
hemispheric[1],
])),
);
Ok(output)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HemisphericToScalarTransform {
variable_name: String,
unit: String,
grid: HemisphericGrid,
}
impl HemisphericToScalarTransform {
pub fn new(variable_name: &str, unit: &str, grid: HemisphericGrid) -> Self {
Self {
variable_name: variable_name.to_string(),
unit: unit.to_string(),
grid,
}
}
pub fn with_equal_weights(variable_name: &str, unit: &str) -> Self {
Self::new(variable_name, unit, HemisphericGrid::equal_weights())
}
fn input_name(&self) -> String {
format!("{}|Hemispheric", self.variable_name)
}
}
#[typetag::serde]
impl Component for HemisphericToScalarTransform {
fn definitions(&self) -> Vec<RequirementDefinition> {
vec![
RequirementDefinition::with_grid(
&self.input_name(),
&self.unit,
RequirementType::Input,
GridType::Hemispheric,
),
RequirementDefinition::with_grid(
&self.variable_name,
&self.unit,
RequirementType::Output,
GridType::Scalar,
),
]
}
fn solve(
&self,
_t_current: Time,
_t_next: Time,
input_state: &InputState,
) -> RSCMResult<OutputState> {
let window = input_state.get_hemispheric_window(&self.input_name());
let values = window.current_all();
let global = self.grid.aggregate_global(&values);
let mut output = HashMap::new();
output.insert(
self.variable_name.clone(),
crate::state::StateValue::Scalar(global),
);
Ok(output)
}
}
pub fn grids_compatible(producer: GridType, consumer: GridType) -> bool {
match (producer, consumer) {
(GridType::Scalar, GridType::Scalar) => true,
(GridType::FourBox, GridType::FourBox) => true,
(GridType::Hemispheric, GridType::Hemispheric) => true,
(GridType::FourBox, GridType::Scalar) => true,
(GridType::FourBox, GridType::Hemispheric) => true,
(GridType::Hemispheric, GridType::Scalar) => true,
(GridType::Scalar, GridType::FourBox) => false,
(GridType::Scalar, GridType::Hemispheric) => false,
(GridType::Hemispheric, GridType::FourBox) => false,
}
}
pub fn needs_transform(producer: GridType, consumer: GridType) -> bool {
producer != consumer && grids_compatible(producer, consumer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grids_compatible_same_type() {
assert!(grids_compatible(GridType::Scalar, GridType::Scalar));
assert!(grids_compatible(GridType::FourBox, GridType::FourBox));
assert!(grids_compatible(
GridType::Hemispheric,
GridType::Hemispheric
));
}
#[test]
fn test_grids_compatible_finer_to_coarser() {
assert!(grids_compatible(GridType::FourBox, GridType::Scalar));
assert!(grids_compatible(GridType::FourBox, GridType::Hemispheric));
assert!(grids_compatible(GridType::Hemispheric, GridType::Scalar));
}
#[test]
fn test_grids_incompatible_coarser_to_finer() {
assert!(!grids_compatible(GridType::Scalar, GridType::FourBox));
assert!(!grids_compatible(GridType::Scalar, GridType::Hemispheric));
assert!(!grids_compatible(GridType::Hemispheric, GridType::FourBox));
}
#[test]
fn test_needs_transform() {
assert!(!needs_transform(GridType::Scalar, GridType::Scalar));
assert!(!needs_transform(GridType::FourBox, GridType::FourBox));
assert!(needs_transform(GridType::FourBox, GridType::Scalar));
assert!(needs_transform(GridType::FourBox, GridType::Hemispheric));
assert!(needs_transform(GridType::Hemispheric, GridType::Scalar));
assert!(!needs_transform(GridType::Scalar, GridType::FourBox));
}
#[test]
fn test_four_box_to_scalar_transform_definitions() {
let transform = FourBoxToScalarTransform::with_standard_weights("Temperature", "K");
let defs = transform.definitions();
assert_eq!(defs.len(), 2);
assert_eq!(defs[0].name, "Temperature|FourBox");
assert_eq!(defs[0].grid_type, GridType::FourBox);
assert_eq!(defs[1].name, "Temperature");
assert_eq!(defs[1].grid_type, GridType::Scalar);
}
#[test]
fn test_hemispheric_to_scalar_transform_definitions() {
let transform = HemisphericToScalarTransform::with_equal_weights("Precipitation", "mm/yr");
let defs = transform.definitions();
assert_eq!(defs.len(), 2);
assert_eq!(defs[0].name, "Precipitation|Hemispheric");
assert_eq!(defs[0].grid_type, GridType::Hemispheric);
assert_eq!(defs[1].name, "Precipitation");
assert_eq!(defs[1].grid_type, GridType::Scalar);
}
mod integration_tests {
use super::*;
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::spatial::FourBoxGrid;
use crate::timeseries::{FloatValue, GridTimeseries, TimeAxis};
use crate::timeseries_collection::{TimeseriesData, TimeseriesItem, VariableType};
use numpy::array;
use numpy::ndarray::Array2;
use std::sync::Arc;
fn create_four_box_item(name: &str, values: [FloatValue; 4]) -> TimeseriesItem {
let grid = FourBoxGrid::magicc_standard();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let data = Array2::from_shape_vec(
(2, 4),
vec![
values[0], values[1], values[2], values[3], values[0], values[1], values[2], values[3], ],
)
.unwrap();
let ts = GridTimeseries::new(
data,
time_axis,
grid,
"K".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, values: [FloatValue; 2]) -> TimeseriesItem {
use crate::spatial::HemisphericGrid;
let grid = HemisphericGrid::equal_weights();
let time_axis = Arc::new(TimeAxis::from_values(array![2000.0, 2001.0]));
let data = Array2::from_shape_vec(
(2, 2),
vec![
values[0], values[1], values[0], values[1], ],
)
.unwrap();
let ts = GridTimeseries::new(
data,
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_four_box_to_scalar_transform_solve() {
let transform = FourBoxToScalarTransform::with_standard_weights("Temperature", "K");
let item = create_four_box_item("Temperature|FourBox", [16.0, 14.0, 12.0, 10.0]);
let input_state = InputState::build(vec![&item], 2000.0);
let output = transform.solve(2000.0, 2001.0, &input_state).unwrap();
assert_eq!(
output.get("Temperature"),
Some(&crate::state::StateValue::Scalar(13.0))
);
}
#[test]
#[should_panic(expected = "Variable 'Temperature|FourBox' not found in input state")]
fn test_four_box_to_scalar_transform_with_missing_input() {
let transform = FourBoxToScalarTransform::with_standard_weights("Temperature", "K");
let input_state = InputState::empty();
let _ = transform.solve(2000.0, 2001.0, &input_state);
}
#[test]
fn test_hemispheric_to_scalar_transform_solve() {
let transform =
HemisphericToScalarTransform::with_equal_weights("Precipitation", "mm/yr");
let item = create_hemispheric_item("Precipitation|Hemispheric", [1000.0, 500.0]);
let input_state = InputState::build(vec![&item], 2000.0);
let output = transform.solve(2000.0, 2001.0, &input_state).unwrap();
assert_eq!(
output.get("Precipitation"),
Some(&crate::state::StateValue::Scalar(750.0))
);
}
#[test]
fn test_hemispheric_to_scalar_with_weighted_grid() {
let grid = HemisphericGrid::with_weights([0.7, 0.3]);
let transform = HemisphericToScalarTransform::new("Precipitation", "mm/yr", grid);
let item = create_hemispheric_item("Precipitation|Hemispheric", [1000.0, 500.0]);
let input_state = InputState::build(vec![&item], 2000.0);
let output = transform.solve(2000.0, 2001.0, &input_state).unwrap();
assert_eq!(
output.get("Precipitation"),
Some(&crate::state::StateValue::Scalar(850.0))
);
}
#[test]
fn test_four_box_to_scalar_with_weighted_grid() {
let grid = FourBoxGrid::with_weights([0.4, 0.1, 0.4, 0.1]);
let transform = FourBoxToScalarTransform::new("Temperature", "K", grid);
let item = create_four_box_item("Temperature|FourBox", [16.0, 14.0, 12.0, 10.0]);
let input_state = InputState::build(vec![&item], 2000.0);
let output = transform.solve(2000.0, 2001.0, &input_state).unwrap();
let expected = 16.0 * 0.4 + 14.0 * 0.1 + 12.0 * 0.4 + 10.0 * 0.1;
let value = output.get("Temperature").unwrap();
if let crate::state::StateValue::Scalar(v) = value {
assert!((v - expected).abs() < 1e-10);
} else {
panic!("Expected scalar output");
}
}
}
}