Skip to content

Commit

Permalink
feat: Add initial value for inter-network transfers.
Browse files Browse the repository at this point in the history
This helps support circular transfers between networks. Added
a basic test model with circular transfers.
  • Loading branch information
jetuk committed Oct 23, 2023
1 parent 15fa0bf commit 16104e6
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 16 deletions.
40 changes: 31 additions & 9 deletions pywr-core/src/models/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::network::{Network, NetworkState, RunTimings};
use crate::parameters::{downcast_internal_state, ParameterIndex};
use crate::scenario::ScenarioIndex;
use crate::solvers::{Solver, SolverSettings};
use crate::timestep::Timestep;
use crate::PywrError;
use std::any::Any;
use std::fmt;
Expand Down Expand Up @@ -34,12 +35,14 @@ impl OtherNetworkIndex {

/// A special parameter that retrieves a value from a metric in another model.
struct MultiNetworkTransfer {
// The model to get the value from.
/// The model to get the value from.
from_model_idx: OtherNetworkIndex,
// The metric to get the value from.
/// The metric to get the value from.
from_metric: Metric,
// The parameter to save the value to.
/// The parameter to save the value to.
to_parameter_idx: ParameterIndex,
/// Optional initial value to use on the first time-step
initial_value: Option<f64>,
}

struct MultiNetworkEntry {
Expand Down Expand Up @@ -82,6 +85,14 @@ impl MultiNetworkModel {
.ok_or(PywrError::NetworkIndexNotFound(idx))
}

/// Get a mutable reference to a network by index.
pub fn network_mut(&mut self, idx: usize) -> Result<&mut Network, PywrError> {
self.networks
.get_mut(idx)
.map(|n| &mut n.network)
.ok_or(PywrError::NetworkIndexNotFound(idx))
}

/// Get the index of a network by name.
pub fn get_network_index_by_name(&self, name: &str) -> Result<usize, PywrError> {
self.networks
Expand All @@ -102,17 +113,20 @@ impl MultiNetworkModel {
idx
}

pub fn add_parameter(
/// Add a transfer of data from one network to another.
pub fn add_inter_network_transfer(
&mut self,
from_network_idx: usize,
from_metric: Metric,
to_network_idx: usize,
to_parameter_idx: ParameterIndex,
initial_value: Option<f64>,
) {
let parameter = MultiNetworkTransfer {
from_model_idx: OtherNetworkIndex::new(from_network_idx, to_network_idx),
from_metric,
to_parameter_idx,
initial_value,
};

self.networks[to_network_idx].parameters.push(parameter);
Expand Down Expand Up @@ -151,6 +165,7 @@ impl MultiNetworkModel {
fn compute_inter_model_transfers(
&self,
model_idx: usize,
timestep: &Timestep,
scenario_indices: &[ScenarioIndex],
states: &mut [NetworkState],
) -> Result<(), PywrError> {
Expand All @@ -164,6 +179,7 @@ impl MultiNetworkModel {
// Compute inter-model transfers for all scenarios
for scenario_index in scenario_indices.iter() {
compute_inter_model_transfers(
timestep,
scenario_index,
&this_model.parameters,
this_models_state,
Expand Down Expand Up @@ -195,7 +211,7 @@ impl MultiNetworkModel {

for (idx, entry) in self.networks.iter().enumerate() {
// Perform inter-model state updates
self.compute_inter_model_transfers(idx, scenario_indices, &mut state.states)?;
self.compute_inter_model_transfers(idx, timestep, scenario_indices, &mut state.states)?;

let sub_model_solvers = state.solvers.get_mut(idx).unwrap();
let sub_model_states = state.states.get_mut(idx).unwrap();
Expand Down Expand Up @@ -284,6 +300,7 @@ impl MultiNetworkModel {
///
///
fn compute_inter_model_transfers(
timestep: &Timestep,
scenario_index: &ScenarioIndex,
inter_model_transfers: &[MultiNetworkTransfer],
state: &mut NetworkState,
Expand All @@ -302,10 +319,15 @@ fn compute_inter_model_transfers(
}
OtherNetworkIndex::After(i) => (&after_models[i.get() - 1], &after_states[i.get() - 1]),
};
// Get the value from the other model's state/metric
let value = parameter
.from_metric
.get_value(&other_model.network, other_model_state.state(scenario_index))?;

let value = match timestep.is_first().then(|| parameter.initial_value).flatten() {
// Use the initial value if it is given and it is the first time-step.
Some(initial_value) => initial_value,
// Otherwise, get the value from the other model's state/metric
None => parameter
.from_metric
.get_value(&other_model.network, other_model_state.state(scenario_index))?,
};

// Save the value in the internal state of receiving network's parameter
// This will panic if the parameter index points to the wrong type of parameter (i.e.
Expand Down
101 changes: 99 additions & 2 deletions pywr-schema/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ pub struct PywrMultiNetworkTransfer {
pub from_network: String,
pub metric: MetricFloatReference,
pub to_parameter: String,
pub initial_value: Option<f64>,
}

#[derive(serde::Deserialize, serde::Serialize, Clone)]
Expand Down Expand Up @@ -431,7 +432,13 @@ impl PywrMultiNetworkModel {

let to_parameter_idx = to_network.get_parameter_index_by_name(&transfer.to_parameter)?;

model.add_parameter(from_network_idx, from_metric, to_network_idx, to_parameter_idx);
model.add_inter_network_transfer(
from_network_idx,
from_metric,
to_network_idx,
to_parameter_idx,
transfer.initial_value,
);
}
}

Expand Down Expand Up @@ -607,7 +614,97 @@ mod tests {
model_fn.push("src/test_models/multi1/model.json");

let schema = PywrMultiNetworkModel::from_path(model_fn.as_path()).unwrap();
let model = schema.build_model(model_fn.parent(), None).unwrap();
let mut model = schema.build_model(model_fn.parent(), None).unwrap();

// Add some recorders for the expected outputs
let network_1_idx = model
.get_network_index_by_name("network1")
.expect("network 1 not found");
let network_1 = model.network_mut(network_1_idx).expect("network 1 not found");
let demand1_idx = network_1.get_node_index_by_name("demand1", None).unwrap();

let expected_values: Array1<f64> = [10.0; 365].to_vec().into();
let expected_values: Array2<f64> = expected_values.insert_axis(Axis(1));

let rec = AssertionRecorder::new(
"assert-demand1",
Metric::NodeInFlow(demand1_idx),
expected_values,
None,
None,
);
network_1.add_recorder(Box::new(rec)).unwrap();

// Inflow to demand2 should be 10.0 via the transfer from network1 (demand1)
let network_2_idx = model
.get_network_index_by_name("network2")
.expect("network 1 not found");
let network_2 = model.network_mut(network_2_idx).expect("network 2 not found");
let demand1_idx = network_2.get_node_index_by_name("demand2", None).unwrap();

let expected_values: Array1<f64> = [10.0; 365].to_vec().into();
let expected_values: Array2<f64> = expected_values.insert_axis(Axis(1));

let rec = AssertionRecorder::new(
"assert-demand2",
Metric::NodeInFlow(demand1_idx),
expected_values,
None,
None,
);
network_2.add_recorder(Box::new(rec)).unwrap();

model.run::<ClpSolver>(&Default::default()).unwrap();
}

/// Test the multi2 model
#[test]
fn test_multi2_model() {
let mut model_fn = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
model_fn.push("src/test_models/multi2/model.json");

let schema = PywrMultiNetworkModel::from_path(model_fn.as_path()).unwrap();
let mut model = schema.build_model(model_fn.parent(), None).unwrap();

// Add some recorders for the expected outputs
// inflow1 should be set to a max of 20.0 from the "demand" parameter in network2
let network_1_idx = model
.get_network_index_by_name("network1")
.expect("network 1 not found");
let network_1 = model.network_mut(network_1_idx).expect("network 1 not found");
let demand1_idx = network_1.get_node_index_by_name("demand1", None).unwrap();

let expected_values: Array1<f64> = [10.0; 365].to_vec().into();
let expected_values: Array2<f64> = expected_values.insert_axis(Axis(1));

let rec = AssertionRecorder::new(
"assert-demand1",
Metric::NodeInFlow(demand1_idx),
expected_values,
None,
None,
);
network_1.add_recorder(Box::new(rec)).unwrap();

// Inflow to demand2 should be 10.0 via the transfer from network1 (demand1)
let network_2_idx = model
.get_network_index_by_name("network2")
.expect("network 1 not found");
let network_2 = model.network_mut(network_2_idx).expect("network 2 not found");
let demand1_idx = network_2.get_node_index_by_name("demand2", None).unwrap();

let expected_values: Array1<f64> = [10.0; 365].to_vec().into();
let expected_values: Array2<f64> = expected_values.insert_axis(Axis(1));

let rec = AssertionRecorder::new(
"assert-demand2",
Metric::NodeInFlow(demand1_idx),
expected_values,
None,
None,
);
network_2.add_recorder(Box::new(rec)).unwrap();

model.run::<ClpSolver>(&Default::default()).unwrap();
}
}
10 changes: 5 additions & 5 deletions pywr-schema/src/test_models/multi1/model.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
},
"networks": [
{
"name": "sub-model1",
"network": "sub-model1.json",
"name": "network1",
"network": "network1.json",
"transfers": []
},
{
"name": "sub-model2",
"network": "sub-model2.json",
"name": "network2",
"network": "network2.json",
"transfers": [
{
"from_network": "sub-model1",
"from_network": "network1",
"metric": {
"type": "NodeInFlow",
"name": "demand1"
Expand Down
44 changes: 44 additions & 0 deletions pywr-schema/src/test_models/multi2/model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"metadata": {
"title": "Multi-model 1",
"description": "A simple multi-model that passes data from sub-model1 to sub-model2, and back again",
"minimum_version": "0.1"
},
"timestepper": {
"start": "2015-01-01",
"end": "2015-12-31",
"timestep": 1
},
"networks": [
{
"name": "network1",
"network": "network1.json",
"transfers": [
{
"from_network": "network2",
"metric": {
"type": "Parameter",
"name": "demand"
},
"to_parameter": "inflow",
"initial_value": 10.0
}
]
},
{
"name": "network2",
"network": "network2.json",
"transfers": [
{
"from_network": "network1",
"metric": {
"type": "NodeInFlow",
"name": "demand1"
},
"to_parameter": "inflow"
}
]
}
]

}
46 changes: 46 additions & 0 deletions pywr-schema/src/test_models/multi2/network1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"nodes": [
{
"name": "supply1",
"type": "Input",
"max_flow": {
"type": "Parameter",
"name": "inflow"
}
},
{
"name": "link1",
"type": "Link"
},
{
"name": "demand1",
"type": "Output",
"max_flow": {
"type": "Parameter",
"name": "demand"
},
"cost": -10
}
],
"edges": [
{
"from_node": "supply1",
"to_node": "link1"
},
{
"from_node": "link1",
"to_node": "demand1"
}
],
"parameters": [
{
"name": "demand",
"type": "Constant",
"value": 10.0
},
{
"name": "inflow",
"type": "InterModelTransfer"
}
]
}
46 changes: 46 additions & 0 deletions pywr-schema/src/test_models/multi2/network2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"nodes": [
{
"name": "supply2",
"type": "Input",
"max_flow": {
"type": "Parameter",
"name": "inflow"
}
},
{
"name": "link2",
"type": "Link"
},
{
"name": "demand2",
"type": "Output",
"max_flow": {
"type": "Parameter",
"name": "demand"
},
"cost": -10
}
],
"edges": [
{
"from_node": "supply2",
"to_node": "link2"
},
{
"from_node": "link2",
"to_node": "demand2"
}
],
"parameters": [
{
"name": "demand",
"type": "Constant",
"value": 20.0
},
{
"name": "inflow",
"type": "InterModelTransfer"
}
]
}

0 comments on commit 16104e6

Please sign in to comment.