use rscm_core::component::{
Component, GridType, InputState, OutputState, RequirementDefinition, RequirementType,
TimeseriesWindow,
};
use rscm_core::errors::RSCMResult;
use rscm_core::state::{FourBoxSlice, StateValue};
use rscm_core::timeseries::Time;
use rscm_core::ComponentIO;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FourBoxOceanHeatUptakeParameters {
pub northern_ocean_ratio: f64,
pub northern_land_ratio: f64,
pub southern_ocean_ratio: f64,
pub southern_land_ratio: f64,
}
impl Default for FourBoxOceanHeatUptakeParameters {
fn default() -> Self {
Self {
northern_ocean_ratio: 1.2, northern_land_ratio: 0.6, southern_ocean_ratio: 1.6, southern_land_ratio: 0.6, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ComponentIO)]
#[component(tags = ["temperature", "ocean", "regional", "four-box", "experimental"], category = "Ocean")]
#[inputs(
erf { name = "Effective Radiative Forcing|Aggregated", unit = "W/m^2" },
)]
#[outputs(
heat_uptake { name = "Ocean Heat Uptake|FourBox", unit = "W/m^2", grid = "FourBox" },
)]
pub struct FourBoxOceanHeatUptake {
pub parameters: FourBoxOceanHeatUptakeParameters,
}
impl FourBoxOceanHeatUptake {
pub fn from_parameters(parameters: FourBoxOceanHeatUptakeParameters) -> Self {
let avg = (parameters.northern_ocean_ratio
+ parameters.northern_land_ratio
+ parameters.southern_ocean_ratio
+ parameters.southern_land_ratio)
/ 4.0;
assert!(
(avg - 1.0).abs() < 0.01,
"Regional ratios must average to 1.0 with equal weights (got {})",
avg
);
Self { parameters }
}
}
#[typetag::serde]
impl Component for FourBoxOceanHeatUptake {
fn definitions(&self) -> Vec<RequirementDefinition> {
Self::generated_definitions()
}
fn solve(
&self,
_t_current: Time,
_t_next: Time,
input_state: &InputState,
) -> RSCMResult<OutputState> {
let inputs = FourBoxOceanHeatUptakeInputs::from_input_state(input_state);
let erf = inputs.erf.current();
let outputs = FourBoxOceanHeatUptakeOutputs {
heat_uptake: FourBoxSlice::from_array([
erf * self.parameters.northern_ocean_ratio,
erf * self.parameters.northern_land_ratio,
erf * self.parameters.southern_ocean_ratio,
erf * self.parameters.southern_land_ratio,
]),
};
Ok(outputs.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use numpy::array;
use rscm_core::spatial::SpatialGrid;
use rscm_core::state::StateValue;
use rscm_core::timeseries::Timeseries;
use rscm_core::timeseries_collection::{TimeseriesData, TimeseriesItem, VariableType};
#[test]
fn test_default_parameters_average_to_one() {
let params = FourBoxOceanHeatUptakeParameters::default();
let avg = (params.northern_ocean_ratio
+ params.northern_land_ratio
+ params.southern_ocean_ratio
+ params.southern_land_ratio)
/ 4.0;
assert!((avg - 1.0).abs() < 0.01);
}
#[test]
#[should_panic(expected = "Regional ratios must average to 1.0")]
fn test_invalid_parameters_panic() {
let params = FourBoxOceanHeatUptakeParameters {
northern_ocean_ratio: 2.0,
northern_land_ratio: 2.0,
southern_ocean_ratio: 2.0,
southern_land_ratio: 2.0,
};
FourBoxOceanHeatUptake::from_parameters(params);
}
#[test]
fn test_solve_basic() {
let component =
FourBoxOceanHeatUptake::from_parameters(FourBoxOceanHeatUptakeParameters::default());
let erf_timeseries = TimeseriesItem {
data: TimeseriesData::Scalar(Timeseries::from_values(
array![2.5, 2.5],
array![2020.0, 2021.0],
)),
name: "Effective Radiative Forcing|Aggregated".to_string(),
variable_type: VariableType::Exogenous,
};
let input_state = InputState::build(vec![&erf_timeseries], 2020.0);
let erf_value = input_state
.get_scalar_window("Effective Radiative Forcing|Aggregated")
.current();
println!("ERF input value: {}", erf_value);
let output = component.solve(2020.0, 2021.0, &input_state).unwrap();
assert!(output.contains_key("Ocean Heat Uptake|FourBox"));
let uptake_state = output.get("Ocean Heat Uptake|FourBox").unwrap();
let uptake_slice = match uptake_state {
StateValue::FourBox(slice) => slice,
_ => panic!("Expected FourBox output"),
};
println!("Heat uptake output: {:?}", uptake_slice);
let grid = rscm_core::spatial::FourBoxGrid::magicc_standard();
let global_uptake = grid.aggregate_global(&uptake_slice.0);
assert!((global_uptake - erf_value).abs() < 0.01);
}
#[test]
fn test_solve_with_custom_ratios() {
let params = FourBoxOceanHeatUptakeParameters {
northern_ocean_ratio: 1.5,
northern_land_ratio: 0.5,
southern_ocean_ratio: 1.5,
southern_land_ratio: 0.5,
};
let component = FourBoxOceanHeatUptake::from_parameters(params);
let erf_timeseries = TimeseriesItem {
data: TimeseriesData::Scalar(Timeseries::from_values(
array![10.0, 10.0],
array![2020.0, 2021.0],
)),
name: "Effective Radiative Forcing|Aggregated".to_string(),
variable_type: VariableType::Exogenous,
};
let input_state = InputState::build(vec![&erf_timeseries], 2020.0);
let erf_value = input_state
.get_scalar_window("Effective Radiative Forcing|Aggregated")
.current();
let output = component.solve(2020.0, 2021.0, &input_state).unwrap();
let uptake_state = output.get("Ocean Heat Uptake|FourBox").unwrap();
let uptake_slice = match uptake_state {
StateValue::FourBox(slice) => slice,
_ => panic!("Expected FourBox output"),
};
let grid = rscm_core::spatial::FourBoxGrid::magicc_standard();
let global_uptake = grid.aggregate_global(&uptake_slice.0);
assert!((global_uptake - erf_value).abs() < 0.01);
}
#[test]
fn test_component_definitions() {
let component =
FourBoxOceanHeatUptake::from_parameters(FourBoxOceanHeatUptakeParameters::default());
let inputs = component.input_names();
assert_eq!(inputs.len(), 1);
assert!(inputs.contains(&"Effective Radiative Forcing|Aggregated".to_string()));
let outputs = component.output_names();
assert_eq!(outputs.len(), 1);
assert!(outputs.contains(&"Ocean Heat Uptake|FourBox".to_string()));
}
}