use crate::component::{
Component, InputState, OutputState, RequirementDefinition, RequirementType,
};
use crate::errors::RSCMResult;
use crate::interpolate::strategies::{InterpolationStrategy, LinearSplineStrategy};
use crate::timeseries::{FloatValue, Time, TimeAxis, Timeseries};
use crate::timeseries_collection::{TimeseriesCollection, VariableType};
use numpy::ndarray::Array;
use petgraph::dot::{Config, Dot};
use petgraph::graph::NodeIndex;
use petgraph::visit::{Bfs, IntoNeighbors, IntoNodeIdentifiers, Visitable};
use petgraph::Graph;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::ops::Index;
use std::sync::Arc;
type C = Arc<dyn Component>;
type CGraph = Graph<C, RequirementDefinition>;
#[derive(Debug)]
struct VariableDefinition {
name: String,
unit: String,
}
impl VariableDefinition {
fn from_requirement_definition(definition: &RequirementDefinition) -> Self {
Self {
name: definition.name.clone(),
unit: definition.unit.clone(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct NullComponent {}
#[typetag::serde]
impl Component for NullComponent {
fn definitions(&self) -> Vec<RequirementDefinition> {
vec![]
}
fn solve(
&self,
_t_current: Time,
_t_next: Time,
_input_state: &InputState,
) -> RSCMResult<OutputState> {
Ok(OutputState::new())
}
}
pub struct ModelBuilder {
components: Vec<C>,
exogenous_variables: TimeseriesCollection,
initial_values: HashMap<String, FloatValue>,
pub time_axis: Arc<TimeAxis>,
}
fn verify_definition(
definitions: &mut HashMap<String, VariableDefinition>,
definition: &RequirementDefinition,
) {
let existing = definitions.get(&definition.name);
match existing {
Some(existing) => {
assert_eq!(existing.unit, definition.unit);
}
None => {
definitions.insert(
definition.name.clone(),
VariableDefinition::from_requirement_definition(definition),
);
}
}
}
pub fn extract_state(
collection: &TimeseriesCollection,
input_names: Vec<String>,
t_current: Time,
) -> InputState {
let mut state = Vec::new();
input_names.into_iter().for_each(|name| {
let ts = collection
.get_by_name(name.as_str())
.unwrap_or_else(|| panic!("No timeseries with variable='{}'", name));
state.push(ts);
});
InputState::build(state, t_current)
}
fn is_valid_graph<G>(g: G) -> bool
where
G: IntoNodeIdentifiers + IntoNeighbors + Visitable,
{
use petgraph::visit::{depth_first_search, DfsEvent};
depth_first_search(g, g.node_identifiers(), |event| match event {
DfsEvent::BackEdge(a, b) => {
match a == b {
true => Ok(()),
false => Err(()),
}
}
_ => Ok(()),
})
.is_err()
}
impl ModelBuilder {
pub fn new() -> Self {
Self {
components: vec![],
initial_values: HashMap::new(),
exogenous_variables: TimeseriesCollection::new(),
time_axis: Arc::new(TimeAxis::from_values(Array::range(2000.0, 2100.0, 1.0))),
}
}
pub fn with_component(&mut self, component: Arc<dyn Component + Send + Sync>) -> &mut Self {
self.components.push(component);
self
}
pub fn with_exogenous_variable(
&mut self,
name: &str,
timeseries: Timeseries<FloatValue>,
) -> &mut Self {
self.exogenous_variables.add_timeseries(
name.to_string(),
timeseries,
VariableType::Exogenous,
);
self
}
pub fn with_exogenous_collection(&mut self, collection: TimeseriesCollection) -> &mut Self {
collection.into_iter().for_each(|x| {
self.exogenous_variables
.add_timeseries(x.name, x.timeseries, x.variable_type)
});
self
}
pub fn with_initial_values(
&mut self,
initial_values: HashMap<String, FloatValue>,
) -> &mut Self {
for (name, value) in initial_values.into_iter() {
self.initial_values.insert(name, value);
}
self
}
pub fn with_time_axis(&mut self, time_axis: TimeAxis) -> &mut Self {
self.time_axis = Arc::new(time_axis);
self
}
pub fn build(&self) -> Model {
let mut graph: CGraph = Graph::new();
let mut endrogoneous: HashMap<String, NodeIndex> = HashMap::new();
let mut exogenous: Vec<String> = vec![];
let mut definitions: HashMap<String, VariableDefinition> = HashMap::new();
let initial_node = graph.add_node(Arc::new(NullComponent {}));
self.components.iter().for_each(|component| {
let node = graph.add_node(component.clone());
let mut has_dependencies = false;
let requires = component.inputs();
let provides = component.outputs();
requires.iter().for_each(|requirement| {
verify_definition(&mut definitions, requirement);
if exogenous.contains(&requirement.name) {
graph.add_edge(endrogoneous[&requirement.name], node, requirement.clone());
has_dependencies = true;
} else {
exogenous.push(requirement.name.clone())
}
});
if !has_dependencies {
graph.add_edge(
initial_node,
node,
RequirementDefinition::new("", "", RequirementType::EmptyLink),
);
}
provides.iter().for_each(|requirement| {
verify_definition(&mut definitions, requirement);
let val = endrogoneous.get(&requirement.name);
match val {
None => {
endrogoneous.insert(requirement.name.clone(), node);
}
Some(node_index) => {
graph.add_edge(*node_index, node, requirement.clone());
endrogoneous.insert(requirement.name.clone(), node);
}
}
});
});
assert!(!is_valid_graph(&graph));
let mut collection = TimeseriesCollection::new();
for (name, definition) in definitions {
assert_eq!(definition.name, name);
if exogenous.contains(&name) {
if self.initial_values.contains_key(&name) {
let mut ts = Timeseries::new_empty(
self.time_axis.clone(),
definition.unit,
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
);
ts.set(0, self.initial_values[&name]);
collection.add_timeseries(name, ts, VariableType::Endogenous)
} else {
let timeseries = self.exogenous_variables.get_timeseries_by_name(&name);
match timeseries {
Some(timeseries) => collection.add_timeseries(
name,
timeseries
.to_owned()
.interpolate_into(self.time_axis.clone()),
VariableType::Exogenous,
),
None => println!("No exogenous data for {}", definition.name),
}
}
} else {
collection.add_timeseries(
definition.name,
Timeseries::new_empty(
self.time_axis.clone(),
definition.unit,
InterpolationStrategy::from(LinearSplineStrategy::new(true)),
),
VariableType::Endogenous,
)
}
}
Model::new(graph, initial_node, collection, self.time_axis.clone())
}
}
impl Default for ModelBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Model {
components: CGraph,
initial_node: NodeIndex,
collection: TimeseriesCollection,
time_axis: Arc<TimeAxis>,
time_index: usize,
}
impl Model {
pub fn new(
components: CGraph,
initial_node: NodeIndex,
collection: TimeseriesCollection,
time_axis: Arc<TimeAxis>,
) -> Self {
Self {
components,
initial_node,
collection,
time_axis,
time_index: 0,
}
}
pub fn current_time(&self) -> Time {
self.time_axis.at(self.time_index).unwrap()
}
pub fn current_time_bounds(&self) -> (Time, Time) {
self.time_axis.at_bounds(self.time_index).unwrap()
}
fn step_model_component(&mut self, component: C) {
let input_state = extract_state(
&self.collection,
component.input_names(),
self.current_time(),
);
let (start, end) = self.current_time_bounds();
let result = component.solve(start, end, &input_state);
match result {
Ok(output_state) => output_state.iter().for_each(|(key, value)| {
let ts = self.collection.get_timeseries_by_name_mut(key).unwrap();
ts.set(self.time_index + 1, *value)
}),
Err(err) => {
println!("Solving failed: {}", err)
}
}
}
fn step_model(&mut self) {
let mut bfs = Bfs::new(&self.components, self.initial_node);
while let Some(nx) = bfs.next(&self.components) {
let c = self.components.index(nx);
self.step_model_component(c.clone())
}
}
pub fn step(&mut self) {
assert!(self.time_index < self.time_axis.len() - 1);
self.step_model();
self.time_index += 1;
}
pub fn run(&mut self) {
while self.time_index < self.time_axis.len() - 1 {
self.step();
}
}
pub fn as_dot(&self) -> Dot<&CGraph> {
Dot::with_attr_getters(
&self.components,
&[Config::NodeNoLabel, Config::EdgeNoLabel],
&|_, er| format!("label = {:?}", er.weight().name),
&|_, (_, component)| format!("label = \"{:?}\"", component),
)
}
pub fn finished(&self) -> bool {
self.time_index == self.time_axis.len() - 1
}
pub fn timeseries(&self) -> &TimeseriesCollection {
&self.collection
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::example_components::{TestComponent, TestComponentParameters};
use crate::interpolate::strategies::PreviousStrategy;
use is_close::is_close;
use numpy::array;
use numpy::ndarray::Array;
use std::iter::zip;
fn get_emissions() -> Timeseries<FloatValue> {
Timeseries::new(
array![0.0, 10.0],
Arc::new(TimeAxis::from_bounds(array![1800.0, 1850.0, 2100.0])),
"GtC / yr".to_string(),
InterpolationStrategy::from(PreviousStrategy::new(true)),
)
}
#[test]
fn step() {
let time_axis = TimeAxis::from_values(Array::range(2020.0, 2025.0, 1.0));
let mut model = ModelBuilder::new()
.with_time_axis(time_axis)
.with_component(Arc::new(TestComponent::from_parameters(
TestComponentParameters { p: 0.5 },
)))
.with_exogenous_variable("Emissions|CO2", get_emissions())
.build();
assert_eq!(model.time_index, 0);
model.step();
model.step();
assert_eq!(model.time_index, 2);
assert_eq!(model.current_time(), 2022.0);
model.run();
assert_eq!(model.time_index, 4);
assert!(model.finished());
let concentrations = model
.collection
.get_timeseries_by_name("Concentrations|CO2")
.unwrap();
println!("{:?}", concentrations.values());
assert!(concentrations.at(0).unwrap().is_nan());
let mut iter = concentrations.values().into_iter();
iter.next(); assert!(iter.all(|x| !x.is_nan()));
}
#[test]
fn dot() {
let time_axis = TimeAxis::from_values(Array::range(2020.0, 2025.0, 1.0));
let model = ModelBuilder::new()
.with_time_axis(time_axis)
.with_component(Arc::new(TestComponent::from_parameters(
TestComponentParameters { p: 0.5 },
)))
.with_exogenous_variable("Emissions|CO2", get_emissions())
.build();
let exp = r#"digraph {
0 [ label = "NullComponent"]
1 [ label = "TestComponent { parameters: TestComponentParameters { p: 0.5 } }"]
0 -> 1 [ label = ""]
}
"#;
let res = format!("{:?}", model.as_dot());
assert_eq!(res, exp);
}
#[test]
fn serialise_and_deserialise_model() {
let mut model = ModelBuilder::new()
.with_time_axis(TimeAxis::from_values(Array::range(2020.0, 2025.0, 1.0)))
.with_component(Arc::new(TestComponent::from_parameters(
TestComponentParameters { p: 0.5 },
)))
.with_exogenous_variable("Emissions|CO2", get_emissions())
.build();
model.step();
let serialised = serde_json::to_string_pretty(&model).unwrap();
println!("Pretty JSON");
println!("{}", serialised);
let serialised = toml::to_string(&model).unwrap();
println!("TOML");
println!("{}", serialised);
let expected = r#"initial_node = 0
time_index = 1
[components]
node_holes = []
edge_property = "directed"
edges = [[0, 1, { name = "", unit = "", requirement_type = "EmptyLink" }]]
[[components.nodes]]
type = "NullComponent"
[[components.nodes]]
type = "TestComponent"
[components.nodes.parameters]
p = 0.5
[[collection.timeseries]]
name = "Concentrations|CO2"
variable_type = "Endogenous"
[collection.timeseries.timeseries]
units = "ppm"
latest = 1
interpolation_strategy = "Linear"
[collection.timeseries.timeseries.values]
v = 1
dim = [5]
data = [nan, 5.0, nan, nan, nan]
[collection.timeseries.timeseries.time_axis.bounds]
v = 1
dim = [6]
data = [2020.0, 2021.0, 2022.0, 2023.0, 2024.0, 2025.0]
[[collection.timeseries]]
name = "Emissions|CO2"
variable_type = "Exogenous"
[collection.timeseries.timeseries]
units = "GtC / yr"
latest = 4
interpolation_strategy = "Previous"
[collection.timeseries.timeseries.values]
v = 1
dim = [5]
data = [10.0, 10.0, 10.0, 10.0, 10.0]
[collection.timeseries.timeseries.time_axis.bounds]
v = 1
dim = [6]
data = [2020.0, 2021.0, 2022.0, 2023.0, 2024.0, 2025.0]
[time_axis.bounds]
v = 1
dim = [6]
data = [2020.0, 2021.0, 2022.0, 2023.0, 2024.0, 2025.0]
"#;
assert_eq!(serialised, expected);
let deserialised = toml::from_str::<Model>(&serialised).unwrap();
assert!(zip(
model
.collection
.get_timeseries_by_name("Emissions|CO2")
.unwrap()
.values(),
deserialised
.collection
.get_timeseries_by_name("Emissions|CO2")
.unwrap()
.values()
)
.all(|(x0, x1)| { is_close!(*x0, *x1) || (x0.is_nan() && x0.is_nan()) }));
assert_eq!(model.current_time_bounds(), (2021.0, 2022.0));
assert_eq!(deserialised.current_time_bounds(), (2021.0, 2022.0));
}
}