diff --git a/pywr-schema/src/nodes/loss_link.rs b/pywr-schema/src/nodes/loss_link.rs index 01a24078..3c0c84f6 100644 --- a/pywr-schema/src/nodes/loss_link.rs +++ b/pywr-schema/src/nodes/loss_link.rs @@ -26,6 +26,8 @@ pub enum LossFactor { #[cfg(feature = "core")] impl LossFactor { + /// Load the loss factor and return a corresponding [`Relationship`] if the loss factor is + /// not a constant zero. If a zero is loaded, then `None` is returned. pub fn load( &self, network: &mut pywr_core::network::Network, @@ -34,7 +36,7 @@ impl LossFactor { match self { LossFactor::Gross { factor } => { let lf = factor.load(network, args)?; - // Handle the case where we a given a zero loss factor + // Handle the case where we are given a zero loss factor // The aggregated node does not support zero loss factors so filter them here. if lf.is_constant_zero() { return Ok(None); @@ -173,6 +175,11 @@ impl LossLinkNode { if let Some(loss_factor) = &self.loss_factor { let factors = loss_factor.load(network, args)?; + + if factors.is_none() { + // Loaded a constant zero factor; ensure that the loss node has zero flow + network.set_node_max_flow(self.meta.name.as_str(), Self::loss_sub_name(), Some(0.0.into()))?; + } network.set_aggregated_node_relationship(self.meta.name.as_str(), Self::agg_sub_name(), factors)?; } @@ -289,7 +296,7 @@ mod tests { } #[test] - fn test_model_schema() { + fn test_loss_link1_schema() { let data = loss_link1_str(); let schema: PywrModel = serde_json::from_str(data).unwrap(); @@ -299,7 +306,7 @@ mod tests { #[test] #[cfg(feature = "core")] - fn test_model_run() { + fn test_loss_link1_run() { let data = loss_link1_str(); let schema: PywrModel = serde_json::from_str(data).unwrap(); let temp_dir = TempDir::new().unwrap(); @@ -314,4 +321,40 @@ mod tests { // Test all solvers run_all_solvers(&model, &[], &expected_outputs); } + + fn loss_link2_str() -> &'static str { + include_str!("../test_models/loss_link2.json") + } + + #[cfg(feature = "core")] + fn loss_link2_outputs_str() -> &'static str { + include_str!("../test_models/loss_link2-expected.csv") + } + + #[test] + fn test_loss_link2_schema() { + let data = loss_link2_str(); + let schema: PywrModel = serde_json::from_str(data).unwrap(); + + assert_eq!(schema.network.nodes.len(), 4); + assert_eq!(schema.network.edges.len(), 3); + } + + #[test] + #[cfg(feature = "core")] + fn test_loss_link2_run() { + let data = loss_link2_str(); + let schema: PywrModel = serde_json::from_str(data).unwrap(); + let temp_dir = TempDir::new().unwrap(); + + let model = schema.build_model(None, Some(temp_dir.path())).unwrap(); + // After model run there should be an output file. + let expected_outputs = [ExpectedOutputs::new( + temp_dir.path().join("loss_link2.csv"), + loss_link2_outputs_str(), + )]; + + // Test all solvers + run_all_solvers(&model, &[], &expected_outputs); + } } diff --git a/pywr-schema/src/nodes/water_treatment_works.rs b/pywr-schema/src/nodes/water_treatment_works.rs index 846f4ade..a827b981 100644 --- a/pywr-schema/src/nodes/water_treatment_works.rs +++ b/pywr-schema/src/nodes/water_treatment_works.rs @@ -181,6 +181,10 @@ impl WaterTreatmentWorks { if let Some(loss_factor) = &self.loss_factor { let factors = loss_factor.load(network, args)?; + if factors.is_none() { + // Loaded a constant zero factor; ensure that the loss node has zero flow + network.set_node_max_flow(self.meta.name.as_str(), Self::loss_sub_name(), Some(0.0.into()))?; + } network.set_aggregated_node_relationship(self.meta.name.as_str(), Self::agg_sub_name(), factors)?; } @@ -255,7 +259,7 @@ mod tests { } #[test] - fn test_model_schema() { + fn test_wtw1_schema() { let data = wtw1_str(); let schema: PywrModel = serde_json::from_str(data).unwrap(); @@ -265,7 +269,7 @@ mod tests { #[test] #[cfg(feature = "core")] - fn test_model_run() { + fn test_wtw1_run() { let data = wtw1_str(); let schema: PywrModel = serde_json::from_str(data).unwrap(); let temp_dir = TempDir::new().unwrap(); @@ -285,4 +289,45 @@ mod tests { // Test all solvers run_all_solvers(&model, &[], &expected_outputs); } + + fn wtw2_str() -> &'static str { + include_str!("../test_models/wtw2.json") + } + + #[cfg(feature = "core")] + fn wtw2_outputs_str() -> &'static str { + include_str!("../test_models/wtw2-expected.csv") + } + + #[test] + fn test_wtw2_schema() { + let data = wtw2_str(); + let schema: PywrModel = serde_json::from_str(data).unwrap(); + + assert_eq!(schema.network.nodes.len(), 4); + assert_eq!(schema.network.edges.len(), 3); + } + + #[test] + #[cfg(feature = "core")] + fn test_wtw2_run() { + let data = wtw2_str(); + let schema: PywrModel = serde_json::from_str(data).unwrap(); + let temp_dir = TempDir::new().unwrap(); + + let mut model = schema.build_model(None, Some(temp_dir.path())).unwrap(); + + let network = model.network_mut(); + assert_eq!(network.nodes().len(), 7); + assert_eq!(network.edges().len(), 7); + + // After model run there should be an output file. + let expected_outputs = [ExpectedOutputs::new( + temp_dir.path().join("wtw2.csv"), + wtw2_outputs_str(), + )]; + + // Test all solvers + run_all_solvers(&model, &[], &expected_outputs); + } } diff --git a/pywr-schema/src/test_models/loss_link2-expected.csv b/pywr-schema/src/test_models/loss_link2-expected.csv new file mode 100644 index 00000000..e2dab0f7 --- /dev/null +++ b/pywr-schema/src/test_models/loss_link2-expected.csv @@ -0,0 +1,13 @@ +time_start,time_end,scenario_index,metric_set,name,attribute,value +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,loss1,Inflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,loss1,Outflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,loss1,Loss,0.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,spill1,Inflow,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,loss1,Inflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,loss1,Outflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,loss1,Loss,0.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,spill1,Inflow,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,loss1,Inflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,loss1,Outflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,loss1,Loss,0.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,spill1,Inflow,5.0 diff --git a/pywr-schema/src/test_models/loss_link2.json b/pywr-schema/src/test_models/loss_link2.json new file mode 100644 index 00000000..e233e7ac --- /dev/null +++ b/pywr-schema/src/test_models/loss_link2.json @@ -0,0 +1,116 @@ +{ + "metadata": { + "title": "Loss Link Test 1", + "description": "Test LossLink with zero loss", + "minimum_version": "0.1" + }, + "timestepper": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": 1 + }, + "network": { + "nodes": [ + { + "meta": { + "name": "input1" + }, + "type": "Catchment", + "flow": { + "type": "Constant", + "value": 15.0 + } + }, + { + "meta": { + "name": "loss1" + }, + "type": "LossLink", + "loss_factor": { + "type": "Net", + "factor": { + "type": "Constant", + "value": 0.0 + } + } + }, + { + "meta": { + "name": "demand1" + }, + "type": "Output", + "max_flow": { + "type": "Constant", + "value": 10.0 + }, + "cost": { + "type": "Constant", + "value": -10 + } + }, + { + "meta": { + "name": "spill1" + }, + "type": "Output", + "cost": { + "type": "Constant", + "value": 10 + } + } + ], + "edges": [ + { + "from_node": "input1", + "to_node": "loss1" + }, + { + "from_node": "loss1", + "to_node": "demand1" + }, + { + "from_node": "input1", + "to_node": "spill1" + } + ], + "metric_sets": [ + { + "name": "nodes", + "metrics": [ + { + "type": "Node", + "name": "loss1", + "attribute": "Inflow" + }, + { + "type": "Node", + "name": "loss1", + "attribute": "Outflow" + }, + { + "type": "Node", + "name": "loss1", + "attribute": "Loss" + }, + { + "type": "Node", + "name": "spill1", + "attribute": "Inflow" + } + ] + } + ], + "outputs": [ + { + "name": "node-outputs", + "type": "CSV", + "format": "long", + "filename": "loss_link2.csv", + "metric_set": [ + "nodes" + ], + "decimal_places": 1 + } + ] + } +} diff --git a/pywr-schema/src/test_models/wtw2-expected.csv b/pywr-schema/src/test_models/wtw2-expected.csv new file mode 100644 index 00000000..ac13113c --- /dev/null +++ b/pywr-schema/src/test_models/wtw2-expected.csv @@ -0,0 +1,13 @@ +time_start,time_end,scenario_index,metric_set,name,attribute,value +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,wtw1,Inflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,wtw1,Outflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,wtw1,Loss,0.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,nodes,spill1,Inflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,wtw1,Inflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,wtw1,Outflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,wtw1,Loss,0.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,nodes,spill1,Inflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,wtw1,Inflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,wtw1,Outflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,wtw1,Loss,0.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,nodes,spill1,Inflow,10.0 diff --git a/pywr-schema/src/test_models/wtw2.json b/pywr-schema/src/test_models/wtw2.json new file mode 100644 index 00000000..bce185c2 --- /dev/null +++ b/pywr-schema/src/test_models/wtw2.json @@ -0,0 +1,116 @@ +{ + "metadata": { + "title": "WTW Test 1", + "description": "Test WTW work", + "minimum_version": "0.1" + }, + "timestepper": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": 1 + }, + "network": { + "nodes": [ + { + "meta": { + "name": "input1" + }, + "type": "Catchment", + "flow": { + "type": "Constant", + "value": 20.0 + } + }, + { + "meta": { + "name": "wtw1" + }, + "type": "WaterTreatmentWorks", + "max_flow": { + "type": "Constant", + "value": 10.0 + }, + "loss_factor": { + "type": "Net", + "factor": { + "type": "Constant", + "value": 0.0 + } + } + }, + { + "meta": { + "name": "demand1" + }, + "type": "Output", + "cost": { + "type": "Constant", + "value": -10 + } + }, + { + "meta": { + "name": "spill1" + }, + "type": "Output", + "cost": { + "type": "Constant", + "value": 10 + } + } + ], + "edges": [ + { + "from_node": "input1", + "to_node": "wtw1" + }, + { + "from_node": "wtw1", + "to_node": "demand1" + }, + { + "from_node": "input1", + "to_node": "spill1" + } + ], + "metric_sets": [ + { + "name": "nodes", + "metrics": [ + { + "type": "Node", + "name": "wtw1", + "attribute": "Inflow" + }, + { + "type": "Node", + "name": "wtw1", + "attribute": "Outflow" + }, + { + "type": "Node", + "name": "wtw1", + "attribute": "Loss" + }, + { + "type": "Node", + "name": "spill1", + "attribute": "Inflow" + } + ] + } + ], + "outputs": [ + { + "name": "node-outputs", + "type": "CSV", + "format": "long", + "filename": "wtw2.csv", + "metric_set": [ + "nodes" + ], + "decimal_places": 1 + } + ] + } +}