use crate::errors::RSCMResult;
pub use crate::state::{
FourBoxSlice, GridTimeseriesWindow, HemisphericSlice, InputState, OutputState, TimeseriesWindow,
};
use crate::timeseries::Time;
use pyo3::{pyclass, pymethods};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
#[pyclass(eq, eq_int)]
#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)]
pub enum RequirementType {
Input,
Output,
State,
EmptyLink,
}
#[pymethods]
impl RequirementType {
#[getter]
fn name(&self) -> &'static str {
match self {
RequirementType::Input => "Input",
RequirementType::Output => "Output",
RequirementType::State => "State",
RequirementType::EmptyLink => "EmptyLink",
}
}
}
#[pyclass(eq, eq_int)]
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Serialize, Deserialize, Default)]
pub enum GridType {
#[default]
Scalar,
FourBox,
Hemispheric,
}
#[pymethods]
impl GridType {
#[getter]
fn name(&self) -> &'static str {
match self {
GridType::Scalar => "Scalar",
GridType::FourBox => "FourBox",
GridType::Hemispheric => "Hemispheric",
}
}
}
impl std::fmt::Display for GridType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GridType::Scalar => write!(f, "Scalar"),
GridType::FourBox => write!(f, "FourBox"),
GridType::Hemispheric => write!(f, "Hemispheric"),
}
}
}
#[pyclass]
#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)]
pub struct RequirementDefinition {
#[pyo3(get, set)]
pub name: String,
#[pyo3(get, set)]
pub unit: String,
#[pyo3(get, set)]
pub requirement_type: RequirementType,
#[pyo3(get, set)]
pub grid_type: GridType,
}
impl RequirementDefinition {
pub fn new(name: &str, unit: &str, requirement_type: RequirementType) -> Self {
Self {
name: name.to_string(),
unit: unit.to_string(),
requirement_type,
grid_type: GridType::Scalar,
}
}
pub fn with_grid(
name: &str,
unit: &str,
requirement_type: RequirementType,
grid_type: GridType,
) -> Self {
Self {
name: name.to_string(),
unit: unit.to_string(),
requirement_type,
grid_type,
}
}
pub fn scalar_input(name: &str, unit: &str) -> Self {
Self::new(name, unit, RequirementType::Input)
}
pub fn scalar_output(name: &str, unit: &str) -> Self {
Self::new(name, unit, RequirementType::Output)
}
pub fn scalar_state(name: &str, unit: &str) -> Self {
Self::new(name, unit, RequirementType::State)
}
pub fn four_box_input(name: &str, unit: &str) -> Self {
Self::with_grid(name, unit, RequirementType::Input, GridType::FourBox)
}
pub fn four_box_output(name: &str, unit: &str) -> Self {
Self::with_grid(name, unit, RequirementType::Output, GridType::FourBox)
}
pub fn four_box_state(name: &str, unit: &str) -> Self {
Self::with_grid(name, unit, RequirementType::State, GridType::FourBox)
}
pub fn hemispheric_input(name: &str, unit: &str) -> Self {
Self::with_grid(name, unit, RequirementType::Input, GridType::Hemispheric)
}
pub fn hemispheric_output(name: &str, unit: &str) -> Self {
Self::with_grid(name, unit, RequirementType::Output, GridType::Hemispheric)
}
pub fn hemispheric_state(name: &str, unit: &str) -> Self {
Self::with_grid(name, unit, RequirementType::State, GridType::Hemispheric)
}
pub fn is_spatial(&self) -> bool {
self.grid_type != GridType::Scalar
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VariableMetadata {
pub rust_name: String,
pub variable_name: String,
pub unit: String,
pub grid: GridType,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ComponentMetadata {
pub name: String,
pub tags: Vec<String>,
pub category: Option<String>,
pub inputs: Vec<VariableMetadata>,
pub outputs: Vec<VariableMetadata>,
pub states: Vec<VariableMetadata>,
}
#[typetag::serde(tag = "type")]
pub trait Component: Debug + Send + Sync {
fn definitions(&self) -> Vec<RequirementDefinition>;
fn inputs(&self) -> Vec<RequirementDefinition> {
self.definitions()
.iter()
.filter(|d| {
matches!(
d.requirement_type,
RequirementType::Input | RequirementType::State
)
})
.cloned()
.collect()
}
fn input_names(&self) -> Vec<String> {
self.inputs().into_iter().map(|d| d.name).collect()
}
fn outputs(&self) -> Vec<RequirementDefinition> {
self.definitions()
.iter()
.filter(|d| {
matches!(
d.requirement_type,
RequirementType::Output | RequirementType::State
)
})
.cloned()
.collect()
}
fn output_names(&self) -> Vec<String> {
self.outputs().into_iter().map(|d| d.name).collect()
}
fn solve(
&self,
t_current: Time,
t_next: Time,
input_state: &InputState,
) -> RSCMResult<OutputState>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::example_components::{TestComponent, TestComponentParameters};
use crate::timeseries::Timeseries;
use crate::timeseries_collection::{TimeseriesItem, VariableType};
use ndarray::array;
#[test]
#[allow(deprecated)]
fn solve() {
let component = TestComponent::from_parameters(TestComponentParameters {
conversion_factor: 2.0,
});
let emissions_co2 = TimeseriesItem {
data: crate::timeseries_collection::TimeseriesData::Scalar(Timeseries::from_values(
array![1.1, 1.3],
array![2020.0, 2021.0],
)),
name: "Emissions|CO2".to_string(),
variable_type: VariableType::Exogenous,
};
let input_state = InputState::build(vec![&emissions_co2], 2020.0);
assert_eq!(
input_state.get_scalar_window("Emissions|CO2").current(),
1.1
);
let output_state = component.solve(2020.0, 2021.0, &input_state).unwrap();
assert_eq!(
output_state.get("Concentrations|CO2").unwrap(),
&crate::state::StateValue::Scalar(1.1 * 2.0)
);
}
#[test]
fn test_requirement_definition_new_is_scalar() {
let req = RequirementDefinition::new("Emissions|CO2", "GtC / yr", RequirementType::Input);
assert_eq!(req.grid_type, GridType::Scalar);
assert!(!req.is_spatial());
}
#[test]
fn test_requirement_definition_with_grid() {
let req = RequirementDefinition::with_grid(
"Temperature",
"K",
RequirementType::Output,
GridType::FourBox,
);
assert_eq!(req.grid_type, GridType::FourBox);
assert!(req.is_spatial());
}
#[test]
fn test_requirement_definition_convenience_constructors() {
let scalar_in = RequirementDefinition::scalar_input("Emissions|CO2", "GtC / yr");
assert_eq!(scalar_in.grid_type, GridType::Scalar);
assert_eq!(scalar_in.requirement_type, RequirementType::Input);
let scalar_out = RequirementDefinition::scalar_output("Concentrations|CO2", "ppm");
assert_eq!(scalar_out.grid_type, GridType::Scalar);
assert_eq!(scalar_out.requirement_type, RequirementType::Output);
let four_box_in = RequirementDefinition::four_box_input("Temperature", "K");
assert_eq!(four_box_in.grid_type, GridType::FourBox);
assert_eq!(four_box_in.requirement_type, RequirementType::Input);
let four_box_out = RequirementDefinition::four_box_output("Surface Temperature", "K");
assert_eq!(four_box_out.grid_type, GridType::FourBox);
assert_eq!(four_box_out.requirement_type, RequirementType::Output);
let hemi_in = RequirementDefinition::hemispheric_input("Precipitation", "mm / yr");
assert_eq!(hemi_in.grid_type, GridType::Hemispheric);
assert_eq!(hemi_in.requirement_type, RequirementType::Input);
}
#[test]
fn test_grid_type_display() {
assert_eq!(format!("{}", GridType::Scalar), "Scalar");
assert_eq!(format!("{}", GridType::FourBox), "FourBox");
assert_eq!(format!("{}", GridType::Hemispheric), "Hemispheric");
}
#[test]
fn test_grid_type_default() {
let default: GridType = Default::default();
assert_eq!(default, GridType::Scalar);
}
}