From e93352d438137c73e8b8a94df73e03ebb2c461dd Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Fri, 22 May 2026 12:34:40 +0100 Subject: [PATCH 01/12] feat: Initial implementation of builder patterns in pywr-core. This moves some of the logic for constructing a complex core network into core itself. This is done using the builder pattern and should result in general move of code from schema to core. The eventual design goal is to have Network be generated and have only immutable methods. The solver and state can then be created using a unique ID (see generative pattern) allowing them to safely access network items without bounds checking (i.e. all indexes are known to be valid). Removes a lot of mutability in the main structs (e.g. Network). This will allow safe use of unchecked methods during simulation in future. See example with the ParameterTimings struct. # Conflicts: # pywr-core/src/models/multi.rs --- Cargo.toml | 2 +- pywr-cli/src/main.rs | 4 +- pywr-core/benches/random_models.rs | 6 +- pywr-core/src/aggregated_node.rs | 783 ++++--- pywr-core/src/aggregated_storage_node.rs | 134 +- pywr-core/src/edge.rs | 68 +- pywr-core/src/lib.rs | 5 +- pywr-core/src/metric.rs | 367 +++- pywr-core/src/models/mod.rs | 78 +- pywr-core/src/models/multi.rs | 258 ++- pywr-core/src/models/simple.rs | 61 +- pywr-core/src/network.rs | 1795 ++++++++--------- pywr-core/src/node.rs | 934 +++++---- pywr-core/src/parameters/aggregated.rs | 105 +- pywr-core/src/parameters/aggregated_index.rs | 4 +- pywr-core/src/parameters/array.rs | 173 +- pywr-core/src/parameters/asymmetric.rs | 4 +- pywr-core/src/parameters/constant.rs | 55 +- .../parameters/control_curves/apportion.rs | 4 +- .../src/parameters/control_curves/index.rs | 4 +- .../parameters/control_curves/interpolated.rs | 85 +- .../src/parameters/control_curves/mod.rs | 4 +- .../parameters/control_curves/piecewise.rs | 131 +- .../src/parameters/control_curves/simple.rs | 8 +- pywr-core/src/parameters/deficit.rs | 4 +- pywr-core/src/parameters/delay.rs | 154 +- pywr-core/src/parameters/difference.rs | 4 +- pywr-core/src/parameters/discount_factor.rs | 75 +- pywr-core/src/parameters/division.rs | 6 +- pywr-core/src/parameters/errors.rs | 2 +- pywr-core/src/parameters/hydropower.rs | 10 +- pywr-core/src/parameters/indexed_array.rs | 6 +- pywr-core/src/parameters/interpolated.rs | 6 +- pywr-core/src/parameters/max.rs | 4 +- pywr-core/src/parameters/min.rs | 4 +- pywr-core/src/parameters/mod.rs | 1121 +++++++--- pywr-core/src/parameters/multi_threshold.rs | 135 +- pywr-core/src/parameters/muskingum.rs | 4 +- pywr-core/src/parameters/negative.rs | 4 +- pywr-core/src/parameters/negativemax.rs | 4 +- pywr-core/src/parameters/negativemin.rs | 4 +- pywr-core/src/parameters/offset.rs | 4 +- pywr-core/src/parameters/polynomial.rs | 4 +- pywr-core/src/parameters/profiles/mod.rs | 2 +- pywr-core/src/parameters/profiles/monthly.rs | 43 +- pywr-core/src/parameters/py.rs | 72 +- pywr-core/src/parameters/rolling.rs | 189 +- pywr-core/src/parameters/threshold.rs | 4 +- pywr-core/src/parameters/vector.rs | 6 +- pywr-core/src/recorders/memory.rs | 9 +- pywr-core/src/recorders/mod.rs | 321 +-- pywr-core/src/scenario.rs | 119 +- pywr-core/src/solvers/builder.rs | 41 +- pywr-core/src/solvers/col_edge_map.rs | 2 +- pywr-core/src/solvers/highs/mod.rs | 3 +- pywr-core/src/solvers/ipm_ocl/mod.rs | 16 +- pywr-core/src/solvers/ipm_simd/mod.rs | 14 +- pywr-core/src/solvers/mod.rs | 5 +- pywr-core/src/state/mod.rs | 93 +- pywr-core/src/test_utils.rs | 334 +-- pywr-core/src/timestep.rs | 59 +- pywr-core/src/virtual_storage.rs | 570 ++++-- pywr-schema/src/error.rs | 2 +- pywr-schema/src/model.rs | 12 +- pywr-schema/src/nodes/piecewise_storage.rs | 2 +- .../src/nodes/virtual_nodes/aggregated.rs | 2 +- .../nodes/virtual_nodes/virtual_storage.rs | 6 +- .../src/timeseries/align_and_resample.rs | 8 +- pywr-schema/src/timeseries/mod.rs | 4 +- 69 files changed, 5352 insertions(+), 3213 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e75495a..b30c7f4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ rustdoc-args = ["--html-in-header", "katex-header.html"] [profile.dev] # Disabling debug info speeds up builds a bunch, # and we don't rely on it for debugging that much. -debug = 0 +# debug = 0 [profile.release] opt-level = 3 diff --git a/pywr-cli/src/main.rs b/pywr-cli/src/main.rs index 5ec7bb5d..104899a7 100644 --- a/pywr-cli/src/main.rs +++ b/pywr-cli/src/main.rs @@ -15,7 +15,7 @@ use pywr_core::solvers::{HighsSolver, HighsSolverSettings, HighsSolverSettingsBu use pywr_core::solvers::{MicroLpSolver, MicroLpSolverSettings, MicroLpSolverSettingsBuilder}; #[cfg(feature = "ipm-simd")] use pywr_core::solvers::{SimdIpmF64Solver, SimdIpmSolverSettings, SimdIpmSolverSettingsBuilder}; -use pywr_core::test_utils::make_random_model; +use pywr_core::test_utils::make_random_model_builder; use pywr_schema::{ComponentConversionError, ModelSchema, MultiNetworkModelSchema, NetworkSchema}; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; @@ -406,7 +406,7 @@ fn run_multi(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path fn run_random(num_systems: usize, density: usize, num_scenarios: usize, solver: &Solver) { let mut rng = ChaCha8Rng::seed_from_u64(0); - let model = make_random_model(num_systems, density, num_scenarios, &mut rng).unwrap(); + let model = make_random_model_builder(num_systems, density, num_scenarios, &mut rng).unwrap(); match *solver { Solver::Clp => model.run::(&ClpSolverSettings::default()), diff --git a/pywr-core/benches/random_models.rs b/pywr-core/benches/random_models.rs index ad15c0b7..16b4a256 100644 --- a/pywr-core/benches/random_models.rs +++ b/pywr-core/benches/random_models.rs @@ -16,7 +16,7 @@ use pywr_core::solvers::{ClpSolver, ClpSolverSettings, ClpSolverSettingsBuilder} use pywr_core::solvers::{HighsSolver, HighsSolverSettings}; #[cfg(feature = "ipm-simd")] use pywr_core::solvers::{SimdIpmF64Solver, SimdIpmSolverSettings, SimdIpmSolverSettingsBuilder}; -use pywr_core::test_utils::make_random_model; +use pywr_core::test_utils::make_random_model_builder; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; #[cfg(feature = "ipm-ocl")] @@ -44,7 +44,9 @@ fn random_benchmark( // Make a consistent random number generator // ChaCha8 should be consistent across builds and platforms let mut rng = ChaCha8Rng::seed_from_u64(0); - let model = make_random_model(n_sys, density, n_sc, &mut rng).unwrap(); + let model = make_random_model_builder(n_sys, density, n_sc, &mut rng) + .build() + .expect("Failed to build random model!"); let num_timesteps = model.domain().time().timesteps().len(); // This is the number of time-steps diff --git a/pywr-core/src/aggregated_node.rs b/pywr-core/src/aggregated_node.rs index 9377b7fd..6e185918 100644 --- a/pywr-core/src/aggregated_node.rs +++ b/pywr-core/src/aggregated_node.rs @@ -1,73 +1,11 @@ #![warn(clippy::pedantic)] use crate::NodeIndex; -use crate::metric::{ConstantMetricF64Error, MetricF64, MetricF64Error}; -use crate::network::Network; -use crate::node::{FlowConstraints, NodeMeta}; +use crate::metric::{ConstantMetricF64Error, MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; +use crate::network::{AggregatedNodeIndex, Network, ResolutionMaps}; +use crate::node::{FlowConstraints, NodeMeta, UnresolvedNode}; use crate::state::{ConstParameterValues, State}; -use std::fmt::Display; -use std::ops::{Deref, DerefMut}; use thiserror::Error; -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] -pub struct AggregatedNodeIndex(usize); - -impl Deref for AggregatedNodeIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for AggregatedNodeIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Default)] -pub struct AggregatedNodeVec { - nodes: Vec, -} - -impl Deref for AggregatedNodeVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.nodes - } -} - -impl DerefMut for AggregatedNodeVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nodes - } -} - -impl AggregatedNodeVec { - #[must_use] - pub fn get(&self, index: &AggregatedNodeIndex) -> Option<&AggregatedNode> { - self.nodes.get(index.0) - } - - pub fn get_mut(&mut self, index: &AggregatedNodeIndex) -> Option<&mut AggregatedNode> { - self.nodes.get_mut(index.0) - } - - pub fn push_new( - &mut self, - name: &str, - sub_name: Option<&str>, - nodes: &[Vec], - relationship: Option, - ) -> AggregatedNodeIndex { - let node_index = AggregatedNodeIndex(self.nodes.len()); - let node = AggregatedNode::new(node_index, name, sub_name, nodes, relationship); - self.nodes.push(node); - node_index - } -} - /// Factors relating node flows in an aggregated node. #[derive(Debug, PartialEq)] pub enum Factors { @@ -100,6 +38,106 @@ impl Factors { } } +#[derive(Default)] +pub struct ProportionalFactorsBuilder { + factors: Vec, +} + +impl ProportionalFactorsBuilder { + pub fn factor(&mut self, factor: UnresolvedMetricF64) -> &mut Self { + self.factors.push(factor); + self + } +} + +impl RelationshipBuilder for ProportionalFactorsBuilder { + fn build(&self, resolution_maps: &ResolutionMaps) -> Result { + let factors = self + .factors + .iter() + .map(|f| f.resolve(resolution_maps)) + .collect::, _>>() + .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { + attr: "factors".to_string(), + source, + })?; + + Ok(Relationship::Factored(Factors::Proportion { factors })) + } +} + +#[derive(Default)] +pub struct RatioFactorsBuilder { + factors: Vec, +} + +impl RatioFactorsBuilder { + pub fn factor(&mut self, factor: UnresolvedMetricF64) -> &mut Self { + self.factors.push(factor); + self + } +} + +impl RelationshipBuilder for RatioFactorsBuilder { + fn build(&self, resolution_maps: &ResolutionMaps) -> Result { + let factors = self + .factors + .iter() + .map(|f| f.resolve(resolution_maps)) + .collect::, _>>() + .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { + attr: "factors".to_string(), + source, + })?; + + Ok(Relationship::Factored(Factors::Ratio { factors })) + } +} + +#[derive(Default)] +pub struct CoefficientFactorsBuilder { + factors: Vec, + rhs: Option, +} + +impl CoefficientFactorsBuilder { + pub fn factor(&mut self, factor: UnresolvedMetricF64) -> &mut Self { + self.factors.push(factor); + self + } + + pub fn rhs(&mut self, rhs: UnresolvedMetricF64) -> &mut Self { + self.rhs = Some(rhs); + self + } +} + +impl RelationshipBuilder for CoefficientFactorsBuilder { + fn build(&self, resolution_maps: &ResolutionMaps) -> Result { + let factors = self + .factors + .iter() + .map(|f| f.resolve(resolution_maps)) + .collect::, _>>() + .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { + attr: "factors".to_string(), + source, + })?; + + let rhs = self + .rhs + .as_ref() + .map(|r| r.resolve(resolution_maps)) + .transpose() + .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { + attr: "rhs".to_string(), + source, + })?; + + Ok(Relationship::Factored(Factors::Coefficients { factors, rhs })) + } +} + #[derive(Debug, PartialEq)] pub struct Exclusivity { // The minimum number of nodes that must be active @@ -120,6 +158,33 @@ impl Exclusivity { } } +#[derive(Default, Clone)] +pub struct ExclusivityBuilder { + min_active: u64, + max_active: u64, +} + +impl ExclusivityBuilder { + pub fn min_active(&mut self, min_active: u64) -> &mut Self { + self.min_active = min_active; + self + } + + pub fn max_active(&mut self, max_active: u64) -> &mut Self { + self.max_active = max_active; + self + } +} + +impl RelationshipBuilder for ExclusivityBuilder { + fn build(&self, _resolution_maps: &ResolutionMaps) -> Result { + Ok(Relationship::Exclusive(Exclusivity { + min_active: self.min_active, + max_active: self.max_active, + })) + } +} + /// Additional relationship between nodes in an aggregated node. #[derive(Debug, PartialEq)] pub enum Relationship { @@ -158,6 +223,25 @@ impl Relationship { } } +#[derive(Debug, Error)] +pub enum RelationshipBuildError { + #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] + ResolveMetricF64Error { + attr: String, + #[source] + source: MetricF64ResolutionError, + }, +} +pub trait RelationshipBuilder { + /// Try to construct a [`Relationship`]. + /// + /// # Errors + /// + /// A [`RelationshipBuildError`] should be returned if the builder is unable to resolve any of + /// the metrics it references. + fn build(&self, resolution_maps: &ResolutionMaps) -> Result; +} + #[derive(Debug, Error)] pub enum AggregatedNodeError { #[error("Flow constraints are undefined for this node type")] @@ -276,22 +360,6 @@ impl<'a> NodeConstFactorPair<'a> { } impl AggregatedNode { - #[must_use] - pub fn new( - index: AggregatedNodeIndex, - name: &str, - sub_name: Option<&str>, - nodes: &[Vec], - relationship: Option, - ) -> Self { - Self { - meta: NodeMeta::new(index, name, sub_name), - flow_constraints: FlowConstraints::default(), - nodes: nodes.to_vec(), - relationship, - } - } - #[must_use] pub fn name(&self) -> &str { self.meta.name() @@ -444,10 +512,6 @@ impl AggregatedNode { } } - pub fn set_min_flow(&mut self, value: Option) { - self.flow_constraints.min_flow = value; - } - /// Get the min flow constraint value. /// /// # Errors @@ -457,9 +521,6 @@ impl AggregatedNode { pub fn get_min_flow(&self, model: &Network, state: &State) -> Result { self.flow_constraints.get_min_flow(model, state) } - pub fn set_max_flow(&mut self, value: Option) { - self.flow_constraints.max_flow = value; - } /// Get the max flow constraint value. /// @@ -490,6 +551,155 @@ impl AggregatedNode { } } +#[derive(Debug, Error)] +pub enum AggregatedNodeBuilderError { + #[error("Index not found in resolution map.")] + IndexNotFound, + #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] + ResolveMetricF64Error { + attr: String, + #[source] + source: MetricF64ResolutionError, + }, + #[error("Reference to node not found.")] + NodeIndexNotFound { node: UnresolvedNode }, + #[error("Error building relationship: {0}")] + RelationshipBuildError(#[from] RelationshipBuildError), +} + +pub struct AggregatedNodeBuilder { + name: UnresolvedNode, + min_flow: Option, + max_flow: Option, + nodes: Vec>, + relationship: Option>, +} + +impl AggregatedNodeBuilder { + #[must_use] + pub fn new(name: &str) -> Self { + let name = UnresolvedNode::new(name, None); + + Self { + name, + min_flow: None, + max_flow: None, + nodes: Vec::new(), + relationship: None, + } + } + + #[must_use] + pub fn name(&self) -> &UnresolvedNode { + &self.name + } + + pub fn sub_name(&mut self, sub_name: &str) -> &mut Self { + self.name.set_sub_name(Some(sub_name)); + self + } + + pub fn min_flow(&mut self, min_flow: UnresolvedMetricF64) -> &mut Self { + self.min_flow = Some(min_flow); + self + } + pub fn max_flow(&mut self, max_flow: UnresolvedMetricF64) -> &mut Self { + self.max_flow = Some(max_flow); + self + } + pub fn nodes(&mut self, nodes: Vec) -> &mut Self { + self.nodes.push(nodes); + self + } + + pub fn relationship(&mut self, relationship: Box) -> &mut Self { + self.relationship = Some(relationship); + self + } + + /// Build a [`FlowConstraints`] from the builder. + fn build_flow_constraints( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + let min_flow = self + .min_flow + .as_ref() + .map(|min_flow| { + min_flow + .resolve(resolution_maps) + .map_err(|source| AggregatedNodeBuilderError::ResolveMetricF64Error { + attr: "min_flow".to_string(), + source, + }) + }) + .transpose()?; + + let max_flow = self + .max_flow + .as_ref() + .map(|max_flow| { + max_flow + .resolve(resolution_maps) + .map_err(|source| AggregatedNodeBuilderError::ResolveMetricF64Error { + attr: "max_flow".to_string(), + source, + }) + }) + .transpose()?; + + let flow_constraints = FlowConstraints::new(min_flow, max_flow); + + Ok(flow_constraints) + } + + /// Try to construct an [`AggregatedNode`] from this builder. + /// + /// # Errors + /// + /// An [`AggregatedNodeBuilderError`] will be returned if the builder is unable to resolve + /// any of the metrics or node names it references. + pub fn build(&self, resolution_maps: &ResolutionMaps) -> Result { + let index = resolution_maps + .aggregated_nodes + .get(&self.name) + .ok_or(AggregatedNodeBuilderError::IndexNotFound)?; + let meta = NodeMeta::from_unresolved_name(self.name.clone(), *index); + + let flow_constraints = self.build_flow_constraints(resolution_maps)?; + + let nodes = self + .nodes + .iter() + .map(|indices| { + indices + .iter() + .map(|unresolved| { + resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { + AggregatedNodeBuilderError::NodeIndexNotFound { + node: unresolved.clone(), + } + }) + }) + .collect::, _>>() + }) + .collect::, _>>()?; + + let relationship = self + .relationship + .as_ref() + .map(|r| r.build(resolution_maps)) + .transpose()?; + + Ok(AggregatedNode { + meta, + flow_constraints, + nodes, + relationship, + }) + } +} + #[derive(Debug, Error)] pub enum FactorError { #[error("Error calculating proportional factors: {0}")] @@ -891,14 +1101,15 @@ fn get_const_coefficient_factor_pairs<'a>( #[cfg(test)] mod tests { - use crate::aggregated_node::Relationship; - use crate::metric::MetricF64; - use crate::models::Model; - use crate::network::Network; - use crate::parameters::MonthlyProfileParameter; - use crate::recorders::AssertionF64Recorder; + use crate::aggregated_node::{AggregatedNodeBuilder, ExclusivityBuilder, RatioFactorsBuilder}; + use crate::metric::UnresolvedMetricF64; + use crate::models::ModelBuilder; + use crate::network::NetworkBuilder; + use crate::node::NodeBuilder; + use crate::parameters::{MonthlyProfileParameterBuilder, ParameterName}; + use crate::recorders::AssertionF64RecorderBuilder; use crate::state::ParameterReturnValue; - use crate::test_utils::{default_time_domain, run_all_solvers}; + use crate::test_utils::{default_domain_builder, run_all_solvers}; use ndarray::Array2; /// Test the factors forcing a simple ratio of flow @@ -906,45 +1117,66 @@ mod tests { /// The model has a single input that diverges to two links and respective output nodes. #[test] fn test_simple_factors() { - let mut network = Network::default(); + let mut builder = NetworkBuilder::default(); - let input_node = network.add_input_node("input", None).unwrap(); - let link_node0 = network.add_link_node("link", Some("0")).unwrap(); - let output_node0 = network.add_output_node("output", Some("0")).unwrap(); + let input_node = NodeBuilder::input("input"); + builder.node(input_node); - network.connect_nodes(input_node, link_node0).unwrap(); - network.connect_nodes(link_node0, output_node0).unwrap(); + let mut link_node0 = NodeBuilder::link("link"); + link_node0.sub_name("0"); + let link_node0_ref = link_node0.name().clone(); + builder.node(link_node0); - let link_node1 = network.add_link_node("link", Some("1")).unwrap(); - let output_node1 = network.add_output_node("output", Some("1")).unwrap(); + // Setup a demand on output-0 + let mut output_node0 = NodeBuilder::output("output"); + output_node0.sub_name("0").max_flow(100.0.into()).cost((-10.0).into()); + builder.node(output_node0); - network.connect_nodes(input_node, link_node1).unwrap(); - network.connect_nodes(link_node1, output_node1).unwrap(); + builder.connect("input", None, "link", Some("0")); + builder.connect("link", Some("0"), "output", Some("0")); - let relationship = Some(Relationship::new_ratio_factors(&[2.0.into(), 1.0.into()])); + let mut link_node1 = NodeBuilder::link("link"); + link_node1.sub_name("1"); + let link_node1_ref = link_node1.name().clone(); + builder.node(link_node1); - let _agg_node = - network.add_aggregated_node("agg-node", None, &[vec![link_node0], vec![link_node1]], relationship); + let mut output_node1 = NodeBuilder::output("output"); + output_node1.sub_name("1"); + builder.node(output_node1); - // Setup a demand on output-0 - let output_node = network.get_mut_node_by_name("output", Some("0")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); + builder.connect("input", None, "link", Some("1")); + builder.connect("link", Some("1"), "output", Some("1")); - output_node.set_cost(Some((-10.0).into())); + let mut relationship = RatioFactorsBuilder::default(); + relationship.factor(2.0.into()).factor(1.0.into()); - // Set-up assertion for "input" node - let idx = network.get_node_by_name("link", Some("0")).unwrap().index(); + let mut agg_node = AggregatedNodeBuilder::new("agg-node"); + agg_node + .nodes(vec![link_node0_ref.clone()]) + .nodes(vec![link_node1_ref.clone()]) + .relationship(Box::new(relationship)); + builder.agg_node(agg_node); + + // Set-up assertion for "link-0" node let expected = Array2::from_elem((366, 10), 100.0); - let recorder = AssertionF64Recorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-0-flow", + UnresolvedMetricF64::NodeOutFlow(link_node0_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - // Set-up assertion for "input" node - let idx = network.get_node_by_name("link", Some("1")).unwrap().index(); + // Set-up assertion for "link-1" node let expected = Array2::from_elem((366, 10), 50.0); - let recorder = AssertionF64Recorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-0-flow", + UnresolvedMetricF64::NodeOutFlow(link_node1_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - let model = Model::new(default_time_domain().into(), network); + let domain = default_domain_builder(); + let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["ipm-simd", "ipm-ocl"], &[], &[]); } @@ -954,51 +1186,75 @@ mod tests { /// The model has a single input that diverges to two links and respective output nodes. #[test] fn test_simple_factor_profile() { - let mut network = Network::default(); - - let input_node = network.add_input_node("input", None).unwrap(); - let link_node0 = network.add_link_node("link", Some("0")).unwrap(); - let output_node0 = network.add_output_node("output", Some("0")).unwrap(); - - network.connect_nodes(input_node, link_node0).unwrap(); - network.connect_nodes(link_node0, output_node0).unwrap(); - - let link_node1 = network.add_link_node("link", Some("1")).unwrap(); - let output_node1 = network.add_output_node("output", Some("1")).unwrap(); - - network.connect_nodes(input_node, link_node1).unwrap(); - network.connect_nodes(link_node1, output_node1).unwrap(); - - let factor_profile = MonthlyProfileParameter::new("factor-profile".into(), [2.0; 12], None); - let factor_profile_idx = network.add_simple_parameter(Box::new(factor_profile)).unwrap(); + let mut builder = NetworkBuilder::default(); - let relationship = Some(Relationship::new_ratio_factors(&[ - factor_profile_idx.into_metric_f64(ParameterReturnValue::Before), - 1.0.into(), - ])); + let input_node = NodeBuilder::input("input"); + builder.node(input_node); - let _agg_node = - network.add_aggregated_node("agg-node", None, &[vec![link_node0], vec![link_node1]], relationship); + let mut link_node0 = NodeBuilder::link("link"); + link_node0.sub_name("0"); + let link_node0_ref = link_node0.name().clone(); + builder.node(link_node0); // Setup a demand on output-0 - let output_node = network.get_mut_node_by_name("output", Some("0")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); + let mut output_node0 = NodeBuilder::output("output"); + output_node0.sub_name("0").max_flow(100.0.into()).cost((-10.0).into()); + builder.node(output_node0); + + builder.connect("input", None, "link", Some("0")); + builder.connect("link", Some("0"), "output", Some("0")); + + let mut link_node1 = NodeBuilder::link("link"); + link_node1.sub_name("1"); + let link_node1_ref = link_node1.name().clone(); + builder.node(link_node1); + + let mut output_node1 = NodeBuilder::output("output"); + output_node1.sub_name("1"); + builder.node(output_node1); + + builder.connect("input", None, "link", Some("1")); + builder.connect("link", Some("1"), "output", Some("1")); + + let factor_profile_name = ParameterName::new("factor-profile", None); + let factor_profile = MonthlyProfileParameterBuilder::new(factor_profile_name.clone(), [2.0; 12]); + builder.parameters().f64(Box::new(factor_profile)); + + let mut relationship = RatioFactorsBuilder::default(); + relationship + .factor(UnresolvedMetricF64::ParameterValue { + name: factor_profile_name, + return_value: ParameterReturnValue::Before, + }) + .factor(1.0.into()); - output_node.set_cost(Some((-10.0).into())); + let mut agg_node = AggregatedNodeBuilder::new("agg-node"); + agg_node + .nodes(vec![link_node0_ref.clone()]) + .nodes(vec![link_node1_ref.clone()]) + .relationship(Box::new(relationship)); + builder.agg_node(agg_node); // Set-up assertion for "input" node - let idx = network.get_node_by_name("link", Some("0")).unwrap().index(); let expected = Array2::from_elem((366, 10), 100.0); - let recorder = AssertionF64Recorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-0-flow", + UnresolvedMetricF64::NodeOutFlow(link_node0_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); // Set-up assertion for "input" node - let idx = network.get_node_by_name("link", Some("1")).unwrap().index(); let expected = Array2::from_elem((366, 10), 50.0); - let recorder = AssertionF64Recorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-1-flow", + UnresolvedMetricF64::NodeOutFlow(link_node1_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - let model = Model::new(default_time_domain().into(), network); + let domain = default_domain_builder(); + let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["cbc", "ipm-simd", "ipm-ocl"], &[], &[]); } @@ -1008,53 +1264,69 @@ mod tests { /// The model has a single input that diverges to two links, only one of which can be active at a time. #[test] fn test_simple_mutual_exclusivity() { - let mut network = Network::default(); - - let input_node = network.add_input_node("input", None).unwrap(); - let link_node0 = network.add_link_node("link", Some("0")).unwrap(); - let output_node0 = network.add_output_node("output", Some("0")).unwrap(); + let mut builder = NetworkBuilder::default(); + // Setup a demand on output-0 and output-1. + // output-0 has a lower penalty cost than output-1, so the flow should be directed to output-0. + let input_node = NodeBuilder::input("input"); + builder.node(input_node); - network.connect_nodes(input_node, link_node0).unwrap(); - network.connect_nodes(link_node0, output_node0).unwrap(); + let mut link_node0 = NodeBuilder::link("link"); + link_node0.sub_name("0"); + let link_node0_ref = link_node0.name().clone(); + builder.node(link_node0); - let link_node1 = network.add_link_node("link", Some("1")).unwrap(); - let output_node1 = network.add_output_node("output", Some("1")).unwrap(); + // Setup a demand on output-0 + let mut output_node0 = NodeBuilder::output("output"); + output_node0.sub_name("0").max_flow(100.0.into()).cost((-10.0).into()); + builder.node(output_node0); - network.connect_nodes(input_node, link_node1).unwrap(); - network.connect_nodes(link_node1, output_node1).unwrap(); + builder.connect("input", None, "link", Some("0")); + builder.connect("link", Some("0"), "output", Some("0")); - let _me_node = network.add_aggregated_node( - "mutual-exclusivity", - None, - &[vec![link_node0], vec![link_node1]], - Some(Relationship::new_exclusive(0, 1)), - ); + let mut link_node1 = NodeBuilder::link("link"); + link_node1.sub_name("1"); + let link_node1_ref = link_node1.name().clone(); + builder.node(link_node1); - // Setup a demand on output-0 and output-1. - // output-0 has a lower penalty cost than output-1, so the flow should be directed to output-0. - let output_node = network.get_mut_node_by_name("output", Some("0")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); + let mut output_node1 = NodeBuilder::output("output"); + output_node1.sub_name("1").max_flow(100.0.into()).cost((-5.0).into()); + builder.node(output_node1); - output_node.set_cost(Some((-10.0).into())); + builder.connect("input", None, "link", Some("1")); + builder.connect("link", Some("1"), "output", Some("1")); - let output_node = network.get_mut_node_by_name("output", Some("1")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); + let mut relationship = ExclusivityBuilder::default(); + relationship.min_active(0).max_active(1); - output_node.set_cost(Some((-5.0).into())); + let mut agg_node = AggregatedNodeBuilder::new("mutual-exclusivity"); + agg_node + .nodes(vec![link_node0_ref.clone()]) + .nodes(vec![link_node1_ref.clone()]) + .relationship(Box::new(relationship)); + builder.agg_node(agg_node); // Set-up assertion for "output-0" node - let idx = network.get_node_by_name("link", Some("0")).unwrap().index(); + let expected = Array2::from_elem((366, 10), 100.0); - let recorder = AssertionF64Recorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-0-flow", + UnresolvedMetricF64::NodeOutFlow(link_node0_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); // Set-up assertion for "output-1" node - let idx = network.get_node_by_name("link", Some("1")).unwrap().index(); + let expected = Array2::from_elem((366, 10), 0.0); - let recorder = AssertionF64Recorder::new("link-1-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-1-flow", + UnresolvedMetricF64::NodeOutFlow(link_node1_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - let model = Model::new(default_time_domain().into(), network); + let domain = default_domain_builder(); + let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["clp", "ipm-simd", "ipm-ocl"], &[], &[]); } @@ -1066,76 +1338,95 @@ mod tests { /// tests that a node can appear in two different mutual exclusivity constraints. #[test] fn test_double_mutual_exclusivity() { - let mut network = Network::default(); - - let input_node = network.add_input_node("input", None).unwrap(); - let link_node0 = network.add_link_node("link", Some("0")).unwrap(); - let output_node0 = network.add_output_node("output", Some("0")).unwrap(); - - network.connect_nodes(input_node, link_node0).unwrap(); - network.connect_nodes(link_node0, output_node0).unwrap(); - - let link_node1 = network.add_link_node("link", Some("1")).unwrap(); - let output_node1 = network.add_output_node("output", Some("1")).unwrap(); - - network.connect_nodes(input_node, link_node1).unwrap(); - network.connect_nodes(link_node1, output_node1).unwrap(); - - let link_node2 = network.add_link_node("link", Some("2")).unwrap(); - let output_node2 = network.add_output_node("output", Some("2")).unwrap(); - - network.connect_nodes(input_node, link_node2).unwrap(); - network.connect_nodes(link_node2, output_node2).unwrap(); - - let _me_node = network.add_aggregated_node( - "mutual-exclusivity-01", - None, - &[vec![link_node0], vec![link_node1]], - Some(Relationship::new_exclusive(0, 1)), - ); - let _me_node = network.add_aggregated_node( - "mutual-exclusivity-12", - None, - &[vec![link_node1], vec![link_node2]], - Some(Relationship::new_exclusive(0, 1)), - ); - + let mut builder = NetworkBuilder::default(); // Setup a demand on the outputs // output-1 has a lower penalty cost than output-0 and output-2, so the flow should be directed to output-1. - let output_node = network.get_mut_node_by_name("output", Some("0")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); - - output_node.set_cost(Some((-5.0).into())); - - let output_node = network.get_mut_node_by_name("output", Some("1")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); + let input_node = NodeBuilder::input("input"); + builder.node(input_node); - output_node.set_cost(Some((-15.0).into())); + let mut link_node0 = NodeBuilder::link("link"); + link_node0.sub_name("0"); + let link_node0_ref = link_node0.name().clone(); + builder.node(link_node0); - let output_node = network.get_mut_node_by_name("output", Some("2")).unwrap(); - output_node.set_max_flow_constraint(Some(100.0.into())).unwrap(); - - output_node.set_cost(Some((-5.0).into())); - - // Set-up assertion for "output-0" node - let idx = network.get_node_by_name("link", Some("0")).unwrap().index(); + // Setup a demand on output-0 + let mut output_node0 = NodeBuilder::output("output"); + output_node0.sub_name("0").max_flow(100.0.into()).cost((-5.0).into()); + builder.node(output_node0); + + builder.connect("input", None, "link", Some("0")); + builder.connect("link", Some("0"), "output", Some("0")); + + let mut link_node1 = NodeBuilder::link("link"); + link_node1.sub_name("1"); + let link_node1_ref = link_node1.name().clone(); + builder.node(link_node1); + + let mut output_node1 = NodeBuilder::output("output"); + output_node1.sub_name("1").max_flow(100.0.into()).cost((-15.0).into()); + builder.node(output_node1); + + builder.connect("input", None, "link", Some("1")); + builder.connect("link", Some("1"), "output", Some("1")); + + let mut link_node2 = NodeBuilder::link("link"); + link_node2.sub_name("2"); + let link_node2_ref = link_node2.name().clone(); + builder.node(link_node2); + + let mut output_node2 = NodeBuilder::output("output"); + output_node2.sub_name("2").max_flow(100.0.into()).cost((-5.0).into()); + builder.node(output_node2); + + builder.connect("input", None, "link", Some("2")); + builder.connect("link", Some("2"), "output", Some("2")); + + let mut relationship = ExclusivityBuilder::default(); + relationship.min_active(0).max_active(1); + + let mut agg_node = AggregatedNodeBuilder::new("mutual-exclusivity-01"); + agg_node + .nodes(vec![link_node0_ref.clone()]) + .nodes(vec![link_node1_ref.clone()]) + .relationship(Box::new(relationship.clone())); + builder.agg_node(agg_node); + + let mut agg_node = AggregatedNodeBuilder::new("mutual-exclusivity-12"); + agg_node + .nodes(vec![link_node1_ref.clone()]) + .nodes(vec![link_node2_ref.clone()]) + .relationship(Box::new(relationship)); + builder.agg_node(agg_node); + + // Set-up assertion for "link-0" node let expected = Array2::from_elem((366, 10), 0.0); - let recorder = AssertionF64Recorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-0-flow", + UnresolvedMetricF64::NodeOutFlow(link_node0_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - // Set-up assertion for "output-0" node - let idx = network.get_node_by_name("link", Some("1")).unwrap().index(); + // Set-up assertion for "link-1" node let expected = Array2::from_elem((366, 10), 100.0); - let recorder = AssertionF64Recorder::new("link-1-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-1-flow", + UnresolvedMetricF64::NodeOutFlow(link_node1_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - // Set-up assertion for "output-2" node - let idx = network.get_node_by_name("link", Some("2")).unwrap().index(); + // Set-up assertion for "link-2" node let expected = Array2::from_elem((366, 10), 0.0); - let recorder = AssertionF64Recorder::new("link-2-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-2-flow", + UnresolvedMetricF64::NodeOutFlow(link_node2_ref.clone()), + expected, + ); + builder.recorder(Box::new(recorder)); - let model = Model::new(default_time_domain().into(), network); + let domain = default_domain_builder(); + let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["clp", "ipm-ocl", "ipm-simd"], &[], &[]); } diff --git a/pywr-core/src/aggregated_storage_node.rs b/pywr-core/src/aggregated_storage_node.rs index 6e038b46..3fed57d2 100644 --- a/pywr-core/src/aggregated_storage_node.rs +++ b/pywr-core/src/aggregated_storage_node.rs @@ -1,67 +1,9 @@ #![warn(clippy::pedantic)] use crate::NodeIndex; use crate::metric::MetricF64; -use crate::node::NodeMeta; -use std::fmt::Display; -use std::ops::{Deref, DerefMut}; - -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] -pub struct AggregatedStorageNodeIndex(usize); - -impl Deref for AggregatedStorageNodeIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for AggregatedStorageNodeIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Default)] -pub struct AggregatedStorageNodeVec { - nodes: Vec, -} - -impl Deref for AggregatedStorageNodeVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.nodes - } -} - -impl DerefMut for AggregatedStorageNodeVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nodes - } -} - -impl AggregatedStorageNodeVec { - pub fn get(&self, index: AggregatedStorageNodeIndex) -> Option<&AggregatedStorageNode> { - self.nodes.get(index.0) - } - - pub fn get_mut(&mut self, index: AggregatedStorageNodeIndex) -> Option<&mut AggregatedStorageNode> { - self.nodes.get_mut(index.0) - } - - pub fn push_new( - &mut self, - name: &str, - sub_name: Option<&str>, - nodes: Vec, - ) -> AggregatedStorageNodeIndex { - let node_index = AggregatedStorageNodeIndex(self.nodes.len()); - let node = AggregatedStorageNode::new(node_index, name, sub_name, nodes); - self.nodes.push(node); - node_index - } -} +use crate::network::{AggregatedStorageNodeIndex, ResolutionMaps}; +use crate::node::{NodeMeta, UnresolvedNode}; +use thiserror::Error; #[derive(Debug, PartialEq, Eq)] pub struct AggregatedStorageNode { @@ -70,13 +12,6 @@ pub struct AggregatedStorageNode { } impl AggregatedStorageNode { - pub fn new(index: AggregatedStorageNodeIndex, name: &str, sub_name: Option<&str>, nodes: Vec) -> Self { - Self { - meta: NodeMeta::new(index, name, sub_name), - nodes, - } - } - pub fn name(&self) -> &str { self.meta.name() } @@ -103,3 +38,66 @@ impl AggregatedStorageNode { self.nodes.iter().map(|n| MetricF64::NodeOutFlow(*n)).collect() } } + +#[derive(Debug, Error)] +pub enum AggregatedStorageNodeBuilderError { + #[error("Index not found in resolution map.")] + IndexNotFound, + #[error("Reference to node not found.")] + NodeIndexNotFound { node: UnresolvedNode }, +} + +pub struct AggregatedStorageNodeBuilder { + name: UnresolvedNode, + nodes: Vec, +} + +impl AggregatedStorageNodeBuilder { + pub fn new(name: &str) -> Self { + let name = UnresolvedNode::new(name, None); + + Self { + name, + nodes: Vec::new(), + } + } + + pub fn name(&self) -> &UnresolvedNode { + &self.name + } + + pub fn sub_name(&mut self, sub_name: &str) -> &mut Self { + self.name.set_sub_name(Some(sub_name)); + self + } + + pub fn node(&mut self, node: UnresolvedNode) -> &mut Self { + self.nodes.push(node); + self + } + + pub fn build( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + let index = resolution_maps + .aggregated_storage_nodes + .get(&self.name) + .ok_or(AggregatedStorageNodeBuilderError::IndexNotFound)?; + let meta = NodeMeta::from_unresolved_name(self.name.clone(), *index); + + let nodes = self + .nodes + .iter() + .map(|unresolved| { + resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { + AggregatedStorageNodeBuilderError::NodeIndexNotFound { + node: unresolved.clone(), + } + }) + }) + .collect::, _>>()?; + + Ok(AggregatedStorageNode { meta, nodes }) + } +} diff --git a/pywr-core/src/edge.rs b/pywr-core/src/edge.rs index bd56e6bd..35536fe2 100644 --- a/pywr-core/src/edge.rs +++ b/pywr-core/src/edge.rs @@ -1,28 +1,10 @@ use crate::NodeIndex; -use crate::network::Network; -use crate::node::{NodeError, NodeVec}; +use crate::network::{EdgeIndex, Network}; +use crate::node::{Node, NodeError}; use crate::state::State; -use std::fmt::{Display, Formatter}; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; use thiserror::Error; -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash)] -pub struct EdgeIndex(usize); - -impl Deref for EdgeIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for EdgeIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - #[derive(Debug, Error)] pub enum EdgeError { #[error("From node index not found: {0}")] @@ -61,12 +43,12 @@ impl Edge { self.to_node_index } - pub fn cost(&self, nodes: &NodeVec, model: &Network, state: &State) -> Result { + pub fn cost(&self, nodes: &[Node], model: &Network, state: &State) -> Result { let from_node = nodes - .get(&self.from_node_index) + .get(*self.from_node_index.deref()) .ok_or(EdgeError::FromNodeIndexNotFound(self.from_node_index))?; let to_node = nodes - .get(&self.to_node_index) + .get(*self.to_node_index.deref()) .ok_or(EdgeError::ToNodeIndexNotFound(self.from_node_index))?; let from_cost = from_node @@ -79,41 +61,3 @@ impl Edge { Ok(from_cost + to_cost) } } - -#[derive(Default)] -pub struct EdgeVec { - edges: Vec, -} - -impl Deref for EdgeVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.edges - } -} - -impl DerefMut for EdgeVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.edges - } -} - -impl EdgeVec { - pub fn get(&self, index: &EdgeIndex) -> Option<&Edge> { - self.edges.get(index.0) - } - - pub fn get_mut(&mut self, index: &EdgeIndex) -> Option<&mut Edge> { - self.edges.get_mut(index.0) - } - - pub fn push(&mut self, from_node_index: NodeIndex, to_node_index: NodeIndex) -> EdgeIndex { - let index = EdgeIndex(self.edges.len()); - // TODO check whether an edge between these two nodes already exists. - - let node = Edge::new(index, from_node_index, to_node_index); - self.edges.push(node); - index - } -} diff --git a/pywr-core/src/lib.rs b/pywr-core/src/lib.rs index 4e2dce7d..f167756e 100644 --- a/pywr-core/src/lib.rs +++ b/pywr-core/src/lib.rs @@ -1,7 +1,4 @@ -use crate::node::NodeIndex; -use crate::recorders::RecorderIndex; -pub use network::NetworkError; - +pub use network::{NetworkError, NodeIndex}; pub mod agg_funcs; pub mod aggregated_node; mod aggregated_storage_node; diff --git a/pywr-core/src/metric.rs b/pywr-core/src/metric.rs index d6c4fdc0..7d1d0b58 100644 --- a/pywr-core/src/metric.rs +++ b/pywr-core/src/metric.rs @@ -1,14 +1,16 @@ -use crate::aggregated_node::AggregatedNodeIndex; -use crate::aggregated_storage_node::AggregatedStorageNodeIndex; -use crate::edge::EdgeIndex; +use crate::NodeIndex; use crate::models::MultiNetworkTransferIndex; -use crate::network::Network; -use crate::node::{NodeError, NodeIndex}; -use crate::parameters::{ConstParameterIndex, GeneralParameterIndex, ParameterIndex, SimpleParameterIndex}; +use crate::network::{ + AggregatedNodeIndex, AggregatedStorageNodeIndex, EdgeIndex, Network, ResolutionMaps, UnresolvedEdge, + VirtualStorageIndex, +}; +use crate::node::{NodeError, UnresolvedNode}; +use crate::parameters::{ + ConstParameterIndex, GeneralParameterIndex, ParameterIndex, ParameterName, SimpleParameterIndex, +}; use crate::state::{ ConstParameterValues, MultiValue, NetworkStateError, ParameterReturnValue, SimpleParameterValues, State, StateError, }; -use crate::virtual_storage::VirtualStorageIndex; use num::Zero; use thiserror::Error; @@ -176,7 +178,7 @@ pub enum MetricF64 { NodeMaxVolume(NodeIndex), AggregatedNodeInFlow(AggregatedNodeIndex), AggregatedNodeOutFlow(AggregatedNodeIndex), - AggregatedNodeVolume(AggregatedStorageNodeIndex), + AggregatedStorageNodeVolume(AggregatedStorageNodeIndex), AggregatedNodeProportionalVolume(AggregatedStorageNodeIndex), EdgeFlow(EdgeIndex), MultiEdgeFlow { @@ -270,15 +272,17 @@ impl MetricF64 { .sum::>()?; Ok(flow) } - MetricF64::ParameterValue { index, return_value } => Ok(state.get_parameter_value(*index, *return_value)?), + MetricF64::ParameterValue { index, return_value } => { + Ok(state.get_general_parameter_value(*index, *return_value)?) + } MetricF64::IndexParameterValue { index, return_value } => { - Ok(state.get_parameter_index(*index, *return_value)? as f64) + Ok(state.get_general_parameter_index(*index, *return_value)? as f64) } MetricF64::MultiParameterValue { index, key, return_value, - } => Ok(state.get_multi_parameter_value(*index, key, *return_value)?), + } => Ok(state.get_general_multi_parameter_value(*index, key, *return_value)?), MetricF64::VirtualStorageVolume(idx) => Ok(state.get_network_state().get_virtual_storage_volume(idx)?), MetricF64::VirtualStorageProportionalVolume(idx) => { Ok(state.get_network_state().get_virtual_storage_proportional_volume(idx)?) @@ -287,7 +291,7 @@ impl MetricF64 { .get_virtual_storage_node(idx) .ok_or(MetricF64Error::VirtualStorageIndexNotFound(*idx))? .get_max_volume(state)?), - MetricF64::AggregatedNodeVolume(idx) => { + MetricF64::AggregatedStorageNodeVolume(idx) => { let node = network .get_aggregated_storage_node(*idx) .ok_or(MetricF64Error::AggregatedStorageNodeIndexNotFound(*idx))?; @@ -437,6 +441,270 @@ impl TryFrom> for SimpleMetricU64 { } } +#[derive(Debug, Error)] +pub enum MetricF64ResolutionError { + #[error("Node not found when resolving F64 metric: {node}")] + NodeNotFound { node: UnresolvedNode }, + #[error("Parameter not found when resolving F64 metric: {parameter}")] + ParameterNotFound { parameter: ParameterName }, + #[error("Aggregated node not found when resolving F64 metric: {aggregated_node}")] + AggregatedNodeNotFound { aggregated_node: UnresolvedNode }, + #[error("Aggregated storage node not found when resolving F64 metric: {aggregated_node}")] + AggregatedStorageNodeNotFound { aggregated_node: UnresolvedNode }, + #[error("Edge not found when resolving F64 metric: {edge}")] + EdgeNotFound { edge: UnresolvedEdge }, + #[error("Virtual storage node not found when resolving F64 metric: {node}")] + VirtualStorageNodeNotFound { node: UnresolvedNode }, + #[error("Inter-network transfer not found when resolving F64 metric: {transfer}")] + InterNetworkTransferNotFound { transfer: String }, +} + +pub enum UnresolvedMetricF64 { + NodeInFlow(UnresolvedNode), + NodeOutFlow(UnresolvedNode), + NodeVolume(UnresolvedNode), + NodeMaxVolume(UnresolvedNode), + AggregatedNodeInFlow(UnresolvedNode), + AggregatedNodeOutFlow(UnresolvedNode), + AggregatedStorageNodeVolume(UnresolvedNode), + EdgeFlow(UnresolvedEdge), + MultiEdgeFlow { + indices: Vec, + name: String, + }, + ParameterValue { + name: ParameterName, + return_value: ParameterReturnValue, + }, + IndexParameterValue { + name: ParameterName, + return_value: ParameterReturnValue, + }, + MultiParameterValue { + name: ParameterName, + key: String, + return_value: ParameterReturnValue, + }, + VirtualStorageVolume(UnresolvedNode), + VirtualStorageProportionalVolume(UnresolvedNode), + MultiNodeInFlow { + indices: Vec, + name: String, + }, + MultiNodeOutFlow { + indices: Vec, + name: String, + }, + NodeProportionalVolume(UnresolvedNode), + InterNetworkTransfer(String), + Constant(f64), +} + +impl UnresolvedMetricF64 { + pub fn new_parameter_before>(name: N) -> Self { + Self::ParameterValue { + name: name.into(), + return_value: ParameterReturnValue::Before, + } + } + pub fn resolve(&self, resolution_maps: &ResolutionMaps) -> Result { + let m = + match self { + UnresolvedMetricF64::NodeInFlow(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + MetricF64::NodeInFlow(*idx) + } + UnresolvedMetricF64::NodeOutFlow(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + MetricF64::NodeOutFlow(*idx) + } + UnresolvedMetricF64::NodeVolume(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + MetricF64::NodeVolume(*idx) + } + UnresolvedMetricF64::NodeMaxVolume(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + MetricF64::NodeMaxVolume(*idx) + } + UnresolvedMetricF64::AggregatedNodeInFlow(unresolved) => { + let idx = resolution_maps.aggregated_nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::AggregatedNodeNotFound { + aggregated_node: unresolved.clone(), + } + })?; + MetricF64::AggregatedNodeInFlow(*idx) + } + UnresolvedMetricF64::AggregatedNodeOutFlow(unresolved) => { + let idx = resolution_maps.aggregated_nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::AggregatedNodeNotFound { + aggregated_node: unresolved.clone(), + } + })?; + MetricF64::AggregatedNodeOutFlow(*idx) + } + UnresolvedMetricF64::AggregatedStorageNodeVolume(unresolved) => { + let idx = resolution_maps + .aggregated_storage_nodes + .get(unresolved) + .ok_or_else(|| MetricF64ResolutionError::AggregatedNodeNotFound { + aggregated_node: unresolved.clone(), + })?; + MetricF64::AggregatedStorageNodeVolume(*idx) + } + UnresolvedMetricF64::EdgeFlow(unresolved) => { + let idx = resolution_maps.edges.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::EdgeNotFound { + edge: unresolved.clone(), + } + })?; + MetricF64::EdgeFlow(*idx) + } + UnresolvedMetricF64::MultiEdgeFlow { indices, name } => { + let resolved = indices + .iter() + .map(|unresolved| { + resolution_maps.edges.get(unresolved).copied().ok_or_else(|| { + MetricF64ResolutionError::EdgeNotFound { + edge: unresolved.clone(), + } + }) + }) + .collect::, _>>()?; + MetricF64::MultiEdgeFlow { + indices: resolved, + name: name.clone(), + } + } + UnresolvedMetricF64::ParameterValue { name, return_value } => { + let idx = resolution_maps.parameters_f64.get(name).ok_or_else(|| { + MetricF64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; + + idx.into_metric_f64(*return_value) + } + UnresolvedMetricF64::IndexParameterValue { name, return_value } => { + let idx = resolution_maps.parameters_u64.get(name).ok_or_else(|| { + MetricF64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; + + idx.into_metric_f64(*return_value) + } + UnresolvedMetricF64::MultiParameterValue { + name, + key, + return_value, + } => { + let idx = resolution_maps.parameters_multi.get(name).ok_or_else(|| { + MetricF64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; + + idx.clone().into_metric_f64(key, *return_value) + } + UnresolvedMetricF64::VirtualStorageVolume(unresolved) => { + let idx = resolution_maps.virtual_storage_node.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::VirtualStorageNodeNotFound { + node: unresolved.clone(), + } + })?; + + MetricF64::VirtualStorageVolume(*idx) + } + UnresolvedMetricF64::VirtualStorageProportionalVolume(unresolved) => { + let idx = resolution_maps.virtual_storage_node.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::VirtualStorageNodeNotFound { + node: unresolved.clone(), + } + })?; + + MetricF64::VirtualStorageProportionalVolume(*idx) + } + UnresolvedMetricF64::MultiNodeInFlow { name, indices } => { + let resolved = indices + .iter() + .map(|unresolved| { + resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + }) + }) + .collect::, _>>()?; + + MetricF64::MultiNodeInFlow { + indices: resolved, + name: name.clone(), + } + } + UnresolvedMetricF64::MultiNodeOutFlow { name, indices } => { + let resolved = indices + .iter() + .map(|unresolved| { + resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + }) + }) + .collect::, _>>()?; + + MetricF64::MultiNodeOutFlow { + indices: resolved, + name: name.clone(), + } + } + UnresolvedMetricF64::NodeProportionalVolume(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + + MetricF64::NodeProportionalVolume(*idx) + } + UnresolvedMetricF64::InterNetworkTransfer(unresolved) => { + let idx = resolution_maps.inter_network_transfers.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::InterNetworkTransferNotFound { + transfer: unresolved.clone(), + } + })?; + + MetricF64::InterNetworkTransfer(*idx) + } + UnresolvedMetricF64::Constant(value) => (*value).into(), + }; + + Ok(m) + } +} + +impl From for UnresolvedMetricF64 { + fn from(v: f64) -> Self { + Self::Constant(v) + } +} + #[derive(Debug, Error)] pub enum ConstantMetricU64Error { #[error("Index parameter not found: {index}")] @@ -552,12 +820,14 @@ pub enum MetricU64 { impl MetricU64 { pub fn get_value(&self, _network: &Network, state: &State) -> Result { match self { - Self::IndexParameterValue { index, return_value } => Ok(state.get_parameter_index(*index, *return_value)?), + Self::IndexParameterValue { index, return_value } => { + Ok(state.get_general_parameter_index(*index, *return_value)?) + } Self::MultiParameterValue { index, key, return_value, - } => Ok(state.get_multi_parameter_index(*index, key, *return_value)?), + } => Ok(state.get_general_multi_parameter_index(*index, key, *return_value)?), Self::Simple(s) => Ok(s.get_value(&state.get_simple_parameter_values())?), Self::InterNetworkTransfer(_idx) => todo!("Support usize for inter-network transfers"), } @@ -609,3 +879,72 @@ impl TryFrom for ConstantMetricU64 { } } } + +#[derive(Debug, Error)] +pub enum MetricU64ResolutionError { + #[error("Node not found when resolving U64 metric: {node}")] + NodeNotFound { node: UnresolvedNode }, + #[error("Parameter not found when resolving U64 metric: {parameter}")] + ParameterNotFound { parameter: ParameterName }, +} + +pub enum UnresolvedMetricU64 { + IndexParameterValue { + name: ParameterName, + return_value: ParameterReturnValue, + }, + MultiParameterValue { + name: ParameterName, + key: String, + return_value: ParameterReturnValue, + }, + InterNetworkTransfer(MultiNetworkTransferIndex), + Constant(u64), +} + +impl UnresolvedMetricU64 { + pub fn new_index_parameter_before>(name: N) -> Self { + Self::IndexParameterValue { + name: name.into(), + return_value: ParameterReturnValue::Before, + } + } + pub fn resolve(&self, resolution_maps: &ResolutionMaps) -> Result { + let m = match self { + UnresolvedMetricU64::IndexParameterValue { name, return_value } => { + let idx = resolution_maps.parameters_u64.get(name).ok_or_else(|| { + MetricU64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; + + idx.into_metric_u64(*return_value) + } + UnresolvedMetricU64::MultiParameterValue { + name, + key, + return_value, + } => { + let idx = resolution_maps.parameters_multi.get(name).ok_or_else(|| { + MetricU64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; + + idx.clone().into_metric_u64(key, *return_value) + } + UnresolvedMetricU64::InterNetworkTransfer(_) => { + todo!() + } + UnresolvedMetricU64::Constant(value) => (*value).into(), + }; + + Ok(m) + } +} + +impl From for UnresolvedMetricU64 { + fn from(v: u64) -> Self { + Self::Constant(v) + } +} diff --git a/pywr-core/src/models/mod.rs b/pywr-core/src/models/mod.rs index 09e7ec06..cd5d2008 100644 --- a/pywr-core/src/models/mod.rs +++ b/pywr-core/src/models/mod.rs @@ -1,77 +1,75 @@ mod multi; mod simple; -use crate::scenario::{ScenarioDomain, ScenarioDomainBuilder}; -use crate::timestep::{TimeDomain, Timestepper}; +use crate::scenario::{ScenarioDomain, ScenarioDomainBuilder, ScenarioDomainBuilderError}; +use crate::timestep::{TimeDomain, TimeDomainBuilder, TimeDomainBuilderError}; pub use multi::{ - InterNetworkTransferError, MultiNetworkModel, MultiNetworkModelError, MultiNetworkModelFinaliseError, - MultiNetworkModelResult, MultiNetworkModelRunError, MultiNetworkModelSetupError, MultiNetworkModelTimings, - MultiNetworkTransferIndex, + InterNetworkTransferError, MultiModelBuilder, MultiModelBuilderError, MultiNetworkEntryBuilder, MultiNetworkModel, + MultiNetworkModelFinaliseError, MultiNetworkModelResult, MultiNetworkModelRunError, MultiNetworkModelSetupError, + MultiNetworkModelTimings, MultiNetworkTransferIndex, }; pub use simple::{ - Model, ModelFinaliseError, ModelResult, ModelRunError, ModelSetupError, ModelState, ModelStepError, ModelTimings, + Model, ModelBuilder, ModelBuilderError, ModelFinaliseError, ModelResult, ModelRunError, ModelSetupError, + ModelState, ModelStepError, ModelTimings, }; use thiserror::Error; #[derive(Error, Debug)] pub enum ModelDomainError { #[error("Error in time domain: {0}")] - TimestepError(#[from] crate::timestep::TimestepError), + TimestepError(#[from] crate::timestep::TimeDomainBuilderError), #[error("Error in scenario domain: {0}")] - ScenarioError(#[from] crate::scenario::ScenarioError), + ScenarioError(#[from] crate::scenario::ScenarioDomainBuilderError), } #[derive(Debug, Clone)] pub struct ModelDomain { time: TimeDomain, - scenarios: ScenarioDomain, + scenario: ScenarioDomain, } impl ModelDomain { - pub fn new(time: TimeDomain, scenarios: ScenarioDomain) -> Self { - Self { time, scenarios } - } - - pub fn try_from( - timestepper: Timestepper, - scenario_builder: ScenarioDomainBuilder, - ) -> Result { - Ok(Self { - time: TimeDomain::try_from(timestepper)?, - scenarios: scenario_builder.build()?, - }) - } - pub fn time(&self) -> &TimeDomain { &self.time } pub fn scenarios(&self) -> &ScenarioDomain { - &self.scenarios + &self.scenario } pub fn shape(&self) -> (usize, usize) { - (self.time.timesteps().len(), self.scenarios.indices().len()) + (self.time.timesteps().len(), self.scenario.indices().len()) } } -impl TryFrom for ModelDomain { - type Error = ModelDomainError; +#[derive(Debug, Error)] +pub enum ModelDomainBuilderError { + #[error("Error building time domain: {0}")] + Time(#[from] TimeDomainBuilderError), + #[error("Error building scenario domain: {0}")] + Scenario(#[from] ScenarioDomainBuilderError), +} - fn try_from(value: Timestepper) -> Result { - let time = TimeDomain::try_from(value)?; - Ok(Self { - time, - scenarios: ScenarioDomainBuilder::default().build()?, - }) - } +#[derive(Clone)] +pub struct ModelDomainBuilder { + time: TimeDomainBuilder, + scenario: Option, } -impl From for ModelDomain { - fn from(value: TimeDomain) -> Self { - Self { - time: value, - scenarios: ScenarioDomainBuilder::default().build().unwrap(), - } +impl ModelDomainBuilder { + pub fn new(time: TimeDomainBuilder) -> Self { + Self { time, scenario: None } + } + + pub fn scenario(&mut self, scenario: ScenarioDomainBuilder) -> &mut Self { + self.scenario = Some(scenario); + self + } + + pub fn build(self) -> Result { + let time = self.time.build()?; + let scenario = self.scenario.unwrap_or_default().build()?; + + Ok(ModelDomain { time, scenario }) } } diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index 6a11e477..f1d03e36 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -1,8 +1,9 @@ -use crate::metric::{MetricF64, MetricF64Error}; -use crate::models::ModelDomain; +use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; +use crate::models::{ModelDomain, ModelDomainBuilder, ModelDomainBuilderError}; use crate::network::{ - Network, NetworkFinaliseError, NetworkRecorderSaveError, NetworkRecorderSetupError, NetworkResult, - NetworkSetupError, NetworkSolverSetupError, NetworkState, NetworkStepError, NetworkTimings, RunDuration, + Network, NetworkBuildError, NetworkBuilder, NetworkFinaliseError, NetworkRecorderSaveError, + NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, NetworkStepError, NetworkTimings, + RunDuration, }; use crate::recorders::RecorderInternalState; use crate::scenario::ScenarioIndex; @@ -74,7 +75,7 @@ impl Display for MultiNetworkTransferIndex { /// A special parameter that retrieves a value from a metric in another model. struct MultiNetworkTransfer { - /// The model to get the value from. + /// The network to get the value from. from_model_idx: OtherNetworkIndex, /// The metric to get the value from. from_metric: MetricF64, @@ -82,12 +83,46 @@ struct MultiNetworkTransfer { initial_value: Option, } +pub struct UnresolvedMultiNetworkTransfer { + /// Name of this transfer + name: String, + /// Name of the network to get the value from. + from_model: String, + /// Metric to get the value from. + from_metric: UnresolvedMetricF64, + /// Optional initial value to use on the first time-step + initial_value: Option, +} + struct MultiNetworkEntry { name: String, network: Network, transfers: Vec, } +pub struct MultiNetworkEntryBuilder { + name: String, + network: NetworkBuilder, + transfers: Vec, +} + +impl MultiNetworkEntryBuilder { + /// Create a new builder. + pub fn new(name: &str, network: NetworkBuilder) -> Self { + Self { + name: name.to_string(), + network, + transfers: Vec::new(), + } + } + + /// Add a new unresolved transfer to the builder. + pub fn transfer(&mut self, transfer: UnresolvedMultiNetworkTransfer) -> &mut Self { + self.transfers.push(transfer); + self + } +} + pub struct MultiNetworkModelState { current_time_step_idx: usize, states: Vec, @@ -238,12 +273,6 @@ impl MultiNetworkModelTimings { } } -#[derive(Debug, Error)] -pub enum MultiNetworkModelError { - #[error("Network name `{0}` already exists")] - NetworkNameAlreadyExists(String), -} - /// The results of a model run. /// /// Only recorders which produced a result will be present. @@ -316,22 +345,6 @@ impl MultiNetworkModel { self.networks.iter().position(|n| n.name == name) } - /// Add a [`Network`] to the model. The name must be unique. - pub fn add_network(&mut self, name: &str, network: Network) -> Result { - if self.get_network_index_by_name(name).is_some() { - return Err(MultiNetworkModelError::NetworkNameAlreadyExists(name.to_string())); - } - - let idx = self.networks.len(); - self.networks.push(MultiNetworkEntry { - name: name.to_string(), - network, - transfers: Vec::new(), - }); - - Ok(idx) - } - /// Add a transfer of data from one network to another. pub fn add_inter_network_transfer( &mut self, @@ -358,7 +371,7 @@ impl MultiNetworkModel { ::Settings: SolverSettings, { let timesteps = self.domain.time.timesteps(); - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); let mut states = Vec::with_capacity(self.networks.len()); let mut recorder_states = Vec::with_capacity(self.networks.len()); @@ -408,7 +421,7 @@ impl MultiNetworkModel { ::Settings: SolverSettings, { let timesteps = self.domain.time.timesteps(); - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); let mut states = Vec::with_capacity(self.networks.len()); let mut recorder_states = Vec::with_capacity(self.networks.len()); @@ -504,7 +517,7 @@ impl MultiNetworkModel { .get(state.current_time_step_idx) .ok_or(MultiNetworkModelStepError::EndOfTimesteps)?; - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); for (idx, entry) in self.networks.iter().enumerate() { let timing = timings @@ -581,7 +594,7 @@ impl MultiNetworkModel { .get(state.current_time_step_idx) .ok_or(MultiNetworkModelStepError::EndOfTimesteps)?; - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); for (idx, entry) in self.networks.iter().enumerate() { let timing = timings @@ -655,7 +668,7 @@ impl MultiNetworkModel { let result = entry .network .finalise( - self.domain.scenarios.indices(), + self.domain.scenario.indices(), sub_model_ms_states, sub_model_recorder_states, ) @@ -696,7 +709,7 @@ impl MultiNetworkModel { let result = entry .network .finalise( - self.domain.scenarios.indices(), + self.domain.scenario.indices(), sub_model_ms_states, sub_model_recorder_states, ) @@ -768,7 +781,7 @@ impl MultiNetworkModel { timings .run_duration - .complete_scenarios(self.domain.scenarios.indices().len()); + .complete_scenarios(self.domain.scenario.indices().len()); } Ok(()) @@ -822,7 +835,7 @@ impl MultiNetworkModel { timings .run_duration - .complete_scenarios(self.domain.scenarios.indices().len()); + .complete_scenarios(self.domain.scenario.indices().len()); } Ok(()) @@ -915,6 +928,134 @@ impl MultiNetworkModel { } } +#[derive(Debug, Error)] +pub enum MultiModelBuilderError { + #[error("Error building model domain: {0}")] + ModelDomainBuilderError(#[from] ModelDomainBuilderError), + #[error("Error building network `{name}`: {source}")] + NetworkBuilderError { + name: String, + #[source] + source: Box, + }, + #[error("Network `{network_name}` not found when resolving transfer.")] + NetworkNotFoundForTransfer { name: String, network_name: String }, + #[error("Could not resolve f64 metric for transfer `{name}` attribute: {source}")] + ResolveMetricF64ForTransferError { + name: String, + #[source] + source: MetricF64ResolutionError, + }, + #[error("Duplicate network name `{name}` found.")] + DuplicateNetworkName { name: String }, +} + +pub struct MultiModelBuilder { + domain: ModelDomainBuilder, + networks: Vec, +} + +impl MultiModelBuilder { + pub fn new(domain: ModelDomainBuilder) -> Self { + Self { + domain, + networks: Vec::new(), + } + } + + pub fn network(&mut self, network: MultiNetworkEntryBuilder) -> &mut Self { + self.networks.push(network); + self + } + + pub fn build(self) -> Result { + let scenario_group_map = match &self.domain.scenario { + Some(scenario) => scenario.group_map(), + None => Default::default(), + }; + + let domain = self.domain.build()?; + + let mut networks = Vec::with_capacity(self.networks.len()); + let mut network_map = HashMap::with_capacity(self.networks.len()); + let mut resolution_maps = Vec::with_capacity(self.networks.len()); + + for (i, entry_builder) in self.networks.into_iter().enumerate() { + let name = entry_builder.name; + let transfers_map: HashMap<_, _> = entry_builder + .transfers + .iter() + .enumerate() + .map(|(i, t)| { + let idx = MultiNetworkTransferIndex(i); + let name = t.name.clone(); + (name, idx) + }) + .collect(); + + let (network, resolution_map) = entry_builder + .network + .build(&scenario_group_map, &transfers_map) + .map_err(|source| MultiModelBuilderError::NetworkBuilderError { + name: name.clone(), + source: Box::new(source), + })?; + + networks.push((name.clone(), network, entry_builder.transfers)); + resolution_maps.push(resolution_map); + + if network_map.contains_key(&name) { + return Err(MultiModelBuilderError::DuplicateNetworkName { name }); + } + + network_map.insert(name.clone(), i); + } + + let mut entries = Vec::with_capacity(networks.len()); + for (name, network, unresolved_transfers) in networks { + let transfers = unresolved_transfers + .iter() + .map(|t| { + let idx = network_map.get(&t.from_model).copied().ok_or_else(|| { + MultiModelBuilderError::NetworkNotFoundForTransfer { + name: t.name.clone(), + network_name: t.from_model.clone(), + } + })?; + + let r_map = &resolution_maps[idx]; + + let metric = t.from_metric.resolve(r_map).map_err(|source| { + MultiModelBuilderError::ResolveMetricF64ForTransferError { + name: t.name.clone(), + source, + } + })?; + + Ok(MultiNetworkTransfer { + from_model_idx: OtherNetworkIndex::new(idx, entries.len()), + from_metric: metric, + initial_value: t.initial_value, + }) + }) + .collect::, MultiModelBuilderError>>()?; + + entries.push({ + MultiNetworkEntry { + name, + network, + transfers, + } + }) + } + + Ok(MultiNetworkModel { + domain, + networks: entries, + }) + } +} + #[derive(Debug, Error)] pub enum InterNetworkTransferError { #[error("Error retrieving value to transfer to other network: {source}")] @@ -983,39 +1124,39 @@ fn compute_inter_network_transfers( #[cfg(test)] mod tests { - use super::{MultiNetworkModel, MultiNetworkModelTimings}; - use crate::models::ModelDomain; - use crate::network::Network; + use super::{MultiModelBuilder, MultiNetworkEntryBuilder, MultiNetworkModelTimings}; + use crate::models::ModelDomainBuilder; + use crate::network::NetworkBuilder; use crate::scenario::{ScenarioDomainBuilder, ScenarioGroupBuilder}; use crate::solvers::ClpSolver; - use crate::test_utils::{default_timestepper, simple_network}; + use crate::test_utils::{default_time_domain_builder, simple_network}; /// Test basic [`MultiNetworkModel`] functionality by running two independent models. #[test] fn test_multi_model_step() { // Create two simple models - let timestepper = default_timestepper(); + let time_builder = default_time_domain_builder(); let mut scenario_builder = ScenarioDomainBuilder::default(); let scenario_group = ScenarioGroupBuilder::new("test-scenario", 2).build().unwrap(); scenario_builder = scenario_builder.with_group(scenario_group).unwrap(); - let mut multi_model = MultiNetworkModel::new(ModelDomain::try_from(timestepper, scenario_builder).unwrap()); + let mut domain_builder = ModelDomainBuilder::new(time_builder); + domain_builder.scenario(scenario_builder); - let test_scenario_group_idx = multi_model - .domain() - .scenarios - .group_index("test-scenario") - .expect("Scenario group not found."); + let mut builder = MultiModelBuilder::new(domain_builder); - let mut network1 = Network::default(); - simple_network(&mut network1, test_scenario_group_idx, 2); + let mut network1 = NetworkBuilder::default(); + simple_network(&mut network1, "test-scenario", 2); + let network1_entry = MultiNetworkEntryBuilder::new("network1", network1); + builder.network(network1_entry); - let mut network2 = Network::default(); - simple_network(&mut network2, test_scenario_group_idx, 2); + let mut network2 = NetworkBuilder::default(); + simple_network(&mut network2, "test-scenario", 2); + let network2_entry = MultiNetworkEntryBuilder::new("network2", network2); + builder.network(network2_entry); - let _network1_idx = multi_model.add_network("network1", network1); - let _network2_idx = multi_model.add_network("network2", network2); + let multi_model = builder.build().unwrap(); let mut state = multi_model .setup::(&Default::default()) @@ -1030,16 +1171,13 @@ mod tests { #[test] fn test_duplicate_network_names() { - let timestepper = default_timestepper(); - let scenario_collection = ScenarioDomainBuilder::default(); + let time_builder = default_time_domain_builder(); - let mut multi_model = MultiNetworkModel::new(ModelDomain::try_from(timestepper, scenario_collection).unwrap()); + let mut builder = MultiModelBuilder::new(ModelDomainBuilder::new(time_builder)); - let network = Network::default(); - let _network1_idx = multi_model.add_network("network1", network); - let network = Network::default(); - let result = multi_model.add_network("network1", network); + builder.network(MultiNetworkEntryBuilder::new("network1", NetworkBuilder::default())); + builder.network(MultiNetworkEntryBuilder::new("network1", NetworkBuilder::default())); - assert!(result.is_err()); + assert!(builder.build().is_err()); } } diff --git a/pywr-core/src/models/simple.rs b/pywr-core/src/models/simple.rs index dc24ec9e..31deb006 100644 --- a/pywr-core/src/models/simple.rs +++ b/pywr-core/src/models/simple.rs @@ -1,7 +1,8 @@ -use crate::models::ModelDomain; +use crate::models::{ModelDomain, ModelDomainBuilder, ModelDomainBuilderError}; use crate::network::{ - Network, NetworkFinaliseError, NetworkRecorderSaveError, NetworkRecorderSetupError, NetworkResult, - NetworkSetupError, NetworkSolverSetupError, NetworkState, NetworkStepError, NetworkTimings, RunDuration, + Network, NetworkBuildError, NetworkBuilder, NetworkFinaliseError, NetworkRecorderSaveError, + NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, + NetworkStepError, NetworkTimings, RunDuration, }; use crate::recorders::RecorderInternalState; #[cfg(all(feature = "cbc", feature = "pyo3"))] @@ -19,7 +20,7 @@ use crate::timestep::Timestep; #[cfg(feature = "pyo3")] use pyo3::{Bound, PyErr, PyResult, Python, exceptions::PyRuntimeError, pyclass, pymethods, types::PyDict}; use rayon::ThreadPool; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use thiserror::Error; use tracing::{debug, info}; @@ -180,7 +181,7 @@ impl ModelResult { format!( "", self.network_result.len(), - self.domain.scenarios.len(), + self.domain.scenario.len(), self.timings.total_duration(), self.timings.speed() ) @@ -239,7 +240,7 @@ impl Model { ::Settings: SolverSettings, { let timesteps = self.domain.time.timesteps(); - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); let state = self .network @@ -269,7 +270,7 @@ impl Model { ::Settings: SolverSettings, { let timesteps = self.domain.time.timesteps(); - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); let state = self .network @@ -308,7 +309,7 @@ impl Model { .get(state.current_time_step_idx) .ok_or(ModelStepError::EndOfTimesteps)?; - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); debug!("Starting timestep {:?}", timestep); let solvers = &mut state.solvers; @@ -370,7 +371,7 @@ impl Model { .get(state.current_time_step_idx) .ok_or(ModelStepError::EndOfTimesteps)?; - let scenario_indices = self.domain.scenarios.indices(); + let scenario_indices = self.domain.scenario.indices(); debug!("Starting timestep {:?}", timestep); let solvers = &mut state.solvers; @@ -418,7 +419,7 @@ impl Model { let network_result = self .network .finalise( - self.domain.scenarios.indices(), + self.domain.scenario.indices(), state.state.all_metric_set_internal_states_mut(), state.recorder_state, ) @@ -448,7 +449,7 @@ impl Model { let network_result = self .network .finalise( - self.domain.scenarios.indices(), + self.domain.scenario.indices(), state.state.all_metric_set_internal_states_mut(), state.recorder_state, ) @@ -517,7 +518,7 @@ impl Model { timings .run_duration - .complete_scenarios(self.domain.scenarios.indices().len()); + .complete_scenarios(self.domain.scenario.indices().len()); } Ok(()) @@ -569,7 +570,7 @@ impl Model { timings .run_duration - .complete_scenarios(self.domain.scenarios.indices().len()); + .complete_scenarios(self.domain.scenario.indices().len()); } Ok(()) @@ -600,6 +601,40 @@ impl Model { } } +#[derive(Debug, Error)] +pub enum ModelBuilderError { + #[error("Error building model domain: {0}")] + ModelDomainBuilderError(#[from] ModelDomainBuilderError), + #[error("Error building network: {0}")] + NetworkBuildError(#[from] NetworkBuildError), +} + +pub struct ModelBuilder { + domain: ModelDomainBuilder, + network: NetworkBuilder, +} + +impl ModelBuilder { + pub fn new(domain: ModelDomainBuilder, network: NetworkBuilder) -> Self { + Self { domain, network } + } + + /// Get a reference to the [`NetworkBuilder`] + pub fn network_builder(&mut self) -> &mut NetworkBuilder { + &mut self.network + } + + pub fn build(self) -> Result { + let scenario_group_map = match &self.domain.scenario { + Some(scenario) => scenario.group_map(), + None => Default::default(), + }; + let domain = self.domain.build()?; + let (network, _) = self.network.build(&scenario_group_map, &HashMap::new())?; + Ok(Model { domain, network }) + } +} + /// Run a model using the specified multi solver unlocking the GIL #[cfg(feature = "pyo3")] #[pymethods] diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 8e2f9fc3..fb04b178 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -1,34 +1,41 @@ -use crate::aggregated_node::{AggregatedNode, AggregatedNodeIndex, AggregatedNodeVec, Relationship}; -use crate::aggregated_storage_node::{AggregatedStorageNode, AggregatedStorageNodeIndex, AggregatedStorageNodeVec}; -use crate::edge::{Edge, EdgeIndex, EdgeVec}; -use crate::metric::{MetricF64, SimpleMetricF64}; -use crate::models::ModelDomain; -use crate::node::{Node, NodeError, NodeVec, StorageInitialVolume}; +use crate::aggregated_node::{AggregatedNode, AggregatedNodeBuilder, AggregatedNodeBuilderError}; +use crate::aggregated_storage_node::{ + AggregatedStorageNode, AggregatedStorageNodeBuilder, AggregatedStorageNodeBuilderError, +}; +use crate::edge::Edge; +use crate::models::{ModelDomain, MultiNetworkTransferIndex}; +use crate::node::{Node, NodeBuilder, NodeBuilderError, NodeError, UnresolvedNode}; use crate::parameters::{ - GeneralParameterIndex, GeneralParameterType, ParameterCalculationError, ParameterCollection, - ParameterCollectionConstCalculationError, ParameterCollectionError, ParameterCollectionSetupError, - ParameterCollectionSimpleCalculationError, ParameterIndex, ParameterName, ParameterStates, VariableConfig, + GeneralParameterIndex, GeneralParameterType, ParameterCollection, ParameterCollectionBuilder, + ParameterCollectionBuilderError, ParameterCollectionConstCalculationError, ParameterCollectionError, + ParameterCollectionGeneralCalculationError, ParameterCollectionSetupError, + ParameterCollectionSimpleCalculationError, ParameterIndex, ParameterName, ParameterStates, ParameterTiming, + ParameterTimings, VariableConfig, }; use crate::recorders::{ - MetricSet, MetricSetIndex, MetricSetSaveError, MetricSetState, RecorderAggregationError, RecorderFinalResult, - RecorderFinaliseError, RecorderInternalState, RecorderSaveError, RecorderSetupError, + MetricSet, MetricSetIndex, MetricSetSaveError, MetricSetState, RecorderAggregationError, RecorderBuilder, + RecorderBuilderError, RecorderFinalResult, RecorderFinaliseError, RecorderInternalState, RecorderSaveError, + RecorderSetupError, }; use crate::scenario::ScenarioIndex; use crate::solvers::{ MultiStateSolver, Solver, SolverFeatures, SolverSettings, SolverSetupError, SolverSolveError, SolverTimings, }; -use crate::state::{MultiValue, SetStateError, State, StateBuilder}; +use crate::state::{MultiValue, State, StateBuilder}; use crate::timestep::Timestep; use crate::virtual_storage::{ - VirtualStorage, VirtualStorageBuilder, VirtualStorageError, VirtualStorageIndex, VirtualStorageVec, + VirtualStorageError, VirtualStorageNode, VirtualStorageNodeBuilder, VirtualStorageNodeBuilderError, }; -use crate::{NodeIndex, RecorderIndex, parameters, recorders}; +use crate::{parameters, recorders}; #[cfg(feature = "pyo3")] use pyo3::{PyResult, exceptions::PyKeyError, pyclass, pymethods}; #[cfg(feature = "pyo3")] use pyo3_polars::PyDataFrame; use rayon::prelude::*; use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; use std::slice::{Iter, IterMut}; use std::sync::Arc; use std::time::Duration; @@ -120,50 +127,20 @@ impl RunDuration { } } -#[derive(Default, Copy, Clone)] -pub struct ComponentTiming { - calculation: Duration, - after: Duration, -} - -impl ComponentTiming { - /// Time spent in the calculation method of the component. - pub fn calculation(&self) -> Duration { - self.calculation - } - - /// Time spent in the "after" method of the component. - pub fn after(&self) -> Duration { - self.after - } - - /// Total time spent in calculation and after methods. - pub fn total(&self) -> Duration { - self.calculation + self.after - } -} - /// Collect timing information for component of a network. #[derive(Clone)] pub struct ComponentTimings { - /// Timing information for calculation of each component. - calculation: Option>, + /// Timing information for parameters. + parameters: Option, /// Total time spent in component calculations. total: Duration, } impl ComponentTimings { - pub fn new_with_components(num_components: usize) -> Self { - Self { - calculation: Some(vec![ComponentTiming::default(); num_components]), - total: Duration::ZERO, - } - } - - pub fn new_without_components() -> Self { + pub fn new(parameters: Option) -> Self { Self { - calculation: None, - total: Duration::ZERO, + parameters, + total: Default::default(), } } @@ -173,35 +150,11 @@ impl ComponentTimings { pub fn slowest_components( &self, n: usize, - component_types: &[ComponentType], - ) -> Option> { - self.calculation.as_ref().map(|calculation| { - let mut components: Vec<_> = calculation - .iter() - .zip(component_types) - .map(|(d, ct)| (*ct, *d)) - .collect(); - components.sort_by_key(|(_, duration)| duration.total()); - components.iter().rev().take(n).map(|(ct, d)| (*ct, *d)).collect() - }) - } - - /// Add timing information for a component calculation. - pub fn add_component_calculation_timing(&mut self, idx: usize, duration: Duration) { - if let Some(calculation) = &mut self.calculation { - if let Some(c) = calculation.get_mut(idx) { - c.calculation += duration; - } - } - } - - /// Add timing information for a component "after" calculation. - pub fn add_component_after_timing(&mut self, idx: usize, duration: Duration) { - if let Some(calculation) = &mut self.calculation { - if let Some(c) = calculation.get_mut(idx) { - c.after += duration; - } - } + collection: &ParameterCollection, + ) -> Option> { + self.parameters + .as_ref() + .map(|p| p.slowest_parameters_named(n, collection)) } } @@ -216,8 +169,9 @@ pub struct NetworkTimings { impl NetworkTimings { pub fn new_with_component_timings(network: &Network) -> Self { + let parameter_timings = ParameterTimings::from_collection(&network.parameters); Self { - component_timings: ComponentTimings::new_with_components(network.resolve_order.len()), + component_timings: ComponentTimings::new(Some(parameter_timings)), recorder_saving: Duration::ZERO, solve: SolverTimings::default(), } @@ -225,7 +179,7 @@ impl NetworkTimings { pub fn new_without_component_timings() -> Self { Self { - component_timings: ComponentTimings::new_without_components(), + component_timings: ComponentTimings::new(None), recorder_saving: Duration::ZERO, solve: SolverTimings::default(), } @@ -288,18 +242,18 @@ impl NetworkTimings { 100.0 * not_counted / total_duration, ); - if let Some(slowest) = self.component_timings.slowest_components(10, &network.resolve_order) { + if let Some(slowest) = self.component_timings.slowest_components(10, &network.parameters) { info!("Slowest components:"); info!( " {: <24} | {: <10} | {: <10} | {: <10} | {:5}", "Component", "before", "after", "total", "% of total" ); - for (ct, duration) in slowest { + for (name, duration) in slowest { info!( " {: <24} | {: <10.5}s | {: <10.5}s | {: <10.5}s | {:5.2}%", - ct.name(network), - duration.calculation.as_secs_f64(), - duration.after.as_secs_f64(), + name.to_string(), + duration.before().as_secs_f64(), + duration.after().as_secs_f64(), duration.total().as_secs_f64(), 100.0 * duration.total().as_secs_f64() / total_duration, ); @@ -419,36 +373,8 @@ pub enum NetworkStepError { ParameterU64IndexNotFound(GeneralParameterIndex), #[error("General parameter Multi index '{0}' not found.")] ParameterMultiIndexNotFound(GeneralParameterIndex), - #[error("Error calculating value for parameter `{name}`: {source}")] - ParameterCalculationError { - name: ParameterName, - #[source] - source: Box, - }, - #[error("Error performing `after` method on parameter `{name}`: {source}")] - ParameterAfterError { - name: ParameterName, - #[source] - source: Box, - }, - #[error("Error setting state for general F64 parameter `{name}`: {source}")] - ParameterF64SetStateError { - name: ParameterName, - #[source] - source: SetStateError>, - }, - #[error("Error setting state for general U64 parameter `{name}`: {source}")] - ParameterU64SetStateError { - name: ParameterName, - #[source] - source: SetStateError>, - }, - #[error("Error setting state for general Multi parameter `{name}`: {source}")] - ParameterMultiSetStateError { - name: ParameterName, - #[source] - source: SetStateError>, - }, + #[error("Error computing general parameters: `{0}`")] + GeneralParameterCalculationError(#[from] Box), #[error("Error saving metric set `{name}`: `{source}`")] MetricSetSaveError { name: String, @@ -618,39 +544,38 @@ impl NetworkResult { /// #[derive(Default)] pub struct Network { - nodes: NodeVec, - edges: EdgeVec, - aggregated_nodes: AggregatedNodeVec, - aggregated_storage_nodes: AggregatedStorageNodeVec, - virtual_storage_nodes: VirtualStorageVec, + nodes: Vec, + edges: Vec, + aggregated_nodes: Vec, + aggregated_storage_nodes: Vec, + virtual_storage_nodes: Vec, parameters: ParameterCollection, metric_sets: Vec, - resolve_order: Vec, recorders: Vec>, } impl Network { - pub fn nodes(&self) -> &NodeVec { + pub fn nodes(&self) -> &[Node] { &self.nodes } - pub fn edges(&self) -> &EdgeVec { + pub fn edges(&self) -> &[Edge] { &self.edges } - pub fn recorders(&self) -> &Vec> { + pub fn recorders(&self) -> &[Box] { &self.recorders } - pub fn aggregated_nodes(&self) -> &AggregatedNodeVec { + pub fn aggregated_nodes(&self) -> &[AggregatedNode] { &self.aggregated_nodes } - pub fn aggregated_storage_nodes(&self) -> &AggregatedStorageNodeVec { + pub fn aggregated_storage_nodes(&self) -> &[AggregatedStorageNode] { &self.aggregated_storage_nodes } - pub fn virtual_storage_nodes(&self) -> &VirtualStorageVec { + pub fn virtual_storage_nodes(&self) -> &[VirtualStorageNode] { &self.virtual_storage_nodes } @@ -1068,7 +993,7 @@ impl Network { scenario_index: &ScenarioIndex, state: &mut State, internal_states: &mut ParameterStates, - mut timings: Option<&mut ComponentTimings>, + timings: Option<&mut ComponentTimings>, ) -> Result<(), NetworkStepError> { // TODO reset parameter state to zero @@ -1076,121 +1001,34 @@ impl Network { self.parameters .compute_simple(timestep, scenario_index, state, internal_states)?; - for (c_idx, c_type) in self.resolve_order.iter().enumerate() { - let start = Instant::now(); + // Next run "before" on nodes and virtual nodes + for n in &self.nodes { + n.before(timestep, state) + .map_err(|source| NetworkStepError::NodeBeforeError { + name: n.name().to_string(), + source, + })?; + } - match c_type { - ComponentType::Node(idx) => { - let n = self - .nodes - .get(idx) - .ok_or_else(|| NetworkStepError::NodeIndexNotFound(*idx))?; + for vs in &self.virtual_storage_nodes { + vs.before(timestep, state) + .map_err(|source| NetworkStepError::VirtualStorageBeforeError { + name: vs.name().to_string(), + source, + })?; + } - n.before(timestep, state) - .map_err(|source| NetworkStepError::NodeBeforeError { - name: n.name().to_string(), - source, - })?; - } - ComponentType::VirtualStorageNode(idx) => { - let n = self - .virtual_storage_nodes - .get(idx) - .ok_or_else(|| NetworkStepError::VirtualStorageIndexNotFound(*idx))?; - - n.before(timestep, state) - .map_err(|source| NetworkStepError::VirtualStorageBeforeError { - name: n.name().to_string(), - source, - })?; - } - ComponentType::Parameter(p_type) => { - match p_type { - GeneralParameterType::Parameter(idx) => { - // Find the parameter itself - let p = self - .parameters - .get_general_f64(*idx) - .ok_or_else(|| NetworkStepError::ParameterF64IndexNotFound(*idx))?; - - // ... and its internal state - let internal_state = internal_states - .get_general_mut_f64_state(*idx) - .ok_or_else(|| NetworkStepError::ParameterF64IndexNotFound(*idx))?; - - let value = p - .before(timestep, scenario_index, self, state, internal_state) - .map_err(|source| NetworkStepError::ParameterCalculationError { - name: p.name().clone(), - source: Box::new(source), - })?; - - state.set_parameter_value_before(*idx, value).map_err(|source| { - NetworkStepError::ParameterF64SetStateError { - name: p.name().clone(), - source, - } - })?; - } - GeneralParameterType::Index(idx) => { - let p = self - .parameters - .get_general_u64(*idx) - .ok_or_else(|| NetworkStepError::ParameterU64IndexNotFound(*idx))?; - - // ... and its internal state - let internal_state = internal_states - .get_general_mut_u64_state(*idx) - .ok_or_else(|| NetworkStepError::ParameterU64IndexNotFound(*idx))?; - - let value = p - .before(timestep, scenario_index, self, state, internal_state) - .map_err(|source| NetworkStepError::ParameterCalculationError { - name: p.name().clone(), - source: Box::new(source), - })?; - - state.set_parameter_index_before(*idx, value).map_err(|source| { - NetworkStepError::ParameterU64SetStateError { - name: p.name().clone(), - source, - } - })?; - } - GeneralParameterType::Multi(idx) => { - let p = self - .parameters - .get_general_multi(idx) - .ok_or_else(|| NetworkStepError::ParameterMultiIndexNotFound(*idx))?; - - // ... and its internal state - let internal_state = internal_states - .get_general_mut_multi_state(*idx) - .ok_or_else(|| NetworkStepError::ParameterMultiIndexNotFound(*idx))?; - - let value = p - .before(timestep, scenario_index, self, state, internal_state) - .map_err(|source| NetworkStepError::ParameterCalculationError { - name: p.name().clone(), - source: Box::new(source), - })?; - // debug!("Current value of index parameter {}: {}", p.name(), value); - state.set_multi_parameter_value_before(*idx, value).map_err(|source| { - NetworkStepError::ParameterMultiSetStateError { - name: p.name().clone(), - source, - } - })?; - } - } - } - } + let p_timings = timings.and_then(|timings| timings.parameters.as_mut()); - if let Some(timings) = timings.as_deref_mut() { - // Update the component timings - timings.add_component_calculation_timing(c_idx, start.elapsed()); - } - } + // Now we can compute the general parameters that may depend on node state. + self.parameters + .compute_general(timestep, scenario_index, self, state, internal_states, p_timings) + .map_err(|source| NetworkStepError::GeneralParameterCalculationError(Box::new(source)))?; + + // if let Some(timings) = timings.as_deref_mut() { + // // Update the component timings + // timings.add_component_calculation_timing(c_idx, start.elapsed()); + // } Ok(()) } @@ -1209,110 +1047,21 @@ impl Network { state: &mut State, internal_states: &mut ParameterStates, metric_set_states: &mut [MetricSetState], - mut timings: Option<&mut ComponentTimings>, + timings: Option<&mut ComponentTimings>, ) -> Result<(), NetworkStepError> { // TODO reset parameter state to zero self.parameters .after_simple(timestep, scenario_index, state, internal_states)?; - for (c_idx, c_type) in self.resolve_order.iter().enumerate() { - let start = Instant::now(); + // No "after" on nodes and virtual nodes - match c_type { - ComponentType::Node(_) => { - // Nodes do not have an "after" method. - } - ComponentType::VirtualStorageNode(_) => { - // Nodes do not have an "after" method.; - } - ComponentType::Parameter(p_type) => { - match p_type { - GeneralParameterType::Parameter(idx) => { - // Find the parameter itself - let p = self - .parameters - .get_general_f64(*idx) - .ok_or_else(|| NetworkStepError::ParameterF64IndexNotFound(*idx))?; - - // ... and its internal state - let internal_state = internal_states - .get_general_mut_f64_state(*idx) - .ok_or_else(|| NetworkStepError::ParameterF64IndexNotFound(*idx))?; - - let value = - p.after(timestep, scenario_index, self, state, internal_state) - .map_err(|source| NetworkStepError::ParameterAfterError { - name: p.name().clone(), - source: Box::new(source), - })?; - - state.set_parameter_value_after(*idx, value).map_err(|source| { - NetworkStepError::ParameterF64SetStateError { - name: p.name().clone(), - source, - } - })?; - } - GeneralParameterType::Index(idx) => { - let p = self - .parameters - .get_general_u64(*idx) - .ok_or_else(|| NetworkStepError::ParameterU64IndexNotFound(*idx))?; - - // .. and its internal state - let internal_state = internal_states - .get_general_mut_u64_state(*idx) - .ok_or_else(|| NetworkStepError::ParameterU64IndexNotFound(*idx))?; - - let value = - p.after(timestep, scenario_index, self, state, internal_state) - .map_err(|source| NetworkStepError::ParameterAfterError { - name: p.name().clone(), - source: Box::new(source), - })?; - - state.set_parameter_index_after(*idx, value).map_err(|source| { - NetworkStepError::ParameterU64SetStateError { - name: p.name().clone(), - source, - } - })?; - } - GeneralParameterType::Multi(idx) => { - let p = self - .parameters - .get_general_multi(idx) - .ok_or_else(|| NetworkStepError::ParameterMultiIndexNotFound(*idx))?; - - // .. and its internal state - let internal_state = internal_states - .get_general_mut_multi_state(*idx) - .ok_or_else(|| NetworkStepError::ParameterMultiIndexNotFound(*idx))?; - - let value = - p.after(timestep, scenario_index, self, state, internal_state) - .map_err(|source| NetworkStepError::ParameterAfterError { - name: p.name().clone(), - source: Box::new(source), - })?; - - state.set_multi_parameter_value_after(*idx, value).map_err(|source| { - NetworkStepError::ParameterMultiSetStateError { - name: p.name().clone(), - source, - } - })?; - } - } - } - } + let p_timings = timings.and_then(|timings| timings.parameters.as_mut()); - if let Some(timings) = timings.as_deref_mut() { - // Update the component timings - timings.add_component_after_timing(c_idx, start.elapsed()); - } - } + // Now we can compute the general parameters that may depend on node state. + self.parameters + .after_general(timestep, scenario_index, self, state, internal_states, p_timings) + .map_err(|source| NetworkStepError::GeneralParameterCalculationError(Box::new(source)))?; // Finally, save new data to the metric set for (metric_set, ms_state) in self.metric_sets.iter().zip(metric_set_states.iter_mut()) { @@ -1357,7 +1106,7 @@ impl Network { /// Get an [`Edge`] from an edge's index pub fn get_edge(&self, index: &EdgeIndex) -> Option<&Edge> { - self.edges.get(index) + self.edges.get(index.0) } /// Get an [`EdgeIndex`] from connecting node indices. @@ -1374,12 +1123,12 @@ impl Network { /// Get a Node from a node's index pub fn get_node(&self, index: &NodeIndex) -> Option<&Node> { - self.nodes.get(index) + self.nodes.get(*index.deref()) } /// Get a Node from a node's index pub fn get_node_mut(&mut self, index: &NodeIndex) -> Option<&mut Node> { - self.nodes.get_mut(index) + self.nodes.get_mut(*index.deref()) } /// Get a Node from a node's name @@ -1392,135 +1141,9 @@ impl Network { self.nodes.iter_mut().find(|n| n.full_name() == (name, sub_name)) } - pub fn set_node_cost( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_cost(value); - Ok(()) - } - - pub fn set_node_max_flow( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_max_flow_constraint(value) - .map_err(|source| NetworkError::NodeSetAttributeError { - name: node.name().to_string(), - sub_name: node.sub_name().map(|s| s.to_string()), - attribute: "max_flow".to_string(), - source: Box::new(source), - }) - } - - pub fn set_node_min_flow( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_min_flow_constraint(value) - .map_err(|source| NetworkError::NodeSetAttributeError { - name: node.name().to_string(), - sub_name: node.sub_name().map(|s| s.to_string()), - attribute: "min_flow".to_string(), - source: Box::new(source), - }) - } - pub fn set_node_initial_volume( - &mut self, - name: &str, - sub_name: Option<&str>, - initial_volume: StorageInitialVolume, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_initial_volume(initial_volume) - .map_err(|source| NetworkError::NodeSetAttributeError { - name: node.name().to_string(), - sub_name: node.sub_name().map(|s| s.to_string()), - attribute: "initial_volume".to_string(), - source: Box::new(source), - }) - } - - pub fn set_node_max_volume( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_max_volume_constraint(value) - .map_err(|source| NetworkError::NodeSetAttributeError { - name: node.name().to_string(), - sub_name: node.sub_name().map(|s| s.to_string()), - attribute: "max_volume".to_string(), - source: Box::new(source), - }) - } - - pub fn set_node_min_volume( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_min_volume_constraint(value) - .map_err(|source| NetworkError::NodeSetAttributeError { - name: node.name().to_string(), - sub_name: node.sub_name().map(|s| s.to_string()), - attribute: "min_volume".to_string(), - source: Box::new(source), - }) - } - /// Get an [`AggregatedNode`] from its index. pub fn get_aggregated_node(&self, index: &AggregatedNodeIndex) -> Option<&AggregatedNode> { - self.aggregated_nodes.get(index) + self.aggregated_nodes.get(index.0) } /// Get a `AggregatedNode` from a node's name @@ -1545,60 +1168,9 @@ impl Network { self.get_aggregated_node_by_name(name, sub_name).map(|n| n.index()) } - pub fn set_aggregated_node_max_flow( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_aggregated_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_max_flow(value); - Ok(()) - } - - pub fn set_aggregated_node_min_flow( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_aggregated_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_min_flow(value); - Ok(()) - } - - pub fn set_aggregated_node_relationship( - &mut self, - name: &str, - sub_name: Option<&str>, - relationship: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_aggregated_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_relationship(relationship); - Ok(()) - } - /// Get a `&AggregatedStorageNode` from a node's name pub fn get_aggregated_storage_node(&self, index: AggregatedStorageNodeIndex) -> Option<&AggregatedStorageNode> { - self.aggregated_storage_nodes.get(index) + self.aggregated_storage_nodes.get(index.0) } /// Get a `&AggregatedStorageNode` from a node's name @@ -1633,12 +1205,12 @@ impl Network { } /// Get a `VirtualStorageNode` from a node's name - pub fn get_virtual_storage_node(&self, index: &VirtualStorageIndex) -> Option<&VirtualStorage> { - self.virtual_storage_nodes.get(index) + pub fn get_virtual_storage_node(&self, index: &VirtualStorageIndex) -> Option<&VirtualStorageNode> { + self.virtual_storage_nodes.get(index.0) } /// Get a `VirtualStorageNode` from a node's name - pub fn get_virtual_storage_node_by_name(&self, name: &str, sub_name: Option<&str>) -> Option<&VirtualStorage> { + pub fn get_virtual_storage_node_by_name(&self, name: &str, sub_name: Option<&str>) -> Option<&VirtualStorageNode> { self.virtual_storage_nodes .iter() .find(|&n| n.full_name() == (name, sub_name)) @@ -1649,7 +1221,7 @@ impl Network { &mut self, name: &str, sub_name: Option<&str>, - ) -> Option<&mut VirtualStorage> { + ) -> Option<&mut VirtualStorageNode> { self.virtual_storage_nodes .iter_mut() .find(|n| n.full_name() == (name, sub_name)) @@ -1664,57 +1236,6 @@ impl Network { self.get_virtual_storage_node_by_name(name, sub_name).map(|n| n.index()) } - pub fn set_virtual_storage_cost( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_virtual_storage_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_cost(value); - Ok(()) - } - - pub fn set_virtual_storage_max_volume( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_virtual_storage_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_max_volume_constraint(value); - Ok(()) - } - - pub fn set_virtual_storage_min_volume( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_virtual_storage_node_by_name(name, sub_name) - .ok_or(NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - - node.set_min_volume_constraint(value); - Ok(()) - } - /// Get a `Parameter` from a parameter's name pub fn get_parameter(&self, index: ParameterIndex) -> Option<&dyn parameters::Parameter> { self.parameters.get_f64(index) @@ -1767,241 +1288,6 @@ impl Network { .map(RecorderIndex::new) } - /// Add a new Node::Input to the network. - pub fn add_input_node(&mut self, name: &str, sub_name: Option<&str>) -> Result { - // Check for name. - // TODO move this check to `NodeVec` - if self.get_node_by_name(name, sub_name).is_some() { - return Err(NetworkError::NodeAlreadyExists { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - }); - } - - // Now add the node to the network. - let node_index = self.nodes.push_new_input(name, sub_name); - // ... and add it to the resolve order. - self.resolve_order.push(ComponentType::Node(node_index)); - Ok(node_index) - } - - /// Add a new Node::Link to the network. - pub fn add_link_node(&mut self, name: &str, sub_name: Option<&str>) -> Result { - // Check for name. - // TODO move this check to `NodeVec` - if self.get_node_by_name(name, sub_name).is_some() { - return Err(NetworkError::NodeAlreadyExists { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - }); - } - - // Now add the node to the network. - let node_index = self.nodes.push_new_link(name, sub_name); - // ... and add it to the resolve order. - self.resolve_order.push(ComponentType::Node(node_index)); - Ok(node_index) - } - - /// Add a new Node::Link to the network. - pub fn add_output_node(&mut self, name: &str, sub_name: Option<&str>) -> Result { - // Check for name. - // TODO move this check to `NodeVec` - if self.get_node_by_name(name, sub_name).is_some() { - return Err(NetworkError::NodeAlreadyExists { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - }); - } - - // Now add the node to the network. - let node_index = self.nodes.push_new_output(name, sub_name); - // ... and add it to the resolve order. - self.resolve_order.push(ComponentType::Node(node_index)); - Ok(node_index) - } - - /// Add a new Node::Link to the network. - pub fn add_storage_node( - &mut self, - name: &str, - sub_name: Option<&str>, - initial_volume: StorageInitialVolume, - min_volume: Option, - max_volume: Option, - ) -> Result { - // Check for name. - // TODO move this check to `NodeVec` - if self.get_node_by_name(name, sub_name).is_some() { - return Err(NetworkError::NodeAlreadyExists { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - }); - } - - // Now add the node to the network. - let node_index = self - .nodes - .push_new_storage(name, sub_name, initial_volume, min_volume, max_volume); - // ... and add it to the resolve order. - self.resolve_order.push(ComponentType::Node(node_index)); - Ok(node_index) - } - - /// Add a new `aggregated_node::AggregatedNode` to the network. - pub fn add_aggregated_node( - &mut self, - name: &str, - sub_name: Option<&str>, - nodes: &[Vec], - relationship: Option, - ) -> Result { - if self.get_aggregated_node_by_name(name, sub_name).is_some() { - return Err(NetworkError::NodeAlreadyExists { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - }); - } - - let node_index = self.aggregated_nodes.push_new(name, sub_name, nodes, relationship); - Ok(node_index) - } - - /// Add a new `aggregated_storage_node::AggregatedStorageNode` to the network. - pub fn add_aggregated_storage_node( - &mut self, - name: &str, - sub_name: Option<&str>, - nodes: Vec, - ) -> Result { - if self.get_aggregated_storage_node_by_name(name, sub_name).is_some() { - return Err(NetworkError::NodeAlreadyExists { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - }); - } - - let node_index = self.aggregated_storage_nodes.push_new(name, sub_name, nodes); - Ok(node_index) - } - - /// Add a new `VirtualStorage` to the network. - pub fn add_virtual_storage_node( - &mut self, - builder: VirtualStorageBuilder, - ) -> Result { - let vs_node_index = self.virtual_storage_nodes.push_new(builder)?; - - let vs_node = self - .virtual_storage_nodes - .get(&vs_node_index) - .expect("VirtualStorageNode not found; this is a bug and should not be possible."); - - // Link the virtual storage node to the nodes it is including - for node_idx in vs_node.nodes() { - let node = self - .nodes - .get_mut(node_idx) - .ok_or(NetworkError::NodeIndexNotFound { index: *node_idx })?; - - node.add_virtual_storage(vs_node_index) - .map_err(|source| NetworkError::NodeError { - name: node.name().to_string(), - sub_name: node.sub_name().map(|s| s.to_string()), - source: Box::new(source), - })?; - } - - // Add to the resolve order. - self.resolve_order - .push(ComponentType::VirtualStorageNode(vs_node_index)); - - Ok(vs_node_index) - } - - pub fn set_virtual_storage_node_cost( - &mut self, - name: &str, - sub_name: Option<&str>, - value: Option, - ) -> Result<(), NetworkError> { - let node = self - .get_mut_virtual_storage_node_by_name(name, sub_name) - .ok_or_else(|| NetworkError::NodeNotFound { - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - })?; - node.set_cost(value); - Ok(()) - } - - /// Add a [`parameters::GeneralParameter`] to the network - pub fn add_parameter( - &mut self, - parameter: Box>, - ) -> Result, NetworkError> { - let parameter_index = self.parameters.add_general_f64(parameter)?; - - // add it to the general resolve order (simple and constant parameters are resolved separately) - if let ParameterIndex::General(idx) = parameter_index { - self.resolve_order.push(ComponentType::Parameter(idx.into())); - } - - Ok(parameter_index) - } - - /// Add a [`parameters::SimpleParameter`] to the network - pub fn add_simple_parameter( - &mut self, - parameter: Box>, - ) -> Result, NetworkError> { - Ok(self.parameters.add_simple_f64(parameter)?) - } - - /// Add a [`parameters::SimpleParameter`] to the network - pub fn add_simple_index_parameter( - &mut self, - parameter: Box>, - ) -> Result, NetworkError> { - Ok(self.parameters.add_simple_u64(parameter)?) - } - - /// Add a [`parameters::ConstParameter`] to the network - pub fn add_const_parameter( - &mut self, - parameter: Box>, - ) -> Result, NetworkError> { - Ok(self.parameters.add_const_f64(parameter)?) - } - - /// Add a `parameters::IndexParameter` to the network - pub fn add_index_parameter( - &mut self, - parameter: Box>, - ) -> Result, NetworkError> { - let parameter_index = self.parameters.add_general_u64(parameter)?; - // add it to the general resolve order (simple and constant parameters are resolved separately) - if let ParameterIndex::General(idx) = parameter_index { - self.resolve_order.push(ComponentType::Parameter(idx.into())); - } - - Ok(parameter_index) - } - - /// Add a `parameters::MultiValueParameter` to the network - pub fn add_multi_value_parameter( - &mut self, - parameter: Box>, - ) -> Result, NetworkError> { - let parameter_index = self.parameters.add_general_multi(parameter)?; - // add it to the general resolve order (simple and constant parameters are resolved separately) - if let ParameterIndex::General(idx) = parameter_index { - self.resolve_order.push(ComponentType::Parameter(idx.into())); - } - - Ok(parameter_index) - } - /// Add a [`MetricSet`] to the network. pub fn add_metric_set(&mut self, metric_set: MetricSet) -> Result { if self.get_metric_set_by_name(metric_set.name()).is_ok() { @@ -2034,70 +1320,6 @@ impl Network { } } - /// Add a `recorders::Recorder` to the network - pub fn add_recorder(&mut self, recorder: Box) -> Result { - // TODO reinstate this check - // if let Ok(idx) = self.get_recorder_by_name(&recorder.meta().name) { - // return Err(PywrError::RecorderNameAlreadyExists( - // recorder.meta().name.to_string(), - // idx, - // )); - // } - - let recorder_index = RecorderIndex::new(self.recorders.len()); - self.recorders.push(recorder); - Ok(recorder_index) - } - - /// Connect two nodes together - pub fn connect_nodes( - &mut self, - from_node_index: NodeIndex, - to_node_index: NodeIndex, - ) -> Result { - // The network can get in a bad state here if the edge is added to the `from_node` - // successfully, but fails on the `to_node`. - // Suggest to do a check before attempting to add. - let from_node = self - .nodes - .get_mut(&from_node_index) - .ok_or(NetworkError::NodeIndexNotFound { index: from_node_index })?; - - // Self connections are not allowed. - if from_node_index == to_node_index { - return Err(NetworkError::NodeConnectToSelf { - name: from_node.name().to_string(), - sub_name: from_node.sub_name().map(|s| s.to_string()), - }); - } - - // Next edge index - let edge_index = self.edges.push(from_node_index, to_node_index); - - from_node - .add_outgoing_edge(edge_index) - .map_err(|source| NetworkError::NodeError { - name: from_node.name().to_string(), - sub_name: from_node.sub_name().map(|s| s.to_string()), - source: Box::new(source), - })?; - - let to_node = self - .nodes - .get_mut(&to_node_index) - .ok_or(NetworkError::NodeIndexNotFound { index: from_node_index })?; - - to_node - .add_incoming_edge(edge_index) - .map_err(|source| NetworkError::NodeError { - name: to_node.name().to_string(), - sub_name: to_node.sub_name().map(|s| s.to_string()), - source: Box::new(source), - })?; - - Ok(edge_index) - } - /// Set the variable values on the parameter `parameter_index`. /// /// This will update the internal state of the parameter with the new values for all scenarios. @@ -2333,15 +1555,576 @@ impl Network { } } +/// Errors returned by [`NetworkBuilder`] +#[derive(Debug, Error)] +pub enum NetworkBuilderError {} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct UnresolvedEdge { + from: UnresolvedNode, + to: UnresolvedNode, +} + +impl Display for UnresolvedEdge { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} -> {}", self.from, self.to) + } +} + +/// An index to a regular node type. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash)] +pub struct NodeIndex(usize); + +impl Deref for NodeIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for NodeIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash)] +pub struct EdgeIndex(usize); + +impl Deref for EdgeIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for EdgeIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub struct AggregatedNodeIndex(usize); + +impl Deref for AggregatedNodeIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for AggregatedNodeIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub struct AggregatedStorageNodeIndex(usize); + +impl Deref for AggregatedStorageNodeIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for AggregatedStorageNodeIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash)] +pub struct VirtualStorageIndex(usize); + +impl Deref for VirtualStorageIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for VirtualStorageIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct RecorderIndex(usize); + +impl RecorderIndex { + pub fn new(idx: usize) -> Self { + Self(idx) + } +} + +impl Deref for RecorderIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for RecorderIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A helper struct for building a network. This struct contains look-ups and references +/// for resolving names and other unresolved references during the build process. +#[derive(Default)] +pub struct ResolutionMaps { + pub nodes: HashMap, + /// The edges incoming to each node. + pub incoming_edges: HashMap>, + /// The edges outgoing from each node. + pub outgoing_edges: HashMap>, + pub parameters_f64: HashMap>, + pub parameters_u64: HashMap>, + pub parameters_multi: HashMap>, + /// The index associated with the unresolved virtual storage node names. + pub virtual_storage_node: HashMap, + /// The virtual storage nodes associated with each node index. This is used to resolve the + /// virtual storage nodes after the regular nodes have been resolved. + pub virtual_storage_associated_nodes: HashMap>, + /// The index associated with the unresolved aggregated node names. + pub aggregated_nodes: HashMap, + /// The index associated with the unresolved aggregated storage node names. + pub aggregated_storage_nodes: HashMap, + /// The index associated with each unresolved edge. + pub edges: HashMap, + /// Scenario group index + pub scenario_group: HashMap, + /// Inter-network transfer indices + pub inter_network_transfers: HashMap, +} + +#[derive(Debug, Error)] +pub enum NetworkBuildError { + #[error("Duplicate node names found: {name}")] + DuplicateNodeName { name: UnresolvedNode }, + #[error("Duplicate parameter names found: {name}")] + DuplicateParameterName { name: ParameterName }, + #[error("Duplicate aggregated node names found: {name}")] + DuplicateAggregatedNodeName { name: UnresolvedNode }, + #[error("Node `{name}` not found while resolving edge: {edge}")] + NodeNotFoundForEdge { + name: UnresolvedNode, + edge: Box, + }, + #[error("Node `{name}` not found while resolving virtual storage: {virtual_storage}")] + NodeNotFoundForVirtualStorage { + name: UnresolvedNode, + virtual_storage: UnresolvedNode, + }, + #[error("Error building node `{name}`: {source}")] + NodeBuilderError { + name: UnresolvedNode, + #[source] + source: Box, + }, + #[error("Error building aggregated node `{name}`: {source}")] + AggregatedNodeBuilderError { + name: UnresolvedNode, + #[source] + source: Box, + }, + #[error("Error building aggregated storage node `{name}`: {source}")] + AggregatedStorageNodeBuilderError { + name: UnresolvedNode, + #[source] + source: Box, + }, + #[error("Error building virtual storage node `{name}`: {source}")] + VirtualStorageNodeBuilderError { + name: UnresolvedNode, + #[source] + source: Box, + }, + #[error("Error building recorder `{name}`: {source}")] + RecorderBuilderError { + name: String, + #[source] + source: Box, + }, + #[error("Could not load all parameters due to circulate reference(s).")] + CircularParameterReference, + #[error("Parameter collection build error: {0}")] + ParameterCollectionBuildError(#[from] Box), +} + +/// A builder for [`Network`]. +/// +/// This is the only way to construct a [`Network`] instance. +#[derive(Default)] +pub struct NetworkBuilder { + nodes: Vec, + virtual_storage_nodes: Vec, + edges: Vec, + parameters: ParameterCollectionBuilder, + aggregated_nodes: Vec, + aggregated_storage_nodes: Vec, + recorders: Vec>, +} + +impl NetworkBuilder { + /// Add an input node to the network builder. + pub fn node(&mut self, node: NodeBuilder) -> &mut Self { + self.nodes.push(node); + self + } + + /// Get a reference to an existing node builder by name and sub-name, if it exists. + pub fn node_builder(&mut self, name: &UnresolvedNode) -> Option<&mut NodeBuilder> { + self.nodes.iter_mut().find(|n| n.name() == name) + } + + /// Add a virtual storage node to the network builder. + pub fn virtual_storage_node(&mut self, vs_node: VirtualStorageNodeBuilder) -> &mut Self { + self.virtual_storage_nodes.push(vs_node); + self + } + + /// Connect two nodes together + pub fn connect( + &mut self, + from_name: &str, + from_sub_name: Option<&str>, + to_name: &str, + to_sub_name: Option<&str>, + ) -> &mut Self { + self.edges.push(UnresolvedEdge { + from: UnresolvedNode::new(from_name, from_sub_name), + to: UnresolvedNode::new(to_name, to_sub_name), + }); + self + } + + /// Add an aggregated node to the network builder. + pub fn agg_node(&mut self, agg_node: AggregatedNodeBuilder) -> &mut Self { + self.aggregated_nodes.push(agg_node); + self + } + + pub fn agg_storage_node(&mut self, agg_storage_node: AggregatedStorageNodeBuilder) -> &mut Self { + self.aggregated_storage_nodes.push(agg_storage_node); + self + } + + pub fn parameters(&mut self) -> &mut ParameterCollectionBuilder { + &mut self.parameters + } + + pub fn recorder(&mut self, recorder: Box) -> &mut Self { + self.recorders.push(recorder); + self + } + + fn node_index_map(&self) -> Result, NetworkBuildError> { + // Build the NodeIndex map checking for any duplicate node names. + let mut node_index_map: HashMap = HashMap::with_capacity(self.nodes.len()); + + for (i, nb) in self.nodes.iter().enumerate() { + let unresolved_node = nb.name(); + if node_index_map.contains_key(unresolved_node) { + return Err(NetworkBuildError::DuplicateNodeName { + name: unresolved_node.clone(), + }); + } + node_index_map.insert(unresolved_node.clone(), NodeIndex(i)); + } + + Ok(node_index_map) + } + + fn node_edge_maps( + &self, + node_indices: &HashMap, + ) -> Result<(HashMap>, HashMap>), NetworkBuildError> { + let mut incoming: HashMap> = HashMap::with_capacity(self.nodes.len()); + let mut outgoing: HashMap> = HashMap::with_capacity(self.nodes.len()); + + for (i, edge) in self.edges.iter().enumerate() { + let ei = EdgeIndex(i); + + let from_node_index = + node_indices + .get(&edge.from) + .ok_or_else(|| NetworkBuildError::NodeNotFoundForEdge { + name: edge.from.clone(), + edge: Box::new(edge.clone()), + })?; + + let to_node_index = node_indices + .get(&edge.to) + .ok_or_else(|| NetworkBuildError::NodeNotFoundForEdge { + name: edge.to.clone(), + edge: Box::new(edge.clone()), + })?; + + outgoing.entry(*from_node_index).or_default().push(ei); + incoming.entry(*to_node_index).or_default().push(ei); + } + + Ok((incoming, outgoing)) + } + + fn aggregated_node_map(&self) -> Result, NetworkBuildError> { + let mut agg_node_index_map: HashMap = + HashMap::with_capacity(self.nodes.len()); + for (i, nb) in self.aggregated_nodes.iter().enumerate() { + let unresolved_node = nb.name(); + if agg_node_index_map.contains_key(unresolved_node) { + return Err(NetworkBuildError::DuplicateAggregatedNodeName { + name: unresolved_node.clone(), + }); + } + + agg_node_index_map.insert(unresolved_node.clone(), AggregatedNodeIndex(i)); + } + + Ok(agg_node_index_map) + } + + fn aggregated_storage_node_map( + &self, + ) -> Result, NetworkBuildError> { + let mut agg_node_index_map: HashMap = + HashMap::with_capacity(self.nodes.len()); + for (i, nb) in self.aggregated_storage_nodes.iter().enumerate() { + let unresolved_node = nb.name(); + if agg_node_index_map.contains_key(unresolved_node) { + return Err(NetworkBuildError::DuplicateAggregatedNodeName { + name: unresolved_node.clone(), + }); + } + + agg_node_index_map.insert(unresolved_node.clone(), AggregatedStorageNodeIndex(i)); + } + + Ok(agg_node_index_map) + } + + fn virtual_storage_node_map(&self) -> Result, NetworkBuildError> { + let mut vs_node_index_map: HashMap = + HashMap::with_capacity(self.nodes.len()); + for (i, nb) in self.virtual_storage_nodes.iter().enumerate() { + let unresolved_node = nb.name(); + if vs_node_index_map.contains_key(unresolved_node) { + return Err(NetworkBuildError::DuplicateAggregatedNodeName { + name: unresolved_node.clone(), + }); + } + + vs_node_index_map.insert(unresolved_node.clone(), VirtualStorageIndex(i)); + } + + Ok(vs_node_index_map) + } + + /// Compute a map of [`NodeIndex`] to the associated [`VirtualStorageNode`]s. This is used + /// to build the backward reference from nodes to VS nodes. + fn node_associated_vs_nodes( + &self, + node_indices: &HashMap, + ) -> Result>, NetworkBuildError> { + let mut vs_associated_nodes: HashMap> = + HashMap::with_capacity(self.nodes.len()); + for (i, vs) in self.virtual_storage_nodes.iter().enumerate() { + let vi = VirtualStorageIndex(i); + for node in vs.nodes() { + let index = node_indices + .get(node) + .ok_or_else(|| NetworkBuildError::NodeNotFoundForVirtualStorage { + name: node.clone(), + virtual_storage: vs.name().clone(), + })?; + + vs_associated_nodes.entry(*index).or_default().push(vi); + } + } + + Ok(vs_associated_nodes) + } + + fn build_resolution_map( + &self, + scenario_group_map: &HashMap, + inter_network_transfer_map: &HashMap, + ) -> Result { + let nodes = self.node_index_map()?; + let (incoming_edges, outgoing_edges) = self.node_edge_maps(&nodes)?; + let aggregated_nodes = self.aggregated_node_map()?; + let aggregated_storage_nodes = self.aggregated_storage_node_map()?; + let virtual_storage_node = self.virtual_storage_node_map()?; + let virtual_storage_associated_nodes = self.node_associated_vs_nodes(&nodes)?; + + Ok(ResolutionMaps { + nodes, + incoming_edges, + outgoing_edges, + // Parameter maps start empty and are iteratively populated. + parameters_f64: Default::default(), + parameters_u64: Default::default(), + parameters_multi: Default::default(), + virtual_storage_node, + virtual_storage_associated_nodes, + aggregated_nodes, + aggregated_storage_nodes, + edges: Default::default(), + scenario_group: scenario_group_map.clone(), // TODO can this clone be removed; needs tying to the lifetime of the reference in `build`. + inter_network_transfers: inter_network_transfer_map.clone(), + }) + } + + /// Build the network. + pub fn build( + self, + scenario_group_map: &HashMap, + inter_network_transfer_map: &HashMap, + ) -> Result<(Network, ResolutionMaps), NetworkBuildError> { + // Resolution map + let mut resolution_map = self.build_resolution_map(scenario_group_map, inter_network_transfer_map)?; + + // Iterative load the parameters + + let parameters = self + .parameters + .build(&mut resolution_map) + .map_err(|source| NetworkBuildError::ParameterCollectionBuildError(Box::new(source)))?; + + // Construct all the nodes + let mut nodes = Vec::with_capacity(self.nodes.len()); + for node_builder in self.nodes.iter() { + let node = node_builder + .build(&resolution_map) + .map_err(|source| NetworkBuildError::NodeBuilderError { + name: node_builder.name().clone(), + source: Box::new(source), + })?; + + nodes.push(node); + } + + // Construct all the aggregated nodes + let mut aggregated_nodes = Vec::with_capacity(self.aggregated_nodes.len()); + for agg_node_builder in self.aggregated_nodes.iter() { + let agg_node = agg_node_builder.build(&resolution_map).map_err(|source| { + NetworkBuildError::AggregatedNodeBuilderError { + name: agg_node_builder.name().clone(), + source: Box::new(source), + } + })?; + + aggregated_nodes.push(agg_node); + } + + // Construct all the aggregated storage nodes + let mut aggregated_storage_nodes = Vec::with_capacity(self.aggregated_storage_nodes.len()); + for agg_storage_node_builder in self.aggregated_storage_nodes.iter() { + let agg_node = agg_storage_node_builder.build(&resolution_map).map_err(|source| { + NetworkBuildError::AggregatedStorageNodeBuilderError { + name: agg_storage_node_builder.name().clone(), + source: Box::new(source), + } + })?; + + aggregated_storage_nodes.push(agg_node); + } + + // Construct all virtual storage nodes + let mut virtual_storage_nodes = Vec::with_capacity(self.virtual_storage_nodes.len()); + for vs_node_builder in self.virtual_storage_nodes.iter() { + let vs_node = vs_node_builder.build(&resolution_map).map_err(|source| { + NetworkBuildError::VirtualStorageNodeBuilderError { + name: vs_node_builder.name().clone(), + source: Box::new(source), + } + })?; + + virtual_storage_nodes.push(vs_node); + } + + // Construct all the edges + let mut edges = Vec::with_capacity(self.edges.len()); + for (i, unresolved_edge) in self.edges.iter().enumerate() { + let index = EdgeIndex(i); + let from_node_index = resolution_map + .nodes + .get(&unresolved_edge.from) + .copied() + .ok_or_else(|| NetworkBuildError::NodeNotFoundForEdge { + name: unresolved_edge.from.clone(), + edge: Box::new(unresolved_edge.clone()), + })?; + + let to_node_index = resolution_map.nodes.get(&unresolved_edge.to).copied().ok_or_else(|| { + NetworkBuildError::NodeNotFoundForEdge { + name: unresolved_edge.to.clone(), + edge: Box::new(unresolved_edge.clone()), + } + })?; + + let edge = Edge { + index, + from_node_index, + to_node_index, + }; + + edges.push(edge); + } + + // Construct all recorders + let mut recorders = Vec::with_capacity(self.recorders.len()); + for recorder_builder in self.recorders.into_iter() { + let name = recorder_builder.name().to_string(); + let recorder = + recorder_builder + .build(&resolution_map) + .map_err(|source| NetworkBuildError::RecorderBuilderError { + name, + source: Box::new(source), + })?; + + recorders.push(recorder); + } + + let network = Network { + nodes, + edges, + aggregated_nodes, + aggregated_storage_nodes: Default::default(), + virtual_storage_nodes, + parameters, + metric_sets: vec![], + recorders, + }; + + Ok((network, resolution_map)) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::metric::MetricF64; - use crate::network::Network; - use crate::parameters::{ActivationFunction, ControlCurveInterpolatedParameter, Parameter}; - use crate::recorders::AssertionF64Recorder; + use crate::metric::UnresolvedMetricF64; + use crate::models::ModelBuilder; + use crate::parameters::{ActivationFunction, ControlCurveInterpolatedParameterBuilder}; + use crate::recorders::AssertionF64RecorderBuilder; use crate::solvers::{ClpSolver, ClpSolverSettings}; - use crate::test_utils::{run_all_solvers, simple_model, simple_storage_model}; + use crate::test_utils::{ + default_domain_builder, run_all_solvers, simple_model, simple_storage_model, simple_storage_network, + }; use float_cmp::assert_approx_eq; use ndarray::{Array, Array2}; use std::default::Default; @@ -2349,25 +2132,28 @@ mod tests { #[test] fn test_simple_network() { - let mut network = Network::default(); + let mut builder = NetworkBuilder::default(); - let input_node = network.add_input_node("input", None).unwrap(); - let link_node = network.add_link_node("link", None).unwrap(); - let output_node = network.add_output_node("output", None).unwrap(); + builder + .node(NodeBuilder::input("input")) + .node(NodeBuilder::link("link")) + .node(NodeBuilder::output("output")); - assert_eq!(*input_node.deref(), 0); - assert_eq!(*link_node.deref(), 1); - assert_eq!(*output_node.deref(), 2); + builder.connect("input", None, "link", None); + builder.connect("link", None, "output", None); - let edge = network.connect_nodes(input_node, link_node).unwrap(); - assert_eq!(*edge.deref(), 0); - let edge = network.connect_nodes(link_node, output_node).unwrap(); - assert_eq!(*edge.deref(), 1); + let scenario_group_map = HashMap::new(); + let (network, _) = builder.build(&scenario_group_map, &HashMap::new()).unwrap(); // Now assert the internal structure is as expected. let input_node = network.get_node_by_name("input", None).unwrap(); let link_node = network.get_node_by_name("link", None).unwrap(); let output_node = network.get_node_by_name("output", None).unwrap(); + + assert_eq!(*input_node.index().deref(), 0); + assert_eq!(*link_node.index().deref(), 1); + assert_eq!(*output_node.index().deref(), 2); + assert_eq!(input_node.get_outgoing_edges().unwrap().len(), 1); assert_eq!(link_node.get_incoming_edges().unwrap().len(), 1); assert_eq!(link_node.get_outgoing_edges().unwrap().len(), 1); @@ -2377,64 +2163,79 @@ mod tests { #[test] /// Test the duplicate node names are not permitted. fn test_duplicate_node_name() { - let mut network = Network::default(); + let mut builder = NetworkBuilder::default(); + let scenario_group_map = HashMap::new(); - network.add_input_node("my-node", None).unwrap(); - // Second add with the same name - assert!(matches!( - network.add_input_node("my-node", None), - Err(NetworkError::NodeAlreadyExists { name, .. }) if name == "my-node")); + builder + .node(NodeBuilder::input("my-node")) + .node(NodeBuilder::link("my-node")); - network.add_input_node("my-node", Some("a")).unwrap(); - // Second add with the same name - assert!(matches!( - network.add_input_node("my-node", Some("a")), - Err(NetworkError::NodeAlreadyExists { name, .. }) if name == "my-node")); + let result = builder.build(&scenario_group_map, &HashMap::new()); + assert!( + matches!(result, Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" + ) + ); - assert!(matches!( - network.add_link_node("my-node", None), - Err(NetworkError::NodeAlreadyExists { name, .. }) if name == "my-node")); + let mut builder = NetworkBuilder::default(); + builder + .node(NodeBuilder::input("my-node")) + .node(NodeBuilder::input("my-node")); - assert!(matches!( - network.add_output_node("my-node", None), - Err(NetworkError::NodeAlreadyExists { name, .. }) if name == "my-node")); + assert!( + matches!(builder.build(&scenario_group_map, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" + ) + ); - assert!(matches!( - network.add_storage_node( - "my-node", - None, - StorageInitialVolume::Absolute(10.0), - None, - Some(10.0.into()) - ), - Err(NetworkError::NodeAlreadyExists { name, .. }) if name == "my-node")); + let mut builder = NetworkBuilder::default(); + builder + .node(NodeBuilder::input("my-node")) + .node(NodeBuilder::link("my-other-node")) + .node(NodeBuilder::output("my-node")); + assert!( + matches!(builder.build(&scenario_group_map, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" + ) + ); + // Second add with the same name + let mut n1 = NodeBuilder::input("my-node"); + n1.sub_name("sub1"); + let mut n2 = NodeBuilder::input("my-node"); + n2.sub_name("sub1"); + + let mut builder = NetworkBuilder::default(); + builder.node(n1).node(n2); + assert!( + matches!(builder.build(&scenario_group_map, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node[sub1]" + ) + ); } #[test] /// Test adding a constant parameter to a network. fn test_constant_parameter() { - let mut network = Network::default(); - let _node_index = network.add_input_node("input", None).unwrap(); + let mut builder = NetworkBuilder::default(); + let scenario_group_map = HashMap::new(); - let input_max_flow = parameters::ConstantParameter::new("my-constant".into(), 10.0); - let parameter = network.add_const_parameter(Box::new(input_max_flow)).unwrap(); + let mut input_node_builder = NodeBuilder::input("input"); + // Add the reference to the constant parameter we have not yet added. + input_node_builder.max_flow(UnresolvedMetricF64::new_parameter_before("my-constant")); + builder.node(input_node_builder); - // assign the new parameter to one of the nodes. - let node = network.get_mut_node_by_name("input", None).unwrap(); - node.set_max_flow_constraint(Some(parameter.into_metric_f64_before())) - .unwrap(); + let output_node_builder = NodeBuilder::output("output"); + builder.node(output_node_builder); - // Try to assign a constraint not defined for particular node type - assert!(matches!( - node.set_max_volume_constraint(Some(10.0.into())), - Err(NodeError::StorageConstraintsUndefined) - )); + builder.connect("input", None, "output", None); + + let input_max_flow_builder = parameters::ConstantParameterBuilder::new("my-constant".into(), 10.0); + builder.parameters().f64(Box::new(input_max_flow_builder)); + + builder.build(&scenario_group_map, &HashMap::new()).unwrap(); } #[test] fn test_step() { const NUM_SCENARIOS: usize = 2; - let model = simple_model(NUM_SCENARIOS, None); + + let model = simple_model(NUM_SCENARIOS, None).build().unwrap(); let mut timings = NetworkTimings::new_without_component_timings(); @@ -2447,6 +2248,7 @@ mod tests { for j in 0..NUM_SCENARIOS { let state_j = state.network_state().states.get(j).unwrap(); + let output_inflow = state_j .get_network_state() .get_node_in_flow(&output_node.index()) @@ -2459,32 +2261,43 @@ mod tests { #[test] /// Test running a simple model fn test_run() { - let mut model = simple_model(10, None); + let mut model_builder = simple_model(10, None); + + let network = model_builder.network_builder(); // Set-up assertion for "input" node - let idx = model.network().get_node_by_name("input", None).unwrap().index(); let expected = Array::from_shape_fn((366, 10), |(i, j)| (1.0 + i as f64 + j as f64).min(12.0)); - let recorder = - AssertionF64Recorder::new("input-flow", MetricF64::NodeOutFlow(idx), expected.clone(), None, None); - model.network_mut().add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "input-flow", + UnresolvedMetricF64::NodeOutFlow("input".into()), + expected.clone(), + ); + network.recorder(Box::new(recorder)); - let idx = model.network().get_node_by_name("link", None).unwrap().index(); - let recorder = - AssertionF64Recorder::new("link-flow", MetricF64::NodeOutFlow(idx), expected.clone(), None, None); - model.network_mut().add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "link-flow", + UnresolvedMetricF64::NodeOutFlow("link".into()), + expected.clone(), + ); + network.recorder(Box::new(recorder)); - let idx = model.network().get_node_by_name("output", None).unwrap().index(); - let recorder = AssertionF64Recorder::new("output-flow", MetricF64::NodeInFlow(idx), expected, None, None); - model.network_mut().add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "output-flow", + UnresolvedMetricF64::NodeInFlow("output".into()), + expected, + ); + network.recorder(Box::new(recorder)); - let idx = model - .network() - .get_parameter_index_by_name(&"total-demand".into()) - .unwrap(); let expected = Array2::from_elem((366, 10), 12.0); - let recorder = AssertionF64Recorder::new("total-demand", idx.into_metric_f64_before(), expected, None, None); - model.network_mut().add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "total-demand", + UnresolvedMetricF64::new_parameter_before("total-demand"), + expected, + ); + network.recorder(Box::new(recorder)); + + let model = model_builder.build().unwrap(); // Test all solvers run_all_solvers(&model, &[], &[], &[]); @@ -2492,23 +2305,27 @@ mod tests { #[test] fn test_run_storage() { - let mut model = simple_storage_model(); - - let network = model.network_mut(); - - let idx = network.get_node_by_name("output", None).unwrap().index(); + let mut network = simple_storage_network(); let expected = Array2::from_shape_fn((15, 10), |(i, _j)| if i < 10 { 10.0 } else { 0.0 }); - let recorder = AssertionF64Recorder::new("output-flow", MetricF64::NodeInFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); - - let idx = network.get_node_by_name("reservoir", None).unwrap().index(); + let recorder = AssertionF64RecorderBuilder::new( + "output-flow", + UnresolvedMetricF64::NodeInFlow("output".into()), + expected, + ); + network.recorder(Box::new(recorder)); let expected = Array2::from_shape_fn((15, 10), |(i, _j)| (90.0 - 10.0 * i as f64).max(0.0)); - let recorder = AssertionF64Recorder::new("reservoir-volume", MetricF64::NodeVolume(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "reservoir-volume", + UnresolvedMetricF64::NodeVolume("reservoir".into()), + expected, + ); + network.recorder(Box::new(recorder)); + + let model = ModelBuilder::new(default_domain_builder(), network).build().unwrap(); // Test all solvers run_all_solvers(&model, &[], &[], &[]); @@ -2520,34 +2337,37 @@ mod tests { /// parameter may required a value for the initial time-step based on the initial volume. #[test] fn test_storage_proportional_volume() { - let mut model = simple_storage_model(); - let network = model.network_mut(); - let idx = network.get_node_by_name("reservoir", None).unwrap().index(); + let mut model_builder = simple_storage_model(); + let network = model_builder.network_builder(); // These are the expected values for the proportional volume at the end of the time-step let expected = Array2::from_shape_fn((15, 10), |(i, _j)| (90.0 - 10.0 * i as f64).max(0.0) / 100.0); - let recorder = AssertionF64Recorder::new( + let recorder = AssertionF64RecorderBuilder::new( "reservoir-proportion-volume", - MetricF64::NodeProportionalVolume(idx), + UnresolvedMetricF64::NodeProportionalVolume("reservoir".into()), expected, - None, - None, ); - network.add_recorder(Box::new(recorder)).unwrap(); + network.recorder(Box::new(recorder)); // Set-up a control curve that uses the proportional volume // This should be use the initial proportion (100%) on the first time-step, and then the previous day's end value - let cc = ControlCurveInterpolatedParameter::new( + let mut cc = ControlCurveInterpolatedParameterBuilder::new( "interp".into(), - MetricF64::NodeProportionalVolume(idx), - vec![], - vec![100.0.into(), 0.0.into()], + UnresolvedMetricF64::NodeProportionalVolume("reservoir".into()), ); - let p_idx = network.add_parameter(Box::new(cc)).unwrap(); + cc.value(100.0.into()).value(0.0.into()); + network.parameters().f64(Box::new(cc)); + let expected = Array2::from_shape_fn((15, 10), |(i, _j)| (100.0 - 10.0 * i as f64).max(0.0)); - let recorder = AssertionF64Recorder::new("reservoir-cc", p_idx.into_metric_f64_before(), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "reservoir-cc", + UnresolvedMetricF64::new_parameter_before("interp"), + expected, + ); + network.recorder(Box::new(recorder)); + + let model = model_builder.build().unwrap(); // Test all solvers run_all_solvers(&model, &[], &[], &[]); @@ -2556,41 +2376,44 @@ mod tests { #[test] /// Test the variable API fn test_variable_api() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); let variable = ActivationFunction::Unit { min: 0.0, max: 10.0 }; - let input_max_flow = parameters::ConstantParameter::new("my-constant".into(), 10.0); + let my_constant: ParameterName = "my-constant".into(); + let input_max_flow = parameters::ConstantParameterBuilder::new(my_constant.clone(), 10.0); - assert!(input_max_flow.can_be_f64_variable()); - - let input_max_flow_idx = model - .network_mut() - .add_const_parameter(Box::new(input_max_flow)) - .unwrap(); + model_builder + .network_builder() + .parameters() + .f64(Box::new(input_max_flow)); // assign the new parameter to one of the nodes. - let node = model.network_mut().get_mut_node_by_name("input", None).unwrap(); - node.set_max_flow_constraint(Some(input_max_flow_idx.into_metric_f64_before())) - .unwrap(); + let input_node_ref = "input".into(); + let node_builder = model_builder.network_builder().node_builder(&input_node_ref).unwrap(); + node_builder.max_flow(UnresolvedMetricF64::new_parameter_before("my-constant")); + + let model = model_builder.build().unwrap(); let mut state = model.setup::(&ClpSolverSettings::default()).unwrap(); + let input_max_flow_idx = model.network().get_parameter_index_by_name(&my_constant).unwrap(); + // Initially the variable value should be unset let variable_values = model - .network_mut() + .network() .get_f64_parameter_variable_values(input_max_flow_idx, state.network_state()) .unwrap(); assert_eq!(variable_values, vec![None]); // Update the variable values model - .network_mut() + .network() .set_f64_parameter_variable_values(input_max_flow_idx, &[5.0], &variable, state.network_state_mut()) .unwrap(); // After update the variable value should match what was set let variable_values = model - .network_mut() + .network() .get_f64_parameter_variable_values(input_max_flow_idx, state.network_state()) .unwrap(); diff --git a/pywr-core/src/node.rs b/pywr-core/src/node.rs index 654d631d..c2f15073 100644 --- a/pywr-core/src/node.rs +++ b/pywr-core/src/node.rs @@ -1,30 +1,13 @@ -use crate::edge::EdgeIndex; -use crate::metric::{ConstantMetricF64Error, MetricF64, MetricF64Error, SimpleMetricF64, SimpleMetricF64Error}; -use crate::network::Network; +use crate::metric::{ + ConstantMetricF64Error, MetricF64, MetricF64Error, MetricF64ResolutionError, SimpleMetricF64, SimpleMetricF64Error, + UnresolvedMetricF64, +}; +use crate::network::{EdgeIndex, Network, NodeIndex, ResolutionMaps, VirtualStorageIndex}; use crate::state::{ConstParameterValues, NetworkStateError, NodeState, SimpleParameterValues, State, StateError}; use crate::timestep::Timestep; -use crate::virtual_storage::VirtualStorageIndex; use std::fmt::{Display, Formatter}; -use std::ops::{Deref, DerefMut}; use thiserror::Error; -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash)] -pub struct NodeIndex(usize); - -impl Deref for NodeIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for NodeIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - #[derive(Debug, Error)] pub enum NodeError { #[error("Flow constraints are undefined for this node type")] @@ -57,81 +40,416 @@ pub enum NodeError { NodeIndexNotFound(NodeIndex), } -#[derive(Debug, PartialEq)] -pub enum Node { - Input(InputNode), - Output(OutputNode), - Link(LinkNode), - Storage(StorageNode), +#[derive(Debug, Error)] +pub enum NodeBuilderError { + #[error("Index not found in resolution map.")] + IndexNotFound, + #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] + ResolveMetricF64Error { + attr: String, + #[source] + source: MetricF64ResolutionError, + }, + #[error("Could not simplify f64 metric for `{attr}`: {source}")] + CouldNotSimplifyMetricF64 { + attr: String, + #[source] + source: MetricF64Error, + }, + #[error("Initial volume not defined.")] + InitialVolumeNotDefined, + #[error("Node type `{node_type}` must have at-least one outgoing edge.")] + NoOutgoingEdges { node_type: NodeType }, + #[error("Node type `{node_type}` must have at-least one incoming edge.")] + NoIncomingEdges { node_type: NodeType }, + #[error("Node type `{node_type}` must not have any outgoing edges ({num} found).")] + UnexpectedOutgoingEdges { node_type: NodeType, num: usize }, + #[error("Node type `{node_type}` must not have any incoming edges ({num} found).")] + UnexpectedIncomingEdges { node_type: NodeType, num: usize }, } -#[derive(Eq, PartialEq)] -pub enum NodeType { - Input, - Output, - Link, - Storage, +pub struct NodeBuilder { + name: UnresolvedNode, + node_type: NodeType, + cost: Option, + cost_agg_func: Option, + min_flow: Option, + max_flow: Option, + initial_volume: Option, + max_volume: Option, + min_volume: Option, } -#[derive(Default)] -pub struct NodeVec { - nodes: Vec, -} +impl NodeBuilder { + /// The name of node that will be built. + pub fn name(&self) -> &UnresolvedNode { + &self.name + } + pub fn input(name: &str) -> Self { + Self::new(name, NodeType::Input) + } -impl Deref for NodeVec { - type Target = Vec; + pub fn output(name: &str) -> Self { + Self::new(name, NodeType::Output) + } - fn deref(&self) -> &Self::Target { - &self.nodes + pub fn link(name: &str) -> Self { + Self::new(name, NodeType::Link) } -} -impl DerefMut for NodeVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nodes + pub fn storage(name: &str) -> Self { + Self::new(name, NodeType::Storage) + } + pub fn new(name: &str, node_type: NodeType) -> Self { + let meta = UnresolvedNode { + name: name.to_string(), + sub_name: None, + }; + Self { + name: meta, + node_type, + cost: None, + cost_agg_func: None, + max_flow: None, + min_flow: None, + initial_volume: None, + min_volume: None, + max_volume: None, + } } -} -impl NodeVec { - pub fn get(&self, index: &NodeIndex) -> Option<&Node> { - self.nodes.get(index.0) + pub fn sub_name(&mut self, sub_name: &str) -> &mut Self { + self.name.sub_name = Some(sub_name.to_string()); + self } - pub fn get_mut(&mut self, index: &NodeIndex) -> Option<&mut Node> { - self.nodes.get_mut(index.0) + pub fn cost(&mut self, cost: UnresolvedMetricF64) -> &mut Self { + self.cost = Some(cost); + self } - pub fn push_new_input(&mut self, name: &str, sub_name: Option<&str>) -> NodeIndex { - let node_index = NodeIndex(self.nodes.len()); - let node = Node::new_input(node_index, name, sub_name); - self.nodes.push(node); - node_index + pub fn cost_agg_func(&mut self, cost_agg_func: CostAggFunc) -> &mut Self { + self.cost_agg_func = Some(cost_agg_func); + self } - pub fn push_new_link(&mut self, name: &str, sub_name: Option<&str>) -> NodeIndex { - let node_index = NodeIndex(self.nodes.len()); - let node = Node::new_link(node_index, name, sub_name); - self.nodes.push(node); - node_index + + pub fn min_flow(&mut self, min_flow: UnresolvedMetricF64) -> &mut Self { + self.min_flow = Some(min_flow); + self + } + + pub fn max_flow(&mut self, max_flow: UnresolvedMetricF64) -> &mut Self { + self.max_flow = Some(max_flow); + self } - pub fn push_new_output(&mut self, name: &str, sub_name: Option<&str>) -> NodeIndex { - let node_index = NodeIndex(self.nodes.len()); - let node = Node::new_output(node_index, name, sub_name); - self.nodes.push(node); - node_index + + pub fn initial_volume(&mut self, initial_volume: UnresolvedStorageInitialVolume) -> &mut Self { + self.initial_volume = Some(initial_volume); + self + } + + pub fn max_volume(&mut self, max_volume: UnresolvedMetricF64) -> &mut Self { + self.max_volume = Some(max_volume); + self + } + + pub fn min_volume(&mut self, min_volume: UnresolvedMetricF64) -> &mut Self { + self.min_volume = Some(min_volume); + self + } + + fn build_cost(&self, resolution_maps: &ResolutionMaps) -> Result { + let index = resolution_maps + .nodes + .get(&self.name) + .ok_or(NodeBuilderError::IndexNotFound)?; + + let local = match &self.cost { + Some(cost) => cost + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "cost".to_string(), + source, + })?, + None => 0.0.into(), + }; + + let virtual_storage_nodes = resolution_maps + .virtual_storage_associated_nodes + .get(index) + .cloned() + .unwrap_or_default(); + + let cost = NodeCost { + local: Some(local), + virtual_storage_nodes, + agg_func: self.cost_agg_func, + }; + + Ok(cost) + } + + /// Build a [`FlowConstraints`] from the builder. + fn build_flow_constraints(&self, resolution_maps: &ResolutionMaps) -> Result { + let min_flow = self + .min_flow + .as_ref() + .map(|min_flow| { + min_flow + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "min_flow".to_string(), + source, + }) + }) + .transpose()?; + + let max_flow = self + .max_flow + .as_ref() + .map(|max_flow| { + max_flow + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "max_flow".to_string(), + source, + }) + }) + .transpose()?; + + let flow_constraints = FlowConstraints::new(min_flow, max_flow); + + Ok(flow_constraints) + } + + /// Build a [`StorageConstraints`] from the builder. + fn build_storage_constraints( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + let min_volume = self + .min_volume + .as_ref() + .map(|min_volume| { + min_volume + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "min_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| NodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "max_volume".to_string(), + source, + }) + }) + .transpose()?; + + let max_volume = self + .max_volume + .as_ref() + .map(|max_volume| { + max_volume + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "max_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| NodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "max_volume".to_string(), + source, + }) + }) + .transpose()?; + + let storage_constraints = StorageConstraints { min_volume, max_volume }; + + Ok(storage_constraints) + } + + fn build_storage_initial_volume( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + match &self.initial_volume { + Some(iv) => match iv { + UnresolvedStorageInitialVolume::Absolute(iv) => Ok(StorageInitialVolume::Absolute(*iv)), + UnresolvedStorageInitialVolume::Proportional(iv) => Ok(StorageInitialVolume::Proportional(*iv)), + UnresolvedStorageInitialVolume::DistributedAbsolute { + absolute, + prior_max_volume, + } => { + let prior_max_volume = prior_max_volume + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "prior_max_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| NodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "prior_max_volume".to_string(), + source, + })?; + Ok(StorageInitialVolume::DistributedAbsolute { + absolute: *absolute, + prior_max_volume, + }) + } + UnresolvedStorageInitialVolume::DistributedProportional { + proportion, + total_volume, + prior_max_volume, + } => { + let total_volume = total_volume + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "total_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| NodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "total_volume".to_string(), + source, + })?; + let prior_max_volume = prior_max_volume + .resolve(resolution_maps) + .map_err(|source| NodeBuilderError::ResolveMetricF64Error { + attr: "prior_max_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| NodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "prior_max_volume".to_string(), + source, + })?; + + Ok(StorageInitialVolume::DistributedProportional { + total_volume, + proportion: *proportion, + prior_max_volume, + }) + } + }, + None => Err(NodeBuilderError::InitialVolumeNotDefined), + } + } + pub fn build(&self, resolution_maps: &ResolutionMaps) -> Result { + let index = resolution_maps + .nodes + .get(&self.name) + .ok_or(NodeBuilderError::IndexNotFound)?; + + let meta = NodeMeta { + index: *index, + name: self.name.name.clone(), + sub_name: self.name.sub_name.clone(), + }; + + let cost = self.build_cost(resolution_maps)?; + + let incoming_edges = resolution_maps.incoming_edges.get(index).cloned().unwrap_or_default(); + let outgoing_edges = resolution_maps.outgoing_edges.get(index).cloned().unwrap_or_default(); + + let node = match self.node_type { + NodeType::Input => { + if outgoing_edges.is_empty() { + return Err(NodeBuilderError::NoOutgoingEdges { + node_type: self.node_type, + }); + } + if !incoming_edges.is_empty() { + return Err(NodeBuilderError::UnexpectedIncomingEdges { + node_type: self.node_type, + num: incoming_edges.len(), + }); + } + + Node::Input(InputNode { + meta, + cost, + flow_constraints: self.build_flow_constraints(resolution_maps)?, + outgoing_edges, + }) + } + NodeType::Output => { + if !outgoing_edges.is_empty() { + return Err(NodeBuilderError::UnexpectedOutgoingEdges { + node_type: self.node_type, + num: outgoing_edges.len(), + }); + } + if incoming_edges.is_empty() { + return Err(NodeBuilderError::NoIncomingEdges { + node_type: self.node_type, + }); + } + + Node::Output(OutputNode { + meta, + cost, + flow_constraints: self.build_flow_constraints(resolution_maps)?, + incoming_edges, + }) + } + NodeType::Link => { + if outgoing_edges.is_empty() { + return Err(NodeBuilderError::NoOutgoingEdges { + node_type: self.node_type, + }); + } + if incoming_edges.is_empty() { + return Err(NodeBuilderError::NoIncomingEdges { + node_type: self.node_type, + }); + } + + Node::Link(LinkNode { + meta, + cost, + flow_constraints: Default::default(), + incoming_edges, + outgoing_edges, + }) + } + NodeType::Storage => Node::Storage(StorageNode { + meta, + cost, + initial_volume: self.build_storage_initial_volume(resolution_maps)?, + incoming_edges, + outgoing_edges, + storage_constraints: self.build_storage_constraints(resolution_maps)?, + }), + }; + + Ok(node) } +} + +#[derive(Debug, PartialEq)] +pub enum Node { + Input(InputNode), + Output(OutputNode), + Link(LinkNode), + Storage(StorageNode), +} + +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum NodeType { + Input, + Output, + Link, + Storage, +} - pub fn push_new_storage( - &mut self, - name: &str, - sub_name: Option<&str>, - initial_volume: StorageInitialVolume, - min_volume: Option, - max_volume: Option, - ) -> NodeIndex { - let node_index = NodeIndex(self.nodes.len()); - let node = Node::new_storage(node_index, name, sub_name, initial_volume, min_volume, max_volume); - self.nodes.push(node); - node_index +impl Display for NodeType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + NodeType::Input => write!(f, "Input"), + NodeType::Output => write!(f, "Output"), + NodeType::Link => write!(f, "Link"), + NodeType::Storage => write!(f, "Storage"), + } } } @@ -165,40 +483,6 @@ pub enum CostAggFunc { } impl Node { - /// Create a new input node - pub fn new_input(node_index: NodeIndex, name: &str, sub_name: Option<&str>) -> Self { - Self::Input(InputNode::new(node_index, name, sub_name)) - } - - /// Create a new output node - pub fn new_output(node_index: NodeIndex, name: &str, sub_name: Option<&str>) -> Self { - Self::Output(OutputNode::new(node_index, name, sub_name)) - } - - /// Create a new link node - pub fn new_link(node_index: NodeIndex, name: &str, sub_name: Option<&str>) -> Self { - Self::Link(LinkNode::new(node_index, name, sub_name)) - } - - /// Create a new storage node - pub fn new_storage( - node_index: NodeIndex, - name: &str, - sub_name: Option<&str>, - initial_volume: StorageInitialVolume, - min_volume: Option, - max_volume: Option, - ) -> Self { - Self::Storage(StorageNode::new( - node_index, - name, - sub_name, - initial_volume, - min_volume, - max_volume, - )) - } - /// Get a node's name pub fn name(&self) -> &str { match self { @@ -273,42 +557,6 @@ impl Node { } } - pub fn add_incoming_edge(&mut self, edge: EdgeIndex) -> Result<(), NodeError> { - match self { - Self::Input(_) => Err(NodeError::InvalidNodeConnectionToInput), - Self::Output(n) => { - n.add_incoming_edge(edge); - Ok(()) - } - Self::Link(n) => { - n.add_incoming_edge(edge); - Ok(()) - } - Self::Storage(n) => { - n.add_incoming_edge(edge); - Ok(()) - } - } - } - - pub fn add_outgoing_edge(&mut self, edge: EdgeIndex) -> Result<(), NodeError> { - match self { - Self::Input(n) => { - n.add_outgoing_edge(edge); - Ok(()) - } - Self::Output(_) => Err(NodeError::InvalidNodeConnectionFromOutput), - Self::Link(n) => { - n.add_outgoing_edge(edge); - Ok(()) - } - Self::Storage(n) => { - n.add_outgoing_edge(edge); - Ok(()) - } - } - } - pub fn get_incoming_edges(&self) -> Result<&Vec, NodeError> { match self { Self::Input(_) => Err(NodeError::InputNodeHasNoIncomingEdges), @@ -327,24 +575,6 @@ impl Node { } } - pub fn add_virtual_storage(&mut self, virtual_storage_index: VirtualStorageIndex) -> Result<(), NodeError> { - match self { - Self::Input(n) => { - n.cost.virtual_storage_nodes.push(virtual_storage_index); - Ok(()) - } - Self::Output(n) => { - n.cost.virtual_storage_nodes.push(virtual_storage_index); - Ok(()) - } - Self::Link(n) => { - n.cost.virtual_storage_nodes.push(virtual_storage_index); - Ok(()) - } - Self::Storage(_) => Err(NodeError::NoVirtualStorageOnStorageNode), - } - } - pub fn before(&self, timestep: &Timestep, state: &mut State) -> Result<(), NodeError> { // Currently only storage nodes do something during before match self { @@ -355,24 +585,6 @@ impl Node { } } - pub fn set_min_flow_constraint(&mut self, value: Option) -> Result<(), NodeError> { - match self { - Self::Input(n) => { - n.set_min_flow(value); - Ok(()) - } - Self::Link(n) => { - n.set_min_flow(value); - Ok(()) - } - Self::Output(n) => { - n.set_min_flow(value); - Ok(()) - } - Self::Storage(_) => Err(NodeError::FlowConstraintsUndefined), - } - } - pub fn get_min_flow(&self, network: &Network, state: &State) -> Result { match self { Self::Input(n) => Ok(n.get_min_flow(network, state)?), @@ -391,24 +603,6 @@ impl Node { } } - pub fn set_max_flow_constraint(&mut self, value: Option) -> Result<(), NodeError> { - match self { - Self::Input(n) => { - n.set_max_flow(value); - Ok(()) - } - Self::Link(n) => { - n.set_max_flow(value); - Ok(()) - } - Self::Output(n) => { - n.set_max_flow(value); - Ok(()) - } - Self::Storage(_) => Err(NodeError::FlowConstraintsUndefined), - } - } - pub fn get_max_flow(&self, network: &Network, state: &State) -> Result { match self { Self::Input(n) => Ok(n.get_max_flow(network, state)?), @@ -436,30 +630,6 @@ impl Node { } } - pub fn set_initial_volume(&mut self, initial_volume: StorageInitialVolume) -> Result<(), NodeError> { - match self { - Self::Input(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Link(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Output(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Storage(n) => { - n.set_initial_volume(initial_volume); - Ok(()) - } - } - } - - pub fn set_min_volume_constraint(&mut self, value: Option) -> Result<(), NodeError> { - match self { - Self::Input(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Link(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Output(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Storage(n) => { - n.set_min_volume(value); - Ok(()) - } - } - } - pub fn get_min_volume(&self, state: &State) -> Result { match self { Self::Input(_) => Err(NodeError::StorageConstraintsUndefined), @@ -469,18 +639,6 @@ impl Node { } } - pub fn set_max_volume_constraint(&mut self, value: Option) -> Result<(), NodeError> { - match self { - Self::Input(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Link(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Output(_) => Err(NodeError::StorageConstraintsUndefined), - Self::Storage(n) => { - n.set_max_volume(value); - Ok(()) - } - } - } - pub fn get_max_volume(&self, state: &State) -> Result { match self { Self::Input(_) => Err(NodeError::StorageConstraintsUndefined), @@ -560,26 +718,6 @@ impl Node { } } - pub fn set_cost(&mut self, value: Option) { - match self { - Self::Input(n) => n.set_cost(value), - Self::Link(n) => n.set_cost(value), - Self::Output(n) => n.set_cost(value), - Self::Storage(n) => n.set_cost(value), - } - } - - pub fn set_cost_agg_func(&mut self, agg_func: Option) -> Result<(), NodeError> { - match self { - Self::Input(n) => n.set_cost_agg_func(agg_func), - Self::Link(n) => n.set_cost_agg_func(agg_func), - Self::Output(n) => n.set_cost_agg_func(agg_func), - Self::Storage(_) => return Err(NodeError::NoVirtualStorageOnStorageNode), - }; - - Ok(()) - } - pub fn get_outgoing_cost(&self, network: &Network, state: &State) -> Result { match self { Self::Input(n) => n.get_cost(network, state), @@ -599,49 +737,91 @@ impl Node { } } +/// Metadata for a node without its index. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct UnresolvedNode { + name: String, + sub_name: Option, +} + +impl From<&str> for UnresolvedNode { + fn from(name: &str) -> Self { + Self { + name: name.to_string(), + sub_name: None, + } + } +} + +impl UnresolvedNode { + pub fn new(name: &str, sub_name: Option<&str>) -> Self { + Self { + name: name.to_string(), + sub_name: sub_name.map(|s| s.to_string()), + } + } + + pub fn set_sub_name(&mut self, sub_name: Option<&str>) { + self.sub_name = sub_name.map(|s| s.to_string()); + } +} + +impl Display for UnresolvedNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.sub_name { + Some(sub) => write!(f, "{}[{}]", self.name, sub), + None => write!(f, "{}", self.name), + } + } +} + /// Meta data common to all nodes. #[derive(Debug, PartialEq, Eq)] pub struct NodeMeta { index: T, name: String, sub_name: Option, - comment: String, } -impl NodeMeta -where - T: Copy, -{ - pub(crate) fn new(index: T, name: &str, sub_name: Option<&str>) -> Self { +impl NodeMeta { + pub fn from_unresolved_name(name: UnresolvedNode, index: T) -> Self { Self { + name: name.name, index, - name: name.to_string(), - sub_name: sub_name.map(|s| s.to_string()), - comment: "".to_string(), + sub_name: name.sub_name, } } +} - pub(crate) fn index(&self) -> &T { +impl NodeMeta +where + T: Copy, +{ + pub fn index(&self) -> &T { &self.index } - pub(crate) fn name(&self) -> &str { + pub fn name(&self) -> &str { self.name.as_str() } - pub(crate) fn sub_name(&self) -> Option<&str> { + pub fn sub_name(&self) -> Option<&str> { self.sub_name.as_deref() } - pub(crate) fn full_name(&self) -> (&str, Option<&str>) { + pub fn full_name(&self) -> (&str, Option<&str>) { (self.name(), self.sub_name()) } } #[derive(Debug, PartialEq, Default)] pub struct FlowConstraints { - pub min_flow: Option, - pub max_flow: Option, + min_flow: Option, + max_flow: Option, } impl FlowConstraints { + pub fn new(min_flow: Option, max_flow: Option) -> Self { + Self { min_flow, max_flow } + } + /// Return the current minimum flow from the parameter state /// /// Defaults to zero if no parameter is defined. @@ -688,14 +868,15 @@ impl FlowConstraints { #[derive(Debug, PartialEq)] pub struct StorageConstraints { - pub(crate) min_volume: Option, - pub(crate) max_volume: Option, + min_volume: Option, + max_volume: Option, } impl StorageConstraints { pub fn new(min_volume: Option, max_volume: Option) -> Self { Self { min_volume, max_volume } } + /// Return the current minimum volume from the parameter state /// /// Defaults to zero if no parameter is defined. @@ -718,7 +899,7 @@ impl StorageConstraints { /// Generic cost data for a node. #[derive(Debug, PartialEq, Default)] -struct NodeCost { +pub struct NodeCost { local: Option, virtual_storage_nodes: Vec, agg_func: Option, @@ -765,42 +946,24 @@ impl NodeCost { #[derive(Debug, PartialEq)] pub struct InputNode { - pub meta: NodeMeta, + meta: NodeMeta, cost: NodeCost, - pub flow_constraints: FlowConstraints, - pub outgoing_edges: Vec, + flow_constraints: FlowConstraints, + outgoing_edges: Vec, } impl InputNode { - fn new(index: NodeIndex, name: &str, sub_name: Option<&str>) -> Self { - Self { - meta: NodeMeta::new(index, name, sub_name), - cost: NodeCost::default(), - flow_constraints: FlowConstraints::default(), - outgoing_edges: Vec::new(), - } - } - fn set_cost(&mut self, value: Option) { - self.cost.local = value - } - fn set_cost_agg_func(&mut self, agg_func: Option) { - self.cost.agg_func = agg_func - } fn get_cost(&self, network: &Network, state: &State) -> Result { self.cost.get_cost(network, state) } - fn set_min_flow(&mut self, value: Option) { - self.flow_constraints.min_flow = value; - } + fn get_min_flow(&self, network: &Network, state: &State) -> Result { self.flow_constraints.get_min_flow(network, state) } fn get_const_min_flow(&self, values: &ConstParameterValues) -> Result, ConstantMetricF64Error> { self.flow_constraints.get_const_min_flow(values) } - fn set_max_flow(&mut self, value: Option) { - self.flow_constraints.max_flow = value; - } + fn get_max_flow(&self, network: &Network, state: &State) -> Result { self.flow_constraints.get_max_flow(network, state) } @@ -810,49 +973,28 @@ impl InputNode { fn is_max_flow_unconstrained(&self) -> bool { self.flow_constraints.is_max_flow_unconstrained() } - fn add_outgoing_edge(&mut self, edge: EdgeIndex) { - self.outgoing_edges.push(edge); - } } #[derive(Debug, PartialEq)] pub struct OutputNode { - pub meta: NodeMeta, + meta: NodeMeta, cost: NodeCost, - pub flow_constraints: FlowConstraints, - pub incoming_edges: Vec, + flow_constraints: FlowConstraints, + incoming_edges: Vec, } impl OutputNode { - fn new(index: NodeIndex, name: &str, sub_name: Option<&str>) -> Self { - Self { - meta: NodeMeta::new(index, name, sub_name), - cost: NodeCost::default(), - flow_constraints: FlowConstraints::default(), - incoming_edges: Vec::new(), - } - } - fn set_cost(&mut self, value: Option) { - self.cost.local = value - } fn get_cost(&self, network: &Network, state: &State) -> Result { self.cost.get_cost(network, state) } - fn set_cost_agg_func(&mut self, agg_func: Option) { - self.cost.agg_func = agg_func - } - fn set_min_flow(&mut self, value: Option) { - self.flow_constraints.min_flow = value; - } + fn get_min_flow(&self, network: &Network, state: &State) -> Result { self.flow_constraints.get_min_flow(network, state) } fn get_const_min_flow(&self, values: &ConstParameterValues) -> Result, ConstantMetricF64Error> { self.flow_constraints.get_const_min_flow(values) } - fn set_max_flow(&mut self, value: Option) { - self.flow_constraints.max_flow = value; - } + fn get_max_flow(&self, network: &Network, state: &State) -> Result { self.flow_constraints.get_max_flow(network, state) } @@ -862,51 +1004,29 @@ impl OutputNode { fn is_max_flow_unconstrained(&self) -> bool { self.flow_constraints.is_max_flow_unconstrained() } - fn add_incoming_edge(&mut self, edge: EdgeIndex) { - self.incoming_edges.push(edge); - } } #[derive(Debug, PartialEq)] pub struct LinkNode { - pub meta: NodeMeta, + meta: NodeMeta, cost: NodeCost, - pub flow_constraints: FlowConstraints, - pub incoming_edges: Vec, - pub outgoing_edges: Vec, + flow_constraints: FlowConstraints, + incoming_edges: Vec, + outgoing_edges: Vec, } impl LinkNode { - fn new(index: NodeIndex, name: &str, sub_name: Option<&str>) -> Self { - Self { - meta: NodeMeta::new(index, name, sub_name), - cost: NodeCost::default(), - flow_constraints: FlowConstraints::default(), - incoming_edges: Vec::new(), - outgoing_edges: Vec::new(), - } - } - fn set_cost(&mut self, value: Option) { - self.cost.local = value - } - fn set_cost_agg_func(&mut self, agg_func: Option) { - self.cost.agg_func = agg_func - } fn get_cost(&self, network: &Network, state: &State) -> Result { self.cost.get_cost(network, state) } - fn set_min_flow(&mut self, value: Option) { - self.flow_constraints.min_flow = value; - } + fn get_min_flow(&self, network: &Network, state: &State) -> Result { self.flow_constraints.get_min_flow(network, state) } fn get_const_min_flow(&self, values: &ConstParameterValues) -> Result, ConstantMetricF64Error> { self.flow_constraints.get_const_min_flow(values) } - fn set_max_flow(&mut self, value: Option) { - self.flow_constraints.max_flow = value; - } + fn get_max_flow(&self, network: &Network, state: &State) -> Result { self.flow_constraints.get_max_flow(network, state) } @@ -916,12 +1036,6 @@ impl LinkNode { fn is_max_flow_unconstrained(&self) -> bool { self.flow_constraints.is_max_flow_unconstrained() } - fn add_incoming_edge(&mut self, edge: EdgeIndex) { - self.incoming_edges.push(edge); - } - fn add_outgoing_edge(&mut self, edge: EdgeIndex) { - self.outgoing_edges.push(edge); - } } /// Initial volume for a storage node. @@ -988,35 +1102,60 @@ impl StorageInitialVolume { } } -#[derive(Debug, PartialEq)] -pub struct StorageNode { - pub meta: NodeMeta, - pub cost: Option, - pub initial_volume: StorageInitialVolume, - pub storage_constraints: StorageConstraints, - pub incoming_edges: Vec, - pub outgoing_edges: Vec, +pub enum UnresolvedStorageInitialVolume { + Absolute(f64), + Proportional(f64), + DistributedAbsolute { + absolute: f64, + prior_max_volume: UnresolvedMetricF64, + }, + DistributedProportional { + total_volume: UnresolvedMetricF64, + proportion: f64, + prior_max_volume: UnresolvedMetricF64, + }, } -impl StorageNode { - fn new( - index: NodeIndex, - name: &str, - sub_name: Option<&str>, - initial_volume: StorageInitialVolume, - min_volume: Option, - max_volume: Option, +impl UnresolvedStorageInitialVolume { + pub fn absolute(absolute: f64) -> Self { + Self::Absolute(absolute) + } + + pub fn proportional(proportion: f64) -> Self { + Self::Proportional(proportion) + } + + pub fn distributed_absolute(absolute: f64, prior_max_volume: UnresolvedMetricF64) -> Self { + Self::DistributedAbsolute { + absolute, + prior_max_volume, + } + } + + pub fn distributed_proportional( + total_volume: UnresolvedMetricF64, + proportion: f64, + prior_max_volume: UnresolvedMetricF64, ) -> Self { - Self { - meta: NodeMeta::new(index, name, sub_name), - cost: None, - initial_volume, - storage_constraints: StorageConstraints::new(min_volume, max_volume), - incoming_edges: Vec::new(), - outgoing_edges: Vec::new(), + Self::DistributedProportional { + total_volume, + proportion, + prior_max_volume, } } +} +#[derive(Debug, PartialEq)] +pub struct StorageNode { + meta: NodeMeta, + cost: NodeCost, + initial_volume: StorageInitialVolume, + storage_constraints: StorageConstraints, + incoming_edges: Vec, + outgoing_edges: Vec, +} + +impl StorageNode { pub fn before(&self, timestep: &Timestep, state: &mut State) -> Result<(), NodeError> { // Set the initial volume if it is the first timestep. if timestep.is_first() { @@ -1028,38 +1167,17 @@ impl StorageNode { Ok(()) } - fn set_cost(&mut self, value: Option) { - self.cost = value - } - fn get_cost(&self, network: &Network, state: &State) -> Result { - match &self.cost { - None => Ok(0.0), - Some(m) => m.get_value(network, state), - } - } - fn set_initial_volume(&mut self, initial_volume: StorageInitialVolume) { - self.initial_volume = initial_volume; - } - fn set_min_volume(&mut self, value: Option) { - // TODO use a set_min_volume method - self.storage_constraints.min_volume = value; + fn get_cost(&self, network: &Network, state: &State) -> Result { + self.cost.get_cost(network, state) } + pub fn get_min_volume(&self, state: &State) -> Result { self.storage_constraints .get_min_volume(&state.get_simple_parameter_values()) } - fn set_max_volume(&mut self, value: Option) { - // TODO use a set_min_volume method - self.storage_constraints.max_volume = value; - } + pub fn get_max_volume(&self, state: &State) -> Result { self.storage_constraints .get_max_volume(&state.get_simple_parameter_values()) } - fn add_incoming_edge(&mut self, edge: EdgeIndex) { - self.incoming_edges.push(edge); - } - fn add_outgoing_edge(&mut self, edge: EdgeIndex) { - self.outgoing_edges.push(edge); - } } diff --git a/pywr-core/src/parameters/aggregated.rs b/pywr-core/src/parameters/aggregated.rs index 32f9c26a..56925071 100644 --- a/pywr-core/src/parameters/aggregated.rs +++ b/pywr-core/src/parameters/aggregated.rs @@ -1,8 +1,11 @@ -use super::{ConstParameter, Parameter, ParameterName, ParameterState, SimpleParameter}; +use super::{ + BuiltParameter, ConstParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterName, ParameterState, SimpleParameter, +}; use crate::agg_funcs::AggFuncF64; -use crate::metric::{ConstantMetricF64, MetricF64, SimpleMetricF64}; -use crate::network::Network; -use crate::parameters::errors::{ConstCalculationError, ParameterCalculationError, SimpleCalculationError}; +use crate::metric::{ConstantMetricF64, MetricF64, MetricF64ResolutionError, SimpleMetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::{ConstCalculationError, GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta}; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, SimpleParameterValues, State}; @@ -14,19 +17,6 @@ pub struct AggregatedParameter { agg_func: AggFuncF64, } -impl AggregatedParameter -where - M: Send + Sync + Clone, -{ - pub fn new(name: ParameterName, metrics: &[M], agg_func: AggFuncF64) -> Self { - Self { - meta: ParameterMeta::new(name), - metrics: metrics.to_vec(), - agg_func, - } - } -} - impl Parameter for AggregatedParameter where M: Send + Sync, @@ -44,7 +34,7 @@ impl GeneralParameter for AggregatedParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let values = self .metrics .iter() @@ -54,13 +44,6 @@ impl GeneralParameter for AggregatedParameter { Ok(Some(self.agg_func.calc_iter_f64(&values)?)) } - fn as_parameter(&self) -> &dyn Parameter - where - Self: Sized, - { - self - } - fn try_into_simple(&self) -> Option>> { // We can make a simple version if all metrics can be simplified let metrics: Vec = self @@ -76,6 +59,13 @@ impl GeneralParameter for AggregatedParameter { agg_func: self.agg_func.clone(), })) } + + fn as_parameter(&self) -> &dyn Parameter + where + Self: Sized, + { + self + } } impl SimpleParameter for AggregatedParameter { @@ -142,3 +132,68 @@ impl ConstParameter for AggregatedParameter { self } } + +pub struct AggregatedParameterBuilder { + meta: ParameterMeta, + agg_func: AggFuncF64, + metrics: Vec, +} + +impl AggregatedParameterBuilder { + pub fn new(name: ParameterName, agg_func: AggFuncF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metrics: Vec::new(), + agg_func, + } + } + + /// Add a new metric to the builder + pub fn metric(&mut self, metric: UnresolvedMetricF64) -> &mut Self { + self.metrics.push(metric); + self + } +} + +impl ParameterBuilder for AggregatedParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let mut metrics = Vec::with_capacity(self.metrics.len()); + for m in &self.metrics { + match m.resolve(resolution_maps) { + Ok(m) => metrics.push(m), + Err(err) => { + return if let MetricF64ResolutionError::ParameterNotFound { .. } = err { + Ok(MaybeBuiltParameter::Retry(self)) + } else { + Err(ParameterBuildError::ResolveMetricF64Error { + attr: "metrics".to_string(), + source: err, + }) + }; + } + } + } + + let p = AggregatedParameter { + meta: self.meta.clone(), + metrics, + agg_func: self.agg_func.clone(), + }; + + let bp = match p.try_into_simple() { + None => BuiltParameter::General(Box::new(p)), + Some(sp) => match sp.try_into_const() { + None => BuiltParameter::Simple(sp), + Some(cp) => BuiltParameter::Const(cp), + }, + }; + + Ok(MaybeBuiltParameter::Built(bp)) + } +} diff --git a/pywr-core/src/parameters/aggregated_index.rs b/pywr-core/src/parameters/aggregated_index.rs index 87ac275c..5b318077 100644 --- a/pywr-core/src/parameters/aggregated_index.rs +++ b/pywr-core/src/parameters/aggregated_index.rs @@ -4,7 +4,7 @@ use super::{ConstParameter, Parameter, ParameterName, ParameterState, SimplePara use crate::agg_funcs::AggFuncU64; use crate::metric::{ConstantMetricU64, MetricU64, SimpleMetricU64}; use crate::network::Network; -use crate::parameters::errors::{ConstCalculationError, ParameterCalculationError, SimpleCalculationError}; +use crate::parameters::errors::{ConstCalculationError, GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta}; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, SimpleParameterValues, State}; @@ -46,7 +46,7 @@ impl GeneralParameter for AggregatedIndexParameter { network: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let values = self .metrics .iter() diff --git a/pywr-core/src/parameters/array.rs b/pywr-core/src/parameters/array.rs index 57d229e5..ef588c04 100644 --- a/pywr-core/src/parameters/array.rs +++ b/pywr-core/src/parameters/array.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::{Timestep, TimestepIndex}; @@ -12,14 +16,6 @@ pub struct Array1Parameter { } impl Array1Parameter { - pub fn new(name: ParameterName, array: Array1, timestep_offset: Option) -> Self { - Self { - meta: ParameterMeta::new(name), - array, - timestep_offset, - } - } - /// Compute the time-step index to use accounting for any defined offset. /// /// The offset is applied to the time-step index and then clamped to the bounds of the array. @@ -98,28 +94,69 @@ impl SimpleParameter for Array1Parameter { } } -pub struct Array2Parameter { +pub struct Array1ParameterBuilder { meta: ParameterMeta, - array: Array2, - scenario_group_index: usize, + array: Array1, timestep_offset: Option, } -impl Array2Parameter { - pub fn new( - name: ParameterName, - array: Array2, - scenario_group_index: usize, - timestep_offset: Option, - ) -> Self { +impl Array1ParameterBuilder { + pub fn new(name: ParameterName, array: Array1) -> Self { Self { meta: ParameterMeta::new(name), array, - scenario_group_index, - timestep_offset, + timestep_offset: None, } } + pub fn timestep_offset(&mut self, offset: i32) -> &mut Self { + self.timestep_offset = Some(offset); + self + } +} + +impl ParameterBuilder for Array1ParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = Array1Parameter { + meta: self.meta, + array: self.array, + timestep_offset: self.timestep_offset, + }; + Ok(BuiltParameter::Simple(Box::new(p)).into()) + } +} + +impl ParameterBuilder for Array1ParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = Array1Parameter { + meta: self.meta, + array: self.array, + timestep_offset: self.timestep_offset, + }; + Ok(BuiltParameter::Simple(Box::new(p)).into()) + } +} + +pub struct Array2Parameter { + meta: ParameterMeta, + array: Array2, + scenario_group_index: usize, + timestep_offset: Option, +} + +impl Array2Parameter { /// Compute the time-step index to use accounting for any defined offset. /// /// The offset is applied to the time-step index and then clamped to the bounds of the array. @@ -229,6 +266,80 @@ impl SimpleParameter for Array2Parameter { } } +pub struct Array2ParameterBuilder { + meta: ParameterMeta, + array: Array2, + scenario_group: String, + timestep_offset: Option, +} + +impl Array2ParameterBuilder { + pub fn new(name: ParameterName, array: Array2, scenario_group: &str) -> Self { + Self { + meta: ParameterMeta::new(name), + array, + scenario_group: scenario_group.to_string(), + timestep_offset: None, + } + } + + pub fn timestep_offset(&mut self, offset: i32) -> &mut Self { + self.timestep_offset = Some(offset); + self + } +} + +impl ParameterBuilder for Array2ParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let scenario_group_index = resolution_maps + .scenario_group + .get(&self.scenario_group) + .ok_or_else(|| ParameterBuildError::ScenarioGroupNotFound { + name: self.scenario_group.clone(), + })?; + + let p = Array2Parameter { + meta: self.meta, + array: self.array, + scenario_group_index: *scenario_group_index, + timestep_offset: self.timestep_offset, + }; + + Ok(BuiltParameter::Simple(Box::new(p)).into()) + } +} + +impl ParameterBuilder for Array2ParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let scenario_group_index = resolution_maps + .scenario_group + .get(&self.scenario_group) + .ok_or_else(|| ParameterBuildError::ScenarioGroupNotFound { + name: self.scenario_group.clone(), + })?; + + let p = Array2Parameter { + meta: self.meta, + array: self.array, + scenario_group_index: *scenario_group_index, + timestep_offset: self.timestep_offset, + }; + Ok(BuiltParameter::Simple(Box::new(p)).into()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -242,7 +353,11 @@ mod tests { let domain = default_domain(); let data = Array::range(0.0, 366.0, 1.0); - let p = Array1Parameter::new("my-array-parameter".into(), data, None); + let p = Array1Parameter { + meta: ParameterMeta::new("my-array-parameter".into()), + array: data, + timestep_offset: None, + }; let spv = StateBuilder::new(Vec::new(), 0).build(); @@ -270,7 +385,12 @@ mod tests { let data = Array::range(0.0, 366.0, 1.0); let data = data.insert_axis(Axis(1)); - let p = Array2Parameter::new("my-array-parameter".into(), data, 0, None); + let p = Array2Parameter { + meta: ParameterMeta::new("my-array-parameter".into()), + array: data, + scenario_group_index: 0, + timestep_offset: None, + }; let spv = StateBuilder::new(Vec::new(), 0).build(); @@ -298,7 +418,12 @@ mod tests { let data = Array::range(0.0, 5.0, 1.0); let data = data.insert_axis(Axis(1)); - let p = Array2Parameter::new("my-array-parameter".into(), data, 0, None); + let p = Array2Parameter { + meta: ParameterMeta::new("my-array-parameter".into()), + array: data, + scenario_group_index: 0, + timestep_offset: None, + }; let spv = StateBuilder::new(Vec::new(), 0).build(); diff --git a/pywr-core/src/parameters/asymmetric.rs b/pywr-core/src/parameters/asymmetric.rs index 73e34d20..75409cf0 100644 --- a/pywr-core/src/parameters/asymmetric.rs +++ b/pywr-core/src/parameters/asymmetric.rs @@ -1,6 +1,6 @@ use crate::metric::MetricU64; use crate::network::Network; -use crate::parameters::errors::{ParameterCalculationError, ParameterSetupError}; +use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, downcast_internal_state_mut, }; @@ -45,7 +45,7 @@ impl GeneralParameter for AsymmetricSwitchIndexParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let on_value = self.on_parameter.get_value(network, state)?; // Downcast the internal state to the correct type diff --git a/pywr-core/src/parameters/constant.rs b/pywr-core/src/parameters/constant.rs index 8274d9ed..2fc8cd56 100644 --- a/pywr-core/src/parameters/constant.rs +++ b/pywr-core/src/parameters/constant.rs @@ -1,8 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::{ConstCalculationError, ParameterSetupError}; use crate::parameters::{ - ActivationFunction, ConstParameter, Parameter, ParameterMeta, ParameterName, ParameterState, VariableConfig, - VariableParameter, VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, - downcast_variable_config_ref, + ActivationFunction, BuiltParameter, ConstParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, + ParameterBuilder, ParameterMeta, ParameterName, ParameterState, VariableConfig, VariableParameter, + VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, }; use crate::scenario::ScenarioIndex; use crate::state::ConstParameterValues; @@ -17,13 +18,6 @@ pub struct ConstantParameter { type InternalValue = Option; impl ConstantParameter { - pub fn new(name: ParameterName, value: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - value, - } - } - /// Return the current value. /// /// If the internal state is None, the value is returned directly. Otherwise, the internal value must @@ -122,9 +116,38 @@ impl VariableParameter for ConstantParameter { } } +pub struct ConstantParameterBuilder { + name: ParameterName, + value: f64, +} + +impl ConstantParameterBuilder { + pub fn new(name: ParameterName, value: f64) -> Self { + Self { name, value } + } +} + +impl ParameterBuilder for ConstantParameterBuilder { + fn name(&self) -> &ParameterName { + &self.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + Ok(BuiltParameter::Const(Box::new(ConstantParameter { + meta: ParameterMeta::new(self.name.clone()), + value: self.value, + })) + .into()) + } +} + #[cfg(test)] mod tests { - use crate::parameters::{ActivationFunction, ConstantParameter, Parameter, VariableParameter}; + use super::ConstantParameter; + use crate::parameters::{ActivationFunction, Parameter, ParameterMeta, VariableParameter}; use crate::test_utils::default_domain; use float_cmp::assert_approx_eq; use std::f64::consts::PI; @@ -134,7 +157,10 @@ mod tests { let domain = default_domain(); let var = ActivationFunction::Unit { min: 0.0, max: 2.0 }; - let p = ConstantParameter::new("test".into(), 1.0); + let p = ConstantParameter { + meta: ParameterMeta::new("my-parameter".into()), + value: 1.0, + }; let mut state = p .setup(domain.time().timesteps(), domain.scenarios().indices().first().unwrap()) .unwrap(); @@ -156,7 +182,10 @@ mod tests { fn test_constant_parameter() { let domain = default_domain(); - let p = ConstantParameter::new("my-parameter".into(), PI); + let p = ConstantParameter { + meta: ParameterMeta::new("my-parameter".into()), + value: PI, + }; let state = p .setup(domain.time().timesteps(), domain.scenarios().indices().first().unwrap()) .unwrap(); diff --git a/pywr-core/src/parameters/control_curves/apportion.rs b/pywr-core/src/parameters/control_curves/apportion.rs index 7d88b01c..fbeb4921 100644 --- a/pywr-core/src/parameters/control_curves/apportion.rs +++ b/pywr-core/src/parameters/control_curves/apportion.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, State}; @@ -45,7 +45,7 @@ impl GeneralParameter for ApportionParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; diff --git a/pywr-core/src/parameters/control_curves/index.rs b/pywr-core/src/parameters/control_curves/index.rs index f6f524f7..71759fe2 100644 --- a/pywr-core/src/parameters/control_curves/index.rs +++ b/pywr-core/src/parameters/control_curves/index.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -36,7 +36,7 @@ impl GeneralParameter for ControlCurveIndexParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; diff --git a/pywr-core/src/parameters/control_curves/interpolated.rs b/pywr-core/src/parameters/control_curves/interpolated.rs index 862562bb..8889dcd3 100644 --- a/pywr-core/src/parameters/control_curves/interpolated.rs +++ b/pywr-core/src/parameters/control_curves/interpolated.rs @@ -1,11 +1,15 @@ -use crate::metric::MetricF64; -use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::interpolate; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_f64_vec}; pub struct ControlCurveInterpolatedParameter { meta: ParameterMeta, @@ -14,17 +18,6 @@ pub struct ControlCurveInterpolatedParameter { values: Vec, } -impl ControlCurveInterpolatedParameter { - pub fn new(name: ParameterName, metric: MetricF64, control_curves: Vec, values: Vec) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - control_curves, - values, - } - } -} - impl Parameter for ControlCurveInterpolatedParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -39,7 +32,7 @@ impl GeneralParameter for ControlCurveInterpolatedParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; @@ -73,3 +66,63 @@ impl GeneralParameter for ControlCurveInterpolatedParameter { self } } + +pub struct ControlCurveInterpolatedParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + control_curves: Vec, + values: Vec, +} + +impl ControlCurveInterpolatedParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + control_curves: Vec::new(), + values: Vec::new(), + } + } + + pub fn control_curve(&mut self, control_curve: UnresolvedMetricF64) -> &mut Self { + self.control_curves.push(control_curve); + self + } + + pub fn value(&mut self, value: UnresolvedMetricF64) -> &mut Self { + self.values.push(value); + self + } +} + +impl ParameterBuilder for ControlCurveInterpolatedParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metrics"); + let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); + let values = resolve_metric_f64_vec!(self, &self.values, resolution_maps, "values"); + + let p = ControlCurveInterpolatedParameter { + meta: self.meta, + metric, + control_curves, + values, + }; + + let bp = match p.try_into_simple() { + None => BuiltParameter::General(Box::new(p)), + Some(sp) => match sp.try_into_const() { + None => BuiltParameter::Simple(sp), + Some(cp) => BuiltParameter::Const(cp), + }, + }; + + Ok(bp.into()) + } +} diff --git a/pywr-core/src/parameters/control_curves/mod.rs b/pywr-core/src/parameters/control_curves/mod.rs index 6464d1fb..34903968 100644 --- a/pywr-core/src/parameters/control_curves/mod.rs +++ b/pywr-core/src/parameters/control_curves/mod.rs @@ -7,7 +7,7 @@ mod volume_between; pub use apportion::ApportionParameter; pub use index::ControlCurveIndexParameter; -pub use interpolated::ControlCurveInterpolatedParameter; -pub use piecewise::PiecewiseInterpolatedParameter; +pub use interpolated::ControlCurveInterpolatedParameterBuilder; +pub use piecewise::PiecewiseInterpolatedParameterBuilder; pub use simple::ControlCurveParameter; pub use volume_between::VolumeBetweenControlCurvesParameter; diff --git a/pywr-core/src/parameters/control_curves/piecewise.rs b/pywr-core/src/parameters/control_curves/piecewise.rs index 0447b1d0..5dac662e 100644 --- a/pywr-core/src/parameters/control_curves/piecewise.rs +++ b/pywr-core/src/parameters/control_curves/piecewise.rs @@ -1,11 +1,15 @@ -use crate::metric::MetricF64; -use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::interpolate; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_f64_vec}; pub struct PiecewiseInterpolatedParameter { meta: ParameterMeta, @@ -16,25 +20,6 @@ pub struct PiecewiseInterpolatedParameter { minimum: f64, } -impl PiecewiseInterpolatedParameter { - pub fn new( - name: ParameterName, - metric: MetricF64, - control_curves: Vec, - values: Vec<[f64; 2]>, - maximum: f64, - minimum: f64, - ) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - control_curves, - values, - maximum, - minimum, - } - } -} impl Parameter for PiecewiseInterpolatedParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -48,7 +33,7 @@ impl GeneralParameter for PiecewiseInterpolatedParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; @@ -59,7 +44,7 @@ impl GeneralParameter for PiecewiseInterpolatedParameter { let v = self .values .get(idx) - .ok_or_else(|| ParameterCalculationError::OutOfBoundsError { + .ok_or_else(|| GeneralCalculationError::OutOfBoundsError { axis: 0, index: idx, length: self.values.len(), @@ -71,7 +56,7 @@ impl GeneralParameter for PiecewiseInterpolatedParameter { let v = self .values .last() - .ok_or_else(|| ParameterCalculationError::OutOfBoundsError { + .ok_or_else(|| GeneralCalculationError::OutOfBoundsError { axis: 0, index: 0, length: self.values.len(), @@ -87,31 +72,105 @@ impl GeneralParameter for PiecewiseInterpolatedParameter { } } +pub struct PiecewiseInterpolatedParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + control_curves: Vec, + values: Vec<[f64; 2]>, + maximum: f64, + minimum: f64, +} + +impl PiecewiseInterpolatedParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, maximum: f64, minimum: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + control_curves: Vec::new(), + values: Vec::new(), + maximum, + minimum, + } + } + + /// Add a control curve to the builder. Control curves should be added in descending order, + /// i.e. the first control curve should be the one with the highest value, and the last control + /// curve should be the one with the lowest value. + pub fn control_curve(&mut self, control_curve: UnresolvedMetricF64) -> &mut Self { + self.control_curves.push(control_curve); + self + } + + /// Add a piecewise-pair to the builder. + pub fn value(&mut self, value: [f64; 2]) -> &mut Self { + self.values.push(value); + self + } +} + +impl ParameterBuilder for PiecewiseInterpolatedParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metrics"); + let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); + + let p = PiecewiseInterpolatedParameter { + meta: self.meta, + metric, + control_curves, + values: self.values, + maximum: self.maximum, + minimum: self.minimum, + }; + + let bp = match p.try_into_simple() { + None => BuiltParameter::General(Box::new(p)), + Some(sp) => match sp.try_into_const() { + None => BuiltParameter::Simple(sp), + Some(cp) => BuiltParameter::Const(cp), + }, + }; + + Ok(bp.into()) + } +} + #[cfg(test)] mod test { - use crate::parameters::{Array1Parameter, PiecewiseInterpolatedParameter}; + use crate::metric::UnresolvedMetricF64; + use crate::parameters::PiecewiseInterpolatedParameterBuilder; + use crate::parameters::array::Array1ParameterBuilder; use crate::test_utils::{run_and_assert_parameter, simple_model}; use ndarray::{Array1, Array2, Axis}; /// Basic functional test of the piecewise interpolation. #[test] fn test_basic() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); // Create an artificial volume series to use for the interpolation test - let volume = Array1Parameter::new("test-x".into(), Array1::linspace(1.0, 0.0, 21), None); + let volume = Array1ParameterBuilder::new("test-x".into(), Array1::linspace(1.0, 0.0, 21)); + model_builder.network_builder().parameters().f64(Box::new(volume)); - let volume_idx = model.network_mut().add_simple_parameter(Box::new(volume)).unwrap(); - - let parameter = PiecewiseInterpolatedParameter::new( + let mut parameter = PiecewiseInterpolatedParameterBuilder::new( "test-parameter".into(), - volume_idx.into_metric_f64_before(), // Interpolate with the parameter based values - vec![0.8.into(), 0.5.into()], - vec![[10.0, 1.0], [0.0, 0.0], [-1.0, -10.0]], + UnresolvedMetricF64::new_parameter_before("test-x"), // Interpolate with the parameter based values 1.0, 0.0, ); + parameter + .control_curve(0.8.into()) + .control_curve(0.5.into()) + .value([10.0, 1.0]) + .value([0.0, 0.0]) + .value([-1.0, -10.0]); + let expected_values: Array1 = [ 10.0, // full 1.0 + 9.0 * 0.15 / 0.2, // 95% @@ -139,6 +198,6 @@ mod test { .into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter(&mut model, Box::new(parameter), expected_values, None, Some(1e-12)); + run_and_assert_parameter(model_builder, Box::new(parameter), expected_values, None, Some(1e-12)); } } diff --git a/pywr-core/src/parameters/control_curves/simple.rs b/pywr-core/src/parameters/control_curves/simple.rs index 01111b06..667916d5 100644 --- a/pywr-core/src/parameters/control_curves/simple.rs +++ b/pywr-core/src/parameters/control_curves/simple.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -38,7 +38,7 @@ impl GeneralParameter for ControlCurveParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; @@ -48,7 +48,7 @@ impl GeneralParameter for ControlCurveParameter { let value = self .values .get(idx) - .ok_or_else(|| ParameterCalculationError::OutOfBoundsError { + .ok_or_else(|| GeneralCalculationError::OutOfBoundsError { axis: 0, index: idx, length: self.values.len(), @@ -60,7 +60,7 @@ impl GeneralParameter for ControlCurveParameter { let value = self .values .last() - .ok_or_else(|| ParameterCalculationError::OutOfBoundsError { + .ok_or_else(|| GeneralCalculationError::OutOfBoundsError { axis: 0, index: 0, length: self.values.len(), diff --git a/pywr-core/src/parameters/deficit.rs b/pywr-core/src/parameters/deficit.rs index 2a850462..8e86dcec 100644 --- a/pywr-core/src/parameters/deficit.rs +++ b/pywr-core/src/parameters/deficit.rs @@ -1,7 +1,7 @@ use crate::metric::MetricF64; use crate::network::Network; use crate::parameters::{ - GeneralParameter, Parameter, ParameterCalculationError, ParameterMeta, ParameterName, ParameterState, + GeneralCalculationError, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, }; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -41,7 +41,7 @@ impl GeneralParameter for DeficitParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let actual_flow = self.flow.get_value(model, state)?; let max_flow = self.max_flow.get_value(model, state)?; diff --git a/pywr-core/src/parameters/delay.rs b/pywr-core/src/parameters/delay.rs index 61a40680..ce8a5ba0 100644 --- a/pywr-core/src/parameters/delay.rs +++ b/pywr-core/src/parameters/delay.rs @@ -1,13 +1,17 @@ -use crate::metric::{MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64}; -use crate::network::Network; -use crate::parameters::errors::{ParameterCalculationError, ParameterSetupError, SimpleCalculationError}; +use crate::metric::{ + MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64, UnresolvedMetricF64, + UnresolvedMetricU64, +}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError, SimpleCalculationError}; use crate::parameters::{ - GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter, - downcast_internal_state_mut, + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, SimpleParameter, downcast_internal_state_mut, }; use crate::scenario::ScenarioIndex; use crate::state::{SimpleParameterValues, State}; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_u64}; use std::collections::VecDeque; pub struct DelayParameter { @@ -17,17 +21,6 @@ pub struct DelayParameter { initial_value: T, } -impl DelayParameter { - pub fn new(name: ParameterName, metric: M, delay: u64, initial_value: T) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - delay, - initial_value, - } - } -} - impl TryInto> for &DelayParameter where T: Copy, @@ -88,13 +81,13 @@ impl GeneralParameter for DelayParameter { _model: &Network, _state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_mut::>(internal_state); // Take the oldest value from the queue // It should be guaranteed that the internal memory/queue has self.delay number of values - let value = memory.pop_front().ok_or_else(|| ParameterCalculationError::Internal { + let value = memory.pop_front().ok_or_else(|| GeneralCalculationError::Internal { message: "Delay parameter queue did not contain any values. This internal error should not be possible!" .into(), })?; @@ -109,7 +102,7 @@ impl GeneralParameter for DelayParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_mut::>(internal_state); @@ -191,13 +184,13 @@ impl GeneralParameter for DelayParameter { _model: &Network, _state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_mut::>(internal_state); // Take the oldest value from the queue // It should be guaranteed that the internal memory/queue has self.delay number of values - let value = memory.pop_front().ok_or_else(|| ParameterCalculationError::Internal { + let value = memory.pop_front().ok_or_else(|| GeneralCalculationError::Internal { message: "Delay parameter queue did not contain any values. This internal error should not be possible!" .into(), })?; @@ -212,7 +205,7 @@ impl GeneralParameter for DelayParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_mut::>(internal_state); @@ -286,26 +279,110 @@ impl SimpleParameter for DelayParameter { } } +pub struct DelayParameterBuilder { + meta: ParameterMeta, + metric: M, + delay: u64, + initial_value: T, +} + +impl DelayParameterBuilder { + pub fn new(name: ParameterName, metric: M, delay: u64, initial_value: T) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + delay, + initial_value, + } + } +} + +impl ParameterBuilder for DelayParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = DelayParameter { + meta: self.meta, + metric, + delay: self.delay, + initial_value: self.initial_value, + }; + + let bp = match p.try_into_simple() { + None => BuiltParameter::General(Box::new(p)), + Some(sp) => match sp.try_into_const() { + None => BuiltParameter::Simple(sp), + Some(cp) => BuiltParameter::Const(cp), + }, + }; + + Ok(bp.into()) + } +} + +impl ParameterBuilder for DelayParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_u64!(self, self.metric, resolution_maps, "metric"); + + let p = DelayParameter { + meta: self.meta, + metric, + delay: self.delay, + initial_value: self.initial_value, + }; + + let bp = match p.try_into_simple() { + None => BuiltParameter::General(Box::new(p)), + Some(sp) => match sp.try_into_const() { + None => BuiltParameter::Simple(sp), + Some(cp) => BuiltParameter::Const(cp), + }, + }; + + Ok(bp.into()) + } +} + #[cfg(test)] mod test { - use crate::metric::MetricU64; - use crate::parameters::{Array1Parameter, DelayParameter}; + use crate::metric::{UnresolvedMetricF64, UnresolvedMetricU64}; + use crate::parameters::Array1ParameterBuilder; + use crate::parameters::delay::DelayParameterBuilder; use crate::test_utils::{run_and_assert_parameter, run_and_assert_parameter_u64, simple_model}; use ndarray::{Array1, Array2, Axis, concatenate, s}; /// Basic functional test of the delay parameter. #[test] fn test_delay_f64() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); // Create an artificial volume series to use for the delay test let volumes = Array1::linspace(1.0, 0.0, 21); - let volume = Array1Parameter::new("test-x".into(), volumes.clone(), None); + let volume = Array1ParameterBuilder::new("test-x".into(), volumes.clone()); - let volume_idx = model.network_mut().add_simple_parameter(Box::new(volume)).unwrap(); + model_builder.network_builder().parameters().f64(Box::new(volume)); const DELAY: u64 = 3; // 3 time-step delay - let parameter = DelayParameter::new("test-parameter".into(), volume_idx.into_metric_f64_before(), DELAY, 0.0); + let parameter = DelayParameterBuilder::new( + "test-parameter".into(), + UnresolvedMetricF64::new_parameter_before("test-x"), + DELAY, + 0.0, + ); // We should have DELAY number of initial values to start with, and then follow the // values in the `volumes` array. @@ -323,26 +400,27 @@ mod test { let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter(&mut model, Box::new(parameter), expected_values, None, Some(1e-12)); + run_and_assert_parameter(model_builder, Box::new(parameter), expected_values, None, Some(1e-12)); } /// Basic functional test of the delay parameter. #[test] fn test_delay_u64() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); // Create an artificial volume series to use for the delay test let volumes: Array1 = Array1::from(Array1::linspace(1.0, 0.0, 21).map(|x| *x as u64)); - let volume = Array1Parameter::new("test-x".into(), volumes.clone(), None); + let volume = Array1ParameterBuilder::new("test-x".into(), volumes.clone()); - let metric: MetricU64 = model - .network_mut() - .add_simple_index_parameter(Box::new(volume)) - .unwrap() - .into_metric_u64_before(); + model_builder.network_builder().parameters().u64(Box::new(volume)); const DELAY: u64 = 3; // 3 time-step delay - let parameter = DelayParameter::new("test-parameter".into(), metric, DELAY, 0); + let parameter = DelayParameterBuilder::new( + "test-parameter".into(), + UnresolvedMetricU64::new_index_parameter_before("test-x"), + DELAY, + 0, + ); // We should have DELAY number of initial values to start with, and then follow the // values in the `volumes` array. @@ -360,6 +438,6 @@ mod test { let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter_u64(&mut model, Box::new(parameter), expected_values); + run_and_assert_parameter_u64(model_builder, Box::new(parameter), expected_values); } } diff --git a/pywr-core/src/parameters/difference.rs b/pywr-core/src/parameters/difference.rs index 5934d4e2..5453892f 100644 --- a/pywr-core/src/parameters/difference.rs +++ b/pywr-core/src/parameters/difference.rs @@ -1,7 +1,7 @@ use super::{Parameter, ParameterName, SimpleParameter}; use crate::metric::{MetricF64, SimpleMetricF64}; use crate::network::Network; -use crate::parameters::errors::{ParameterCalculationError, SimpleCalculationError}; +use crate::parameters::errors::{GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::{SimpleParameterValues, State}; @@ -52,7 +52,7 @@ impl GeneralParameter for DifferenceParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let a = self.a.get_value(model, state)?; let b = self.b.get_value(model, state)?; let min = self.min.as_ref().map(|m| m.get_value(model, state)).transpose()?; diff --git a/pywr-core/src/parameters/discount_factor.rs b/pywr-core/src/parameters/discount_factor.rs index 30228540..1edd4f1f 100644 --- a/pywr-core/src/parameters/discount_factor.rs +++ b/pywr-core/src/parameters/discount_factor.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::GeneralCalculationError; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -13,16 +17,6 @@ pub struct DiscountFactorParameter { base_year: i32, } -impl DiscountFactorParameter { - pub fn new(name: ParameterName, discount_rate: MetricF64, base_year: i32) -> Self { - Self { - meta: ParameterMeta::new(name), - discount_rate, - base_year, - } - } -} - impl Parameter for DiscountFactorParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -36,7 +30,7 @@ impl GeneralParameter for DiscountFactorParameter { network: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let year = timestep.date.year() - self.base_year; let rate = self.discount_rate.get_value(network, state)?; @@ -52,25 +46,62 @@ impl GeneralParameter for DiscountFactorParameter { } } +pub struct DiscountFactorParameterBuilder { + meta: ParameterMeta, + discount_rate: UnresolvedMetricF64, + base_year: i32, +} + +impl DiscountFactorParameterBuilder { + pub fn new(name: ParameterName, discount_rate: UnresolvedMetricF64, base_year: i32) -> Self { + Self { + meta: ParameterMeta::new(name), + discount_rate, + base_year, + } + } +} + +impl ParameterBuilder for DiscountFactorParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let discount_rate = resolve_metric_f64!(self, self.discount_rate, resolution_maps, "discount_rate"); + + let p = DiscountFactorParameter { + meta: self.meta, + discount_rate, + base_year: self.base_year, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(MaybeBuiltParameter::Built(bp)) + } +} + #[cfg(test)] mod test { - use crate::parameters::{Array1Parameter, DiscountFactorParameter}; + use crate::parameters::Array1ParameterBuilder; + use crate::parameters::discount_factor::DiscountFactorParameterBuilder; use crate::test_utils::{run_and_assert_parameter, simple_model}; use ndarray::{Array1, Array2, Axis}; /// Basic functional test of the delay parameter. #[test] fn test_basic() { - let mut model = simple_model(1, None); - let network = model.network_mut(); + let mut model_builder = simple_model(1, None); // Create an artificial volume series to use for the delay test let volumes = Array1::linspace(1.0, 0.0, 21); - let volume = Array1Parameter::new("test-x".into(), volumes.clone(), None); + let volume = Array1ParameterBuilder::new("test-x".into(), volumes.clone()); - let _volume_idx = network.add_simple_parameter(Box::new(volume)).unwrap(); + model_builder.network_builder().parameters().f64(Box::new(volume)); - let parameter = DiscountFactorParameter::new( + let parameter = DiscountFactorParameterBuilder::new( "test-parameter".into(), 0.03.into(), // Interpolate with the parameter based values 2020, @@ -86,6 +117,6 @@ mod test { let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter(&mut model, Box::new(parameter), expected_values, None, Some(1e-12)); + run_and_assert_parameter(model_builder, Box::new(parameter), expected_values, None, Some(1e-12)); } } diff --git a/pywr-core/src/parameters/division.rs b/pywr-core/src/parameters/division.rs index c18d1b71..32e16ee6 100644 --- a/pywr-core/src/parameters/division.rs +++ b/pywr-core/src/parameters/division.rs @@ -1,7 +1,7 @@ use super::{Parameter, ParameterName}; use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, ParameterMeta, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -35,11 +35,11 @@ impl GeneralParameter for DivisionParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let denominator = self.denominator.get_value(model, state)?; if denominator == 0.0 { - return Err(ParameterCalculationError::DivisionByZeroError); + return Err(GeneralCalculationError::DivisionByZeroError); } let numerator = self.numerator.get_value(model, state)?; diff --git a/pywr-core/src/parameters/errors.rs b/pywr-core/src/parameters/errors.rs index 0e095000..95ca5f00 100644 --- a/pywr-core/src/parameters/errors.rs +++ b/pywr-core/src/parameters/errors.rs @@ -21,7 +21,7 @@ pub enum ParameterSetupError { /// Errors returned by parameter calculations. #[derive(Error, Debug)] -pub enum ParameterCalculationError { +pub enum GeneralCalculationError { #[error("F64 metric error: {0}")] MetricF64Error(#[from] MetricF64Error), #[error("U64 metric error: {0}")] diff --git a/pywr-core/src/parameters/hydropower.rs b/pywr-core/src/parameters/hydropower.rs index 1d83056f..33afadf4 100644 --- a/pywr-core/src/parameters/hydropower.rs +++ b/pywr-core/src/parameters/hydropower.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -54,7 +54,7 @@ impl HydropowerTargetParameter { } } - fn head(&self, model: &Network, state: &State) -> Result { + fn head(&self, model: &Network, state: &State) -> Result { let head = if let Some(water_elevation) = &self.water_elevation { water_elevation.get_value(model, state)? - self.turbine_elevation } else { @@ -78,7 +78,7 @@ impl GeneralParameter for HydropowerTargetParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let head = self.head(model, state)?; // apply the minimum head threshold @@ -107,7 +107,7 @@ impl GeneralParameter for HydropowerTargetParameter { } if q < 0.0 { - return Err(ParameterCalculationError::Internal { + return Err(GeneralCalculationError::Internal { message: "The calculated flow is negative".into(), }); } @@ -125,7 +125,7 @@ impl GeneralParameter for HydropowerTargetParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { if let Some(actual_flow) = &self.actual_flow { let flow = actual_flow.get_value(model, state)?; // Calculate the head (the head may be negative) diff --git a/pywr-core/src/parameters/indexed_array.rs b/pywr-core/src/parameters/indexed_array.rs index b8dfd03b..1f24bbdb 100644 --- a/pywr-core/src/parameters/indexed_array.rs +++ b/pywr-core/src/parameters/indexed_array.rs @@ -1,6 +1,6 @@ use crate::metric::{MetricF64, MetricU64}; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -36,13 +36,13 @@ impl GeneralParameter for IndexedArrayParameter { network: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let index = self.index_parameter.get_value(network, state)? as usize; let metric = self .metrics .get(index) - .ok_or(ParameterCalculationError::OutOfBoundsError { + .ok_or(GeneralCalculationError::OutOfBoundsError { index, length: self.metrics.len(), axis: 0, diff --git a/pywr-core/src/parameters/interpolated.rs b/pywr-core/src/parameters/interpolated.rs index c7bb78e7..515b16de 100644 --- a/pywr-core/src/parameters/interpolated.rs +++ b/pywr-core/src/parameters/interpolated.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::linear_interpolation; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; @@ -38,7 +38,7 @@ impl GeneralParameter for InterpolatedParameter { network: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.x.get_value(network, state)?; @@ -49,7 +49,7 @@ impl GeneralParameter for InterpolatedParameter { let xp = x.get_value(network, state)?; let fp = f.get_value(network, state)?; - Ok::<(f64, f64), ParameterCalculationError>((xp, fp)) + Ok::<(f64, f64), GeneralCalculationError>((xp, fp)) }) .collect::, _>>()?; diff --git a/pywr-core/src/parameters/max.rs b/pywr-core/src/parameters/max.rs index 61a09ca7..15806a18 100644 --- a/pywr-core/src/parameters/max.rs +++ b/pywr-core/src/parameters/max.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -36,7 +36,7 @@ impl GeneralParameter for MaxParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; Ok(Some(x.max(self.threshold))) diff --git a/pywr-core/src/parameters/min.rs b/pywr-core/src/parameters/min.rs index f608e80b..70d30b65 100644 --- a/pywr-core/src/parameters/min.rs +++ b/pywr-core/src/parameters/min.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -35,7 +35,7 @@ impl GeneralParameter for MinParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let x = self.metric.get_value(model, state)?; Ok(Some(x.min(self.threshold))) } diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 7504a11d..2db9332a 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -34,38 +34,41 @@ mod vector; use std::any::Any; // Re-imports -use crate::metric::{ConstantMetricF64, ConstantMetricU64, MetricF64, MetricU64, SimpleMetricF64, SimpleMetricU64}; -use crate::network::Network; +use crate::metric::{ + ConstantMetricF64, ConstantMetricU64, MetricF64, MetricF64ResolutionError, MetricU64, MetricU64ResolutionError, + SimpleMetricF64, SimpleMetricU64, +}; +use crate::network::{Network, ResolutionMaps}; use crate::scenario::ScenarioIndex; use crate::state::{ ConstParameterValues, MultiValue, ParameterReturnValue, SetStateError, SimpleParameterValues, State, }; use crate::timestep::Timestep; pub use activation_function::ActivationFunction; -pub use aggregated::AggregatedParameter; +pub use aggregated::AggregatedParameterBuilder; pub use aggregated_index::AggregatedIndexParameter; -pub use array::{Array1Parameter, Array2Parameter}; +pub use array::{Array1ParameterBuilder, Array2ParameterBuilder}; pub use asymmetric::AsymmetricSwitchIndexParameter; -pub use constant::ConstantParameter; +pub use constant::ConstantParameterBuilder; pub use constant_scenario::ConstantScenarioParameter; pub use control_curves::{ - ApportionParameter, ControlCurveIndexParameter, ControlCurveInterpolatedParameter, ControlCurveParameter, - PiecewiseInterpolatedParameter, VolumeBetweenControlCurvesParameter, + ApportionParameter, ControlCurveIndexParameter, ControlCurveInterpolatedParameterBuilder, ControlCurveParameter, + PiecewiseInterpolatedParameterBuilder, VolumeBetweenControlCurvesParameter, }; pub use deficit::DeficitParameter; -pub use delay::DelayParameter; +pub use delay::{DelayParameter, DelayParameterBuilder}; pub use difference::DifferenceParameter; -pub use discount_factor::DiscountFactorParameter; +pub use discount_factor::{DiscountFactorParameter, DiscountFactorParameterBuilder}; pub use division::DivisionParameter; use errors::{ConstCalculationError, SimpleCalculationError}; -pub use errors::{ParameterCalculationError, ParameterSetupError}; +pub use errors::{GeneralCalculationError, ParameterSetupError}; pub use hydropower::{HydropowerTargetData, HydropowerTargetParameter}; pub use indexed_array::IndexedArrayParameter; pub use interpolate::{InterpolationError, interpolate, linear_interpolation}; pub use interpolated::InterpolatedParameter; pub use max::MaxParameter; pub use min::MinParameter; -pub use multi_threshold::MultiThresholdParameter; +pub use multi_threshold::{MultiThresholdParameter, MultiThresholdParameterBuilder}; pub use muskingum::{MuskingumInitialCondition, MuskingumParameter}; pub use negative::NegativeParameter; pub use negativemax::NegativeMaxParameter; @@ -73,18 +76,20 @@ pub use negativemin::NegativeMinParameter; pub use offset::OffsetParameter; pub use polynomial::Polynomial1DParameter; pub use profiles::{ - DailyProfileParameter, DiurnalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, RadialBasisFunction, - RbfProfileParameter, RbfProfileVariableConfig, UniformDrawdownProfileParameter, WeeklyInterpDay, - WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues, + DailyProfileParameter, DiurnalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, + MonthlyProfileParameterBuilder, RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig, + UniformDrawdownProfileParameter, WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues, }; #[cfg(feature = "pyo3")] pub use py::{ParameterInfo, PyClassParameter, PyFuncParameter}; -pub use rolling::RollingParameter; +pub use rolling::{RollingParameter, RollingParameterBuilder}; use std::fmt; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::ops::Deref; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant}; use thiserror::Error; pub use threshold::{Predicate, ThresholdParameter}; pub use vector::VectorParameter; @@ -218,7 +223,7 @@ impl PartialEq for GeneralParameterIndex { impl Eq for GeneralParameterIndex {} impl GeneralParameterIndex { - pub fn new(idx: usize) -> Self { + fn new(idx: usize) -> Self { Self { idx, phantom: PhantomData, @@ -396,7 +401,7 @@ impl ParameterIndex { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParameterName { name: String, // Optional sub-name for parameters that are part of multi-parameter groups @@ -709,7 +714,7 @@ pub trait GeneralParameter: Parameter { #[allow(unused_variables)] model: &Network, #[allow(unused_variables)] state: &State, #[allow(unused_variables)] internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { Ok(None) } @@ -720,7 +725,7 @@ pub trait GeneralParameter: Parameter { #[allow(unused_variables)] model: &Network, #[allow(unused_variables)] state: &State, #[allow(unused_variables)] internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { Ok(None) } @@ -731,6 +736,151 @@ pub trait GeneralParameter: Parameter { fn as_parameter(&self) -> &dyn Parameter; } +#[derive(Debug, Error)] +pub enum ParameterBuildError { + #[error("Scenario group not found: {name}")] + ScenarioGroupNotFound { name: String }, + #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] + ResolveMetricF64Error { + attr: String, + #[source] + source: MetricF64ResolutionError, + }, + #[error("Could not resolve u64 metric for `{attr}` attribute: {source}")] + ResolveMetricU64Error { + attr: String, + #[source] + source: MetricU64ResolutionError, + }, +} + +pub enum BuiltParameter { + General(Box>), + Simple(Box>), + Const(Box>), +} + +pub enum MaybeBuiltParameter { + Built(BuiltParameter), + Retry(Box>), +} + +impl From> for MaybeBuiltParameter { + fn from(built: BuiltParameter) -> Self { + Self::Built(built) + } +} + +pub trait ParameterBuilder { + /// The name of the parameter + fn name(&self) -> &ParameterName; + /// Construct a parameter from the builder. + /// + /// If the construction requires a parameter that is not yet available. This method + /// should return the builder via one of the parameter not found variants of + /// [`ParameterBuildError`] error. This will allow the parameter collection builder to retry + /// the build. + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, ParameterBuildError>; +} + +/// Resolve a single `UnresolvedMetricF64` into a `MetricF64` inside a +/// `ParameterBuilder::build` implementation. +/// +/// On `ParameterNotFound`, the macro early-returns `Ok(MaybeBuiltParameter::Retry($self))` +/// so the builder can be retried after more parameters are added. Any other +/// `MetricF64ResolutionError` is wrapped in `ParameterBuildError::ResolveMetricF64Error` +/// (tagged with `$attr`) and early-returned. +/// +/// # Example +/// ```ignore +/// let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); +/// ``` +#[macro_export] +macro_rules! resolve_metric_f64 { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => { + match $unresolved.resolve($maps) { + Ok(m) => m, + Err(err) => { + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + } + }; +} + +/// Resolve a single `UnresolvedMetricU64` into a `MetricU64` inside a +/// `ParameterBuilder::build` implementation. +/// +/// On `ParameterNotFound`, the macro early-returns `Ok(MaybeBuiltParameter::Retry($self))` +/// so the builder can be retried after more parameters are added. Any other +/// `MetricU64ResolutionError` is wrapped in `ParameterBuildError::ResolveMetricU64Error` +/// (tagged with `$attr`) and early-returned. +/// +/// # Example +/// ```ignore +/// let metric = resolve_metric_u64!(self, self.metric, resolution_maps, "metric"); +/// ``` +#[macro_export] +macro_rules! resolve_metric_u64 { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => { + match $unresolved.resolve($maps) { + Ok(m) => m, + Err(err) => { + return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricU64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + } + }; +} + +/// Resolve a slice/`Vec` of `UnresolvedMetricF64` into a `Vec` inside a +/// `ParameterBuilder::build` implementation. Same retry / error semantics as +/// [`resolve_metric_f64!`]. +/// +/// `$unresolved` must be something that can be iterated as `&UnresolvedMetricF64` +/// and on which `.len()` is callable (e.g. `&self.values`, `self.control_curves.as_slice()`). +/// +/// # Example +/// ```ignore +/// let control_curves = +/// resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); +/// ``` +#[macro_export] +macro_rules! resolve_metric_f64_vec { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + let unresolved = $unresolved; + let mut resolved = Vec::with_capacity(unresolved.len()); + for m in unresolved.iter() { + match m.resolve($maps) { + Ok(m) => resolved.push(m), + Err(err) => { + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + } + } + resolved + }}; +} + /// A trait that defines a component that produces a value each time-step. /// /// The trait is generic over the type of the value produced. @@ -1009,8 +1159,174 @@ pub enum ParameterCollectionSimpleCalculationError { }, } +// Unique ID for each parameter collection. +static PARAMETER_COLLECTION_ID: AtomicU64 = AtomicU64::new(0); + +#[derive(Default, Copy, Clone)] +pub struct ParameterTiming { + before: Duration, + after: Duration, +} + +impl ParameterTiming { + /// Time spent in the before method of the component. + pub fn before(&self) -> Duration { + self.before + } + + /// Time spent in the "after" method of the component. + pub fn after(&self) -> Duration { + self.after + } + + /// Total time spent in calculation and after methods. + pub fn total(&self) -> Duration { + self.before + self.after + } +} + +/// Timing accumulator for parameters +#[derive(Clone)] +pub struct ParameterTimings { + general_f64: Vec, + general_u64: Vec, + general_multi: Vec, + id: u64, +} + +impl ParameterTimings { + pub fn from_collection(collection: &ParameterCollection) -> Self { + Self { + general_f64: vec![ParameterTiming::default(); collection.general_f64.len()], + general_u64: vec![ParameterTiming::default(); collection.general_u64.len()], + general_multi: vec![ParameterTiming::default(); collection.general_multi.len()], + id: collection.id, + } + } + + /// Return the `n` slowest f64 parameter indices and their timings. + fn slowest_f64(&self, n: usize) -> Vec<(GeneralParameterIndex, ParameterTiming)> { + let mut timings = self + .general_f64 + .iter() + .enumerate() + .map(|(idx, timing)| (GeneralParameterIndex::new(idx), *timing)) + .collect::>(); + + timings.sort_by_key(|(_, timing)| timing.total()); + + timings.into_iter().rev().take(n).collect() + } + + /// Return the `n` slowest u64 parameter indices and their timings. + fn slowest_u64(&self, n: usize) -> Vec<(GeneralParameterIndex, ParameterTiming)> { + let mut timings = self + .general_u64 + .iter() + .enumerate() + .map(|(idx, timing)| (GeneralParameterIndex::new(idx), *timing)) + .collect::>(); + + timings.sort_by_key(|(_, timing)| timing.total()); + + timings.into_iter().rev().take(n).collect() + } + + /// Return the `n` slowest multi parameter indices and their timings. + fn slowest_multi(&self, n: usize) -> Vec<(GeneralParameterIndex, ParameterTiming)> { + let mut timings = self + .general_multi + .iter() + .enumerate() + .map(|(idx, timing)| (GeneralParameterIndex::new(idx), *timing)) + .collect::>(); + + timings.sort_by_key(|(_, timing)| timing.total()); + + timings.into_iter().rev().take(n).collect() + } + pub fn slowest_parameters(&self, n: usize) -> Vec<(GeneralParameterType, ParameterTiming)> { + let f64 = self + .slowest_f64(n) + .into_iter() + .map(|(idx, timing)| (GeneralParameterType::from(idx), timing)); + + let u64 = self + .slowest_u64(n) + .into_iter() + .map(|(idx, timing)| (GeneralParameterType::from(idx), timing)); + + let multi = self + .slowest_multi(n) + .into_iter() + .map(|(idx, timing)| (GeneralParameterType::from(idx), timing)); + + let mut all = f64 + .chain(u64) + .chain(multi) + .collect::>(); + all.sort_by_key(|(_, timing)| timing.total()); + all.into_iter().rev().take(n).collect() + } + + pub fn slowest_parameters_named( + &self, + n: usize, + collection: &ParameterCollection, + ) -> Vec<(ParameterName, ParameterTiming)> { + if self.id != collection.id { + todo!("Return error with ID mismatch"); + } + + // SAFETY: The id of the timings must match the id of the collection to ensure that the + // indices are correct. This is checked above and should be guaranteed by construction. + unsafe { + self.slowest_parameters(n) + .into_iter() + .map(|(idx, timing)| (collection.get_general_unchecked(idx).name().clone(), timing)) + .collect() + } + } +} + +#[derive(Error, Debug)] +#[error("Error calculating general parameter '{name}': {source}")] +pub enum ParameterCollectionGeneralCalculationError { + #[error("General parameter F64 index '{0}' not found in collection")] + F64IndexNotFound(GeneralParameterIndex), + #[error("General parameter U64 index '{0}' not found in collection")] + U64IndexNotFound(GeneralParameterIndex), + #[error("General parameter Multi index '{0}' not found in collection")] + MultiIndexNotFound(GeneralParameterIndex), + #[error("Error calculating general parameter '{name}': {source}")] + CalculationError { + name: ParameterName, + #[source] + source: Box, + }, + #[error("Error setting state for general F64 parameter '{name}': {source}")] + F64SetStateError { + name: ParameterName, + #[source] + source: SetStateError>, + }, + #[error("Error setting state for general U64 parameter '{name}': {source}")] + U64SetStateError { + name: ParameterName, + #[source] + source: SetStateError>, + }, + #[error("Error setting state for general Multi parameter '{name}': {source}")] + MultiSetStateError { + name: ParameterName, + #[source] + source: SetStateError>, + }, + #[error("The timing data was created with from a different parameter collection. ")] + TimingsFromAnotherCollection, +} + /// A collection of parameters that return different types. -#[derive(Default)] pub struct ParameterCollection { constant_f64: Vec>>, constant_u64: Vec>>, @@ -1022,11 +1338,31 @@ pub struct ParameterCollection { simple_multi: Vec>>, simple_resolve_order: Vec, - // There is no resolve order for general parameters as they are resolved at a model - // level with other component types (e.g. nodes). general_f64: Vec>>, general_u64: Vec>>, general_multi: Vec>>, + general_resolve_order: Vec, + id: u64, +} + +impl Default for ParameterCollection { + fn default() -> Self { + Self { + constant_f64: Vec::new(), + constant_u64: Vec::new(), + constant_multi: Vec::new(), + constant_resolve_order: Vec::new(), + simple_f64: Vec::new(), + simple_u64: Vec::new(), + simple_multi: Vec::new(), + simple_resolve_order: Vec::new(), + general_f64: Vec::new(), + general_u64: Vec::new(), + general_multi: Vec::new(), + general_resolve_order: Vec::new(), + id: PARAMETER_COLLECTION_ID.fetch_add(1, Ordering::Relaxed), + } + } } impl ParameterCollection { @@ -1197,72 +1533,16 @@ impl ParameterCollection { || self.get_multi_index_by_name(name).is_some() } - /// Add a [`GeneralParameter`] parameter to the collection. - /// - /// This function will add attempt to simplify the parameter and add it to the simple or - /// constant parameter list. If the parameter cannot be simplified it will be added to the - /// general parameter list. - pub fn add_general_f64( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - match parameter.try_into_simple() { - Some(simple) => self.add_simple_f64(simple), - None => { - let index = GeneralParameterIndex::new(self.general_f64.len()); - self.general_f64.push(parameter); - Ok(index.into()) + unsafe fn get_general_unchecked(&self, index: GeneralParameterType) -> &dyn Parameter { + unsafe { + match index { + GeneralParameterType::Parameter(idx) => self.general_f64.get_unchecked(idx.idx).as_parameter(), + GeneralParameterType::Index(idx) => self.general_u64.get_unchecked(idx.idx).as_parameter(), + GeneralParameterType::Multi(idx) => self.general_multi.get_unchecked(idx.idx).as_parameter(), } } } - pub fn add_simple_f64( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - match parameter.try_into_const() { - Some(constant) => self.add_const_f64(constant), - None => { - let index = SimpleParameterIndex::new(self.simple_f64.len()); - - self.simple_f64.push(parameter); - self.simple_resolve_order.push(SimpleParameterType::Parameter(index)); - - Ok(index.into()) - } - } - } - - pub fn add_const_f64( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - let index = ConstParameterIndex::new(self.constant_f64.len()); - - self.constant_f64.push(parameter); - self.constant_resolve_order.push(ConstParameterType::Parameter(index)); - - Ok(index.into()) - } - pub fn get_f64(&self, index: ParameterIndex) -> Option<&dyn Parameter> { match index { ParameterIndex::Const(idx) => self.constant_f64.get(*idx.deref()).map(|p| p.as_parameter()), @@ -1306,67 +1586,6 @@ impl ParameterCollection { } } - pub fn add_general_u64( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - match parameter.try_into_simple() { - Some(simple) => self.add_simple_u64(simple), - None => { - let index = GeneralParameterIndex::new(self.general_u64.len()); - self.general_u64.push(parameter); - Ok(index.into()) - } - } - } - - pub fn add_simple_u64( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - match parameter.try_into_const() { - Some(constant) => self.add_const_u64(constant), - None => { - let index = SimpleParameterIndex::new(self.simple_u64.len()); - - self.simple_u64.push(parameter); - self.simple_resolve_order.push(SimpleParameterType::Index(index)); - - Ok(index.into()) - } - } - } - - pub fn add_const_u64( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - let index = ConstParameterIndex::new(self.constant_u64.len()); - - self.constant_u64.push(parameter); - self.constant_resolve_order.push(ConstParameterType::Index(index)); - - Ok(index.into()) - } - pub fn get_u64(&self, index: ParameterIndex) -> Option<&dyn Parameter> { match index { ParameterIndex::Const(idx) => self.constant_u64.get(*idx.deref()).map(|p| p.as_parameter()), @@ -1410,61 +1629,6 @@ impl ParameterCollection { } } - pub fn add_general_multi( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - match parameter.try_into_simple() { - Some(simple) => self.add_simple_multi(simple).map(|idx| idx.into()), - None => { - let index = GeneralParameterIndex::new(self.general_multi.len()); - self.general_multi.push(parameter); - Ok(index.into()) - } - } - } - - pub fn add_simple_multi( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - let index = SimpleParameterIndex::new(self.simple_multi.len()); - - self.simple_multi.push(parameter); - self.simple_resolve_order.push(SimpleParameterType::Multi(index)); - - Ok(index) - } - - pub fn add_const_multi( - &mut self, - parameter: Box>, - ) -> Result, ParameterCollectionError> { - if self.has_name(parameter.name()) { - return Err(ParameterCollectionError::NameAlreadyExists( - parameter.meta().name.to_string(), - )); - } - - let index = ConstParameterIndex::new(self.constant_multi.len()); - - self.constant_multi.push(parameter); - self.constant_resolve_order.push(ConstParameterType::Multi(index)); - - Ok(index) - } pub fn get_multi(&self, index: &ParameterIndex) -> Option<&dyn Parameter> { match index { ParameterIndex::Const(idx) => self.constant_multi.get(*idx.deref()).map(|p| p.as_parameter()), @@ -1511,6 +1675,245 @@ impl ParameterCollection { } } + pub fn compute_general( + &self, + timestep: &Timestep, + scenario_index: &ScenarioIndex, + network: &Network, + state: &mut State, + internal_states: &mut ParameterStates, + mut timings: Option<&mut ParameterTimings>, + ) -> Result<(), ParameterCollectionGeneralCalculationError> { + if let Some(timings) = timings.as_deref() { + if timings.id != self.id { + return Err(ParameterCollectionGeneralCalculationError::TimingsFromAnotherCollection); + } + } + + for p in &self.general_resolve_order { + let start = Instant::now(); + match p { + GeneralParameterType::Parameter(idx) => { + // Find the parameter itself + let p = self + .general_f64 + .get(*idx.deref()) + .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; + // .. and its internal state + let internal_state = internal_states + .get_general_mut_f64_state(*idx) + .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; + + let value = p + .before(timestep, scenario_index, network, state, internal_state) + .map_err(|source| ParameterCollectionGeneralCalculationError::CalculationError { + name: p.name().clone(), + source: Box::new(source), + })?; + + state + .set_general_parameter_value_before(*idx, value) + .map_err(|source| ParameterCollectionGeneralCalculationError::F64SetStateError { + name: p.name().clone(), + source, + })?; + + if let Some(timings) = timings.as_deref_mut() { + unsafe { + timings.general_f64.get_unchecked_mut(*idx.deref()).before += start.elapsed(); + } + } + } + GeneralParameterType::Index(idx) => { + // Find the parameter itself + let p = self + .general_u64 + .get(*idx.deref()) + .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; + // ... and its internal state + let internal_state = internal_states + .get_general_mut_u64_state(*idx) + .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; + + let value = p + .before(timestep, scenario_index, network, state, internal_state) + .map_err(|source| ParameterCollectionGeneralCalculationError::CalculationError { + name: p.name().clone(), + source: Box::new(source), + })?; + + state + .set_general_parameter_index_before(*idx, value) + .map_err(|source| ParameterCollectionGeneralCalculationError::U64SetStateError { + name: p.name().clone(), + source, + })?; + + if let Some(timings) = timings.as_deref_mut() { + unsafe { + timings.general_u64.get_unchecked_mut(*idx.deref()).before += start.elapsed(); + } + } + } + GeneralParameterType::Multi(idx) => { + // Find the parameter itself + let p = self + .general_multi + .get(*idx.deref()) + .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; + // ... and its internal state + let internal_state = internal_states + .get_general_mut_multi_state(*idx) + .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; + + let value = p + .before(timestep, scenario_index, network, state, internal_state) + .map_err(|source| ParameterCollectionGeneralCalculationError::CalculationError { + name: p.name().clone(), + source: Box::new(source), + })?; + + state + .set_general_multi_parameter_value_before(*idx, value) + .map_err( + |source| ParameterCollectionGeneralCalculationError::MultiSetStateError { + name: p.name().clone(), + source, + }, + )?; + + if let Some(timings) = timings.as_deref_mut() { + unsafe { + timings.general_multi.get_unchecked_mut(*idx.deref()).before += start.elapsed(); + } + } + } + } + } + + Ok(()) + } + + /// Perform the after step for general parameters. + pub fn after_general( + &self, + timestep: &Timestep, + scenario_index: &ScenarioIndex, + network: &Network, + state: &mut State, + internal_states: &mut ParameterStates, + mut timings: Option<&mut ParameterTimings>, + ) -> Result<(), ParameterCollectionGeneralCalculationError> { + if let Some(timings) = timings.as_deref() { + if timings.id != self.id { + return Err(ParameterCollectionGeneralCalculationError::TimingsFromAnotherCollection); + } + } + + for p in &self.general_resolve_order { + let start = Instant::now(); + match p { + GeneralParameterType::Parameter(idx) => { + // Find the parameter itself + let p = self + .general_f64 + .get(*idx.deref()) + .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; + // .. and its internal state + let internal_state = internal_states + .get_general_mut_f64_state(*idx) + .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; + + let value = p + .after(timestep, scenario_index, network, state, internal_state) + .map_err(|source| ParameterCollectionGeneralCalculationError::CalculationError { + name: p.name().clone(), + source: Box::new(source), + })?; + + state.set_general_parameter_value_after(*idx, value).map_err(|source| { + ParameterCollectionGeneralCalculationError::F64SetStateError { + name: p.name().clone(), + source, + } + })?; + + if let Some(timings) = timings.as_deref_mut() { + unsafe { + timings.general_f64.get_unchecked_mut(*idx.deref()).after += start.elapsed(); + } + } + } + GeneralParameterType::Index(idx) => { + // Find the parameter itself + let p = self + .general_u64 + .get(*idx.deref()) + .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; + // .. and its internal state + let internal_state = internal_states + .get_general_mut_u64_state(*idx) + .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; + + let value = p + .after(timestep, scenario_index, network, state, internal_state) + .map_err(|source| ParameterCollectionGeneralCalculationError::CalculationError { + name: p.name().clone(), + source: Box::new(source), + })?; + + state.set_general_parameter_index_after(*idx, value).map_err(|source| { + ParameterCollectionGeneralCalculationError::U64SetStateError { + name: p.name().clone(), + source, + } + })?; + + if let Some(timings) = timings.as_deref_mut() { + unsafe { + timings.general_u64.get_unchecked_mut(*idx.deref()).after += start.elapsed(); + } + } + } + GeneralParameterType::Multi(idx) => { + // Find the parameter itself + let p = self + .general_multi + .get(*idx.deref()) + .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; + // .. and its internal state + let internal_state = internal_states + .get_general_mut_multi_state(*idx) + .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; + + let value = p + .after(timestep, scenario_index, network, state, internal_state) + .map_err(|source| ParameterCollectionGeneralCalculationError::CalculationError { + name: p.name().clone(), + source: Box::new(source), + })?; + + state + .set_general_multi_parameter_value_after(*idx, value) + .map_err( + |source| ParameterCollectionGeneralCalculationError::MultiSetStateError { + name: p.name().clone(), + source, + }, + )?; + + if let Some(timings) = timings.as_deref_mut() { + unsafe { + timings.general_multi.get_unchecked_mut(*idx.deref()).after += start.elapsed(); + } + } + } + } + } + + Ok(()) + } + pub fn compute_simple( &self, timestep: &Timestep, @@ -1793,16 +2196,290 @@ impl ParameterCollection { } } +#[derive(Debug, Error)] +pub enum ParameterCollectionBuilderError { + #[error("Duplicate parameter `{name}` found.")] + DuplicateParameterName { name: ParameterName }, + #[error("Error building parameter `{name}`: {source}")] + ParameterBuildError { + name: ParameterName, + #[source] + source: ParameterBuildError, + }, + #[error("Circulate parameters found: {names:?}")] + CircularParameterReference { names: Vec }, +} + +/// A builder for [`ParameterCollection`] that allows adding parameters without worrying about the +/// internal structure of the collection. +#[derive(Default)] +pub struct ParameterCollectionBuilder { + pub f64: Vec>>, + pub u64: Vec>>, + pub multi: Vec>>, +} + +impl ParameterCollectionBuilder { + pub fn f64(&mut self, value: Box>) -> &mut Self { + self.f64.push(value); + self + } + + pub fn u64(&mut self, value: Box>) -> &mut Self { + self.u64.push(value); + self + } + + pub fn multi(&mut self, value: Box>) -> &mut Self { + self.multi.push(value); + self + } + + /// Returns true if the builder is empty. + fn is_empty(&self) -> bool { + self.f64.is_empty() && self.u64.is_empty() && self.multi.is_empty() + } + + /// Total number of parameter builders in the collection builder. + fn len(&self) -> usize { + self.f64.len() + self.u64.len() + self.multi.len() + } + + pub fn build( + mut self, + resolution_maps: &mut ResolutionMaps, + ) -> Result { + let mut collection = ParameterCollection::default(); + + let mut num_unbuilt = self.len(); + + while !self.is_empty() { + let mut failed_f64 = Vec::new(); + let mut failed_u64 = Vec::new(); + let mut failed_multi = Vec::new(); + + for p in self.f64.into_iter() { + let name = p.name().clone(); + + if collection.has_name(&name) { + return Err(ParameterCollectionBuilderError::DuplicateParameterName { name }); + } + + match p.build(resolution_maps) { + Ok(maybe) => { + match maybe { + MaybeBuiltParameter::Built(built) => { + // Parameter successfully built. Let's add it to the collection, resolve order and resolution map. + let idx = match built { + BuiltParameter::General(p) => { + let idx = GeneralParameterIndex::new(collection.general_f64.len()); + collection.general_f64.push(p); + collection + .general_resolve_order + .push(GeneralParameterType::Parameter(idx)); + ParameterIndex::General(idx) + } + BuiltParameter::Simple(p) => { + let idx = SimpleParameterIndex::new(collection.simple_f64.len()); + collection.simple_f64.push(p); + collection + .simple_resolve_order + .push(SimpleParameterType::Parameter(idx)); + ParameterIndex::Simple(idx) + } + BuiltParameter::Const(p) => { + let idx = ConstParameterIndex::new(collection.constant_f64.len()); + collection.constant_f64.push(p); + collection + .constant_resolve_order + .push(ConstParameterType::Parameter(idx)); + + ParameterIndex::Const(idx) + } + }; + + resolution_maps.parameters_f64.insert(name, idx); + } + MaybeBuiltParameter::Retry(builder) => { + failed_f64.push(builder); + } + } + } + Err(err) => return Err(ParameterCollectionBuilderError::ParameterBuildError { name, source: err }), + } + } + + for p in self.u64.into_iter() { + let name = p.name().clone(); + + if collection.has_name(&name) { + return Err(ParameterCollectionBuilderError::DuplicateParameterName { name }); + } + + match p.build(resolution_maps) { + Ok(maybe) => { + match maybe { + MaybeBuiltParameter::Built(built) => { + // Parameter successfully built. Let's add it to the collection, resolve order and resolution map. + let idx = match built { + BuiltParameter::General(p) => { + let idx = GeneralParameterIndex::new(collection.general_u64.len()); + collection.general_u64.push(p); + collection.general_resolve_order.push(GeneralParameterType::Index(idx)); + ParameterIndex::General(idx) + } + BuiltParameter::Simple(p) => { + let idx = SimpleParameterIndex::new(collection.simple_u64.len()); + collection.simple_u64.push(p); + collection.simple_resolve_order.push(SimpleParameterType::Index(idx)); + ParameterIndex::Simple(idx) + } + BuiltParameter::Const(p) => { + let idx = ConstParameterIndex::new(collection.constant_u64.len()); + collection.constant_u64.push(p); + collection.constant_resolve_order.push(ConstParameterType::Index(idx)); + + ParameterIndex::Const(idx) + } + }; + + resolution_maps.parameters_u64.insert(name, idx); + } + MaybeBuiltParameter::Retry(builder) => { + failed_u64.push(builder); + } + } + } + Err(err) => return Err(ParameterCollectionBuilderError::ParameterBuildError { name, source: err }), + } + } + + for p in self.multi.into_iter() { + let name = p.name().clone(); + + if collection.has_name(&name) { + return Err(ParameterCollectionBuilderError::DuplicateParameterName { name }); + } + + match p.build(resolution_maps) { + Ok(maybe) => { + match maybe { + MaybeBuiltParameter::Built(built) => { + // Parameter successfully built. Let's add it to the collection, resolve order and resolution map. + let idx = match built { + BuiltParameter::General(p) => { + let idx = GeneralParameterIndex::new(collection.general_multi.len()); + collection.general_multi.push(p); + collection.general_resolve_order.push(GeneralParameterType::Multi(idx)); + ParameterIndex::General(idx) + } + BuiltParameter::Simple(p) => { + let idx = SimpleParameterIndex::new(collection.simple_multi.len()); + collection.simple_multi.push(p); + collection.simple_resolve_order.push(SimpleParameterType::Multi(idx)); + ParameterIndex::Simple(idx) + } + BuiltParameter::Const(p) => { + let idx = ConstParameterIndex::new(collection.constant_multi.len()); + collection.constant_multi.push(p); + collection.constant_resolve_order.push(ConstParameterType::Multi(idx)); + + ParameterIndex::Const(idx) + } + }; + + resolution_maps.parameters_multi.insert(name, idx); + } + MaybeBuiltParameter::Retry(builder) => { + failed_multi.push(builder); + } + } + } + Err(err) => return Err(ParameterCollectionBuilderError::ParameterBuildError { name, source: err }), + } + } + + self.f64 = failed_f64; + self.u64 = failed_u64; + self.multi = failed_multi; + + let new_total = self.len(); + + if num_unbuilt == new_total { + let failed_names = self + .f64 + .iter() + .map(|b| b.name().clone()) + .chain( + self.u64 + .iter() + .map(|b| b.name().clone()) + .chain(self.multi.iter().map(|b| b.name().clone())), + ) + .collect::>(); + + return Err(ParameterCollectionBuilderError::CircularParameterReference { names: failed_names }); + } else { + num_unbuilt = new_total; + } + } + + Ok(collection) + } +} + #[cfg(test)] mod tests { use super::{ - ConstParameter, GeneralParameter, Parameter, ParameterCalculationError, ParameterCollection, ParameterMeta, + BuiltParameter, ConstParameter, GeneralCalculationError, GeneralParameter, MaybeBuiltParameter, Parameter, + ParameterBuildError, ParameterBuilder, ParameterCollectionBuilder, ParameterMeta, ParameterName, ParameterState, SimpleParameter, }; + use crate::network::ResolutionMaps; use crate::parameters::errors::{ConstCalculationError, SimpleCalculationError}; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, MultiValue}; + struct TestParameterBuilder { + meta: ParameterMeta, + } + + impl Default for TestParameterBuilder { + fn default() -> Self { + Self { + meta: ParameterMeta::new("test-parameter".into()), + } + } + } + + impl ParameterBuilder for TestParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = TestParameter { meta: self.meta }; + Ok(MaybeBuiltParameter::Built(BuiltParameter::Const(Box::new(p)))) + } + } + + impl ParameterBuilder for TestParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = TestParameter { meta: self.meta }; + Ok(MaybeBuiltParameter::Built(BuiltParameter::Const(Box::new(p)))) + } + } + /// Parameter for testing purposes struct TestParameter { meta: ParameterMeta, @@ -1898,7 +2575,7 @@ mod tests { _model: &crate::network::Network, _state: &crate::state::State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { Ok(Some(T::from(1))) } @@ -1915,7 +2592,7 @@ mod tests { _model: &crate::network::Network, _state: &crate::state::State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { Ok(Some(MultiValue::default())) } @@ -1927,39 +2604,25 @@ mod tests { /// Test naming constraints on parameter collection. #[test] fn test_parameter_collection_name_constraints() { - let mut collection = ParameterCollection::default(); - - let ret = collection.add_const_f64(Box::new(TestParameter::default())); - assert!(ret.is_ok()); - - assert!(collection.has_name(&"test-parameter".into())); - - // Try to add a parameter with the same name - let ret = collection.add_const_f64(Box::new(TestParameter::default())); - assert!(ret.is_err()); + let mut collection = ParameterCollectionBuilder::default(); - let ret = collection.add_simple_f64(Box::new(TestParameter::default())); - assert!(ret.is_err()); + collection.f64(Box::new(TestParameterBuilder::default())); + collection.f64(Box::new(TestParameterBuilder::default())); - let ret = collection.add_general_f64(Box::new(TestParameter::default())); - assert!(ret.is_err()); + assert!(collection.build(&mut ResolutionMaps::default()).is_err()); - let ret = collection.add_const_u64(Box::new(TestParameter::default())); - assert!(ret.is_err()); + let mut collection = ParameterCollectionBuilder::default(); - let ret = collection.add_simple_u64(Box::new(TestParameter::default())); - assert!(ret.is_err()); + collection.u64(Box::new(TestParameterBuilder::default())); + collection.u64(Box::new(TestParameterBuilder::default())); - let ret = collection.add_general_u64(Box::new(TestParameter::default())); - assert!(ret.is_err()); + assert!(collection.build(&mut ResolutionMaps::default()).is_err()); - let ret = collection.add_const_multi(Box::new(TestParameter::default())); - assert!(ret.is_err()); + let mut collection = ParameterCollectionBuilder::default(); - let ret = collection.add_simple_multi(Box::new(TestParameter::default())); - assert!(ret.is_err()); + collection.f64(Box::new(TestParameterBuilder::default())); + collection.u64(Box::new(TestParameterBuilder::default())); - let ret = collection.add_general_multi(Box::new(TestParameter::default())); - assert!(ret.is_err()); + assert!(collection.build(&mut ResolutionMaps::default()).is_err()); } } diff --git a/pywr-core/src/parameters/multi_threshold.rs b/pywr-core/src/parameters/multi_threshold.rs index 5731cb76..35148821 100644 --- a/pywr-core/src/parameters/multi_threshold.rs +++ b/pywr-core/src/parameters/multi_threshold.rs @@ -1,12 +1,14 @@ -use crate::metric::MetricF64; -use crate::network::Network; -use crate::parameters::errors::{ParameterCalculationError, ParameterSetupError}; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ - GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, Predicate, downcast_internal_state_mut, + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, Predicate, downcast_internal_state_mut, }; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_f64_vec}; pub struct MultiThresholdParameter { meta: ParameterMeta, @@ -16,24 +18,6 @@ pub struct MultiThresholdParameter { ratchet: bool, } -impl MultiThresholdParameter { - pub fn new( - name: ParameterName, - metric: MetricF64, - thresholds: &[MetricF64], - predicate: Predicate, - ratchet: bool, - ) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - thresholds: thresholds.to_vec(), - predicate, - ratchet, - } - } -} - impl Parameter for MultiThresholdParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -58,7 +42,7 @@ impl GeneralParameter for MultiThresholdParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let previous_max = downcast_internal_state_mut::(internal_state); @@ -96,18 +80,75 @@ impl GeneralParameter for MultiThresholdParameter { } } +pub struct MultiThresholdParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + thresholds: Vec, + predicate: Predicate, + ratchet: bool, +} + +impl MultiThresholdParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, predicate: Predicate) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + thresholds: Vec::new(), + predicate, + ratchet: false, + } + } + + /// Enable ratchet + pub fn ratchet(&mut self) -> &mut Self { + self.ratchet = true; + self + } + + /// Add a threshold to the builder. The thresholds should be added in order. + pub fn threshold(&mut self, threshold: UnresolvedMetricF64) -> &mut Self { + self.thresholds.push(threshold); + self + } +} + +impl ParameterBuilder for MultiThresholdParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metrics"); + let thresholds = resolve_metric_f64_vec!(self, &self.thresholds, resolution_maps, "thresholds"); + + let p = MultiThresholdParameter { + meta: self.meta, + metric, + thresholds, + predicate: self.predicate, + ratchet: self.ratchet, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(MaybeBuiltParameter::Built(bp)) + } +} + #[cfg(test)] mod tests { - use super::MultiThresholdParameter; - use crate::metric::MetricF64; - use crate::parameters::{Array1Parameter, Predicate}; + use super::MultiThresholdParameterBuilder; + use crate::metric::UnresolvedMetricF64; + use crate::parameters::{Array1ParameterBuilder, Predicate}; use crate::test_utils::{run_and_assert_parameter_u64, simple_model}; use ndarray::{Array1, Array2, Axis, concatenate}; /// Basic functional test of the `MultiThresholdParameter` parameter. #[test] fn test_multi_threshold() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); // Create an artificial volume series to use for the delay test let v1 = Array1::linspace(1.0, 0.0, 11); @@ -115,36 +156,31 @@ mod tests { let volumes = concatenate![Axis(0), v1, v2]; - let volume = Array1Parameter::new("test-x".into(), volumes.clone(), None); - - let volume_idx = model.network_mut().add_simple_parameter(Box::new(volume)).unwrap(); + let volume = Array1ParameterBuilder::new("test-x".into(), volumes.clone()); - let t1: MetricF64 = 0.75.into(); - let t2: MetricF64 = 0.5.into(); + model_builder.network_builder().parameters().f64(Box::new(volume)); - let thresholds = vec![t1, t2]; - - let parameter = MultiThresholdParameter::new( + let mut parameter = MultiThresholdParameterBuilder::new( "test-parameter".into(), - volume_idx.into_metric_f64_before(), - &thresholds, + UnresolvedMetricF64::new_parameter_before("test-x"), Predicate::GreaterThan, - false, ); + parameter.threshold(0.75.into()).threshold(0.5.into()); + // The multi-threshold parameter should return the index of the first threshold that is met. // In this case, the thresholds are 0.75 and 0.5, let expected_values: Array1 = vec![0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0].into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter_u64(&mut model, Box::new(parameter), expected_values); + run_and_assert_parameter_u64(model_builder, Box::new(parameter), expected_values); } /// Basic functional test of the `MultiThresholdParameter` parameter with ratchet enabled. #[test] fn test_multi_threshold_with_ratchet() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); // Create an artificial volume series to use for the delay test let v1 = Array1::linspace(1.0, 0.0, 11); @@ -152,23 +188,18 @@ mod tests { let volumes = concatenate![Axis(0), v1, v2]; - let volume = Array1Parameter::new("test-x".into(), volumes.clone(), None); - - let volume_idx = model.network_mut().add_simple_parameter(Box::new(volume)).unwrap(); + let volume = Array1ParameterBuilder::new("test-x".into(), volumes.clone()); - let t1: MetricF64 = 0.75.into(); - let t2: MetricF64 = 0.5.into(); + model_builder.network_builder().parameters().f64(Box::new(volume)); - let thresholds = vec![t1, t2]; - - let parameter = MultiThresholdParameter::new( + let mut parameter = MultiThresholdParameterBuilder::new( "test-parameter".into(), - volume_idx.into_metric_f64_before(), - &thresholds, + UnresolvedMetricF64::new_parameter_before("test-x"), Predicate::GreaterThan, - true, ); + parameter.threshold(0.75.into()).threshold(0.5.into()).ratchet(); + // The multi-threshold parameter should return the index of the first threshold that is met, // but with ratchet enabled, it should not decrease the value once it has been set. // In this case, the thresholds are 0.75 and 0.5, @@ -176,6 +207,6 @@ mod tests { let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter_u64(&mut model, Box::new(parameter), expected_values); + run_and_assert_parameter_u64(model_builder, Box::new(parameter), expected_values); } } diff --git a/pywr-core/src/parameters/muskingum.rs b/pywr-core/src/parameters/muskingum.rs index 57ed1e43..26c2783b 100644 --- a/pywr-core/src/parameters/muskingum.rs +++ b/pywr-core/src/parameters/muskingum.rs @@ -1,7 +1,7 @@ use crate::metric::MetricF64; use crate::network::Network; use crate::parameters::{ - GeneralParameter, Parameter, ParameterCalculationError, ParameterMeta, ParameterName, ParameterState, + GeneralCalculationError, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, }; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, State}; @@ -76,7 +76,7 @@ impl GeneralParameter for MuskingumParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let weight = self.weight.get_value(model, state)?; let travel_time = self.travel_time.get_value(model, state)?; diff --git a/pywr-core/src/parameters/negative.rs b/pywr-core/src/parameters/negative.rs index a308cc95..7e6050ee 100644 --- a/pywr-core/src/parameters/negative.rs +++ b/pywr-core/src/parameters/negative.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -34,7 +34,7 @@ impl GeneralParameter for NegativeParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; Ok(Some(-x)) diff --git a/pywr-core/src/parameters/negativemax.rs b/pywr-core/src/parameters/negativemax.rs index ba27f6fd..d2ef9082 100644 --- a/pywr-core/src/parameters/negativemax.rs +++ b/pywr-core/src/parameters/negativemax.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -35,7 +35,7 @@ impl GeneralParameter for NegativeMaxParameter { network: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let x = -self.metric.get_value(network, state)?; Ok(Some(x.max(self.threshold))) } diff --git a/pywr-core/src/parameters/negativemin.rs b/pywr-core/src/parameters/negativemin.rs index d86e319e..1b9233dd 100644 --- a/pywr-core/src/parameters/negativemin.rs +++ b/pywr-core/src/parameters/negativemin.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -35,7 +35,7 @@ impl GeneralParameter for NegativeMinParameter { network: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let x = -self.metric.get_value(network, state)?; Ok(Some(x.min(self.threshold))) } diff --git a/pywr-core/src/parameters/offset.rs b/pywr-core/src/parameters/offset.rs index d5a662e8..f1aac633 100644 --- a/pywr-core/src/parameters/offset.rs +++ b/pywr-core/src/parameters/offset.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ ActivationFunction, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, VariableConfig, VariableParameter, VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, @@ -60,7 +60,7 @@ impl GeneralParameter for OffsetParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { let offset = self.offset(internal_state); // Current value let x = self.metric.get_value(model, state)?; diff --git a/pywr-core/src/parameters/polynomial.rs b/pywr-core/src/parameters/polynomial.rs index f60b2be1..e6d4a457 100644 --- a/pywr-core/src/parameters/polynomial.rs +++ b/pywr-core/src/parameters/polynomial.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -40,7 +40,7 @@ impl GeneralParameter for Polynomial1DParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; let x = x * self.scale + self.offset; diff --git a/pywr-core/src/parameters/profiles/mod.rs b/pywr-core/src/parameters/profiles/mod.rs index a85b9587..dc42a7c1 100644 --- a/pywr-core/src/parameters/profiles/mod.rs +++ b/pywr-core/src/parameters/profiles/mod.rs @@ -7,7 +7,7 @@ mod weekly; pub use daily::DailyProfileParameter; pub use diurnal::DiurnalProfileParameter; -pub use monthly::{MonthlyInterpDay, MonthlyProfileParameter}; +pub use monthly::{MonthlyInterpDay, MonthlyProfileParameter, MonthlyProfileParameterBuilder}; pub use rbf::{RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig}; pub use uniform_drawdown::UniformDrawdownProfileParameter; pub use weekly::{WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues}; diff --git a/pywr-core/src/parameters/profiles/monthly.rs b/pywr-core/src/parameters/profiles/monthly.rs index 2743f825..c2467f3f 100644 --- a/pywr-core/src/parameters/profiles/monthly.rs +++ b/pywr-core/src/parameters/profiles/monthly.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; @@ -17,14 +21,43 @@ pub struct MonthlyProfileParameter { interp_day: Option, } -impl MonthlyProfileParameter { - pub fn new(name: ParameterName, values: [f64; 12], interp_day: Option) -> Self { +pub struct MonthlyProfileParameterBuilder { + name: ParameterName, + values: [f64; 12], + interp_day: Option, +} + +impl MonthlyProfileParameterBuilder { + pub fn new(name: ParameterName, values: [f64; 12]) -> Self { Self { - meta: ParameterMeta::new(name), + name, values, - interp_day, + interp_day: None, } } + + pub fn interp_day(&mut self, interp_day: MonthlyInterpDay) -> &mut Self { + self.interp_day = Some(interp_day); + self + } +} + +impl ParameterBuilder for MonthlyProfileParameterBuilder { + fn name(&self) -> &ParameterName { + &self.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + Ok(BuiltParameter::Simple(Box::new(MonthlyProfileParameter { + meta: ParameterMeta::new(self.name.clone()), + values: self.values, + interp_day: self.interp_day, + })) + .into()) + } } fn days_in_year_month(datetime: &NaiveDateTime) -> u32 { diff --git a/pywr-core/src/parameters/py.rs b/pywr-core/src/parameters/py.rs index 1ca697a3..8043b5b9 100644 --- a/pywr-core/src/parameters/py.rs +++ b/pywr-core/src/parameters/py.rs @@ -2,7 +2,7 @@ use super::{GeneralParameter, Parameter, ParameterMeta, ParameterName, Parameter use crate::metric::{MetricF64, MetricU64}; use crate::network::Network; use crate::parameters::downcast_internal_state_mut; -use crate::parameters::errors::{ParameterCalculationError, ParameterSetupError}; +use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, State}; use ahash::RandomState; @@ -91,7 +91,7 @@ impl PyCommon { network: &Network, state: &State, values: &mut HashMap, - ) -> Result<(), ParameterCalculationError> { + ) -> Result<(), GeneralCalculationError> { for (k, m) in self.metrics.iter() { let value = m.get_value(network, state)?; values.insert(k.clone(), value); @@ -105,7 +105,7 @@ impl PyCommon { network: &Network, state: &State, values: &mut HashMap, - ) -> Result<(), ParameterCalculationError> { + ) -> Result<(), GeneralCalculationError> { for (k, m) in self.indices.iter() { let value = m.get_value(network, state)?; values.insert(k.clone(), value); @@ -207,14 +207,14 @@ impl PyClassParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> + ) -> Result, GeneralCalculationError> where T: for<'a, 'py> FromPyObject<'a, 'py>, { let internal = downcast_internal_state_mut::(internal_state); ensure_parameter_info(&mut internal.info_obj, timestep, scenario_index).map_err(|py_error| { - ParameterCalculationError::PythonError { + GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.class.to_string(), py_error: Box::new(py_error), @@ -237,24 +237,23 @@ impl PyClassParameter { self.common.update_indices(network, state, &mut info_mut.index_values)?; } - let args = - PyTuple::new(py, [info_bind]).map_err(|py_error| ParameterCalculationError::PythonError { - name: self.common.meta.name.to_string(), - object: self.class.to_string(), - py_error: Box::new(py_error), - })?; + let args = PyTuple::new(py, [info_bind]).map_err(|py_error| GeneralCalculationError::PythonError { + name: self.common.meta.name.to_string(), + object: self.class.to_string(), + py_error: Box::new(py_error), + })?; internal .user_obj .call_method1(py, method, args) - .map_err(|py_error| ParameterCalculationError::PythonError { + .map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.class.to_string(), py_error: Box::new(py_error), })? .extract(py) .map_err(Into::into) - .map_err(|py_error| ParameterCalculationError::PythonError { + .map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.class.to_string(), py_error: Box::new(py_error), @@ -290,7 +289,7 @@ impl GeneralParameter for PyClassParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.call_method("before", timestep, scenario_index, network, state, internal_state) } @@ -301,7 +300,7 @@ impl GeneralParameter for PyClassParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.call_method("after", timestep, scenario_index, network, state, internal_state) } @@ -321,7 +320,7 @@ impl GeneralParameter for PyClassParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.call_method("before", timestep, scenario_index, network, state, internal_state) } @@ -332,7 +331,7 @@ impl GeneralParameter for PyClassParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.call_method("after", timestep, scenario_index, network, state, internal_state) } @@ -352,7 +351,7 @@ impl GeneralParameter for PyClassParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.call_method("before", timestep, scenario_index, network, state, internal_state) } @@ -363,7 +362,7 @@ impl GeneralParameter for PyClassParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.call_method("after", timestep, scenario_index, model, state, internal_state) } @@ -431,14 +430,14 @@ impl PyFuncParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result + ) -> Result where T: for<'a, 'py> FromPyObject<'a, 'py>, { let internal = downcast_internal_state_mut::(internal_state); ensure_parameter_info(&mut internal.info_obj, timestep, scenario_index).map_err(|py_error| { - ParameterCalculationError::PythonError { + GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.function.to_string(), py_error: Box::new(py_error), @@ -460,7 +459,7 @@ impl PyFuncParameter { self.common.update_indices(network, state, &mut info_mut.index_values)?; } - let args = PyTuple::new(py, [info_bind]).map_err(|py_error| ParameterCalculationError::PythonError { + let args = PyTuple::new(py, [info_bind]).map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.function.to_string(), py_error: Box::new(py_error), @@ -470,14 +469,14 @@ impl PyFuncParameter { let args = args .into_sequence() .concat(self.common.args.bind(py).as_sequence()) - .map_err(|py_error| ParameterCalculationError::PythonError { + .map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.function.to_string(), py_error: Box::new(py_error), })?; let args = args .to_tuple() - .map_err(|py_error| ParameterCalculationError::PythonError { + .map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.function.to_string(), py_error: Box::new(py_error), @@ -487,14 +486,14 @@ impl PyFuncParameter { self.function .call(py, args, Some(kwargs)) - .map_err(|py_error| ParameterCalculationError::PythonError { + .map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.function.to_string(), py_error: Box::new(py_error), })? .extract(py) .map_err(Into::into) - .map_err(|py_error| ParameterCalculationError::PythonError { + .map_err(|py_error| GeneralCalculationError::PythonError { name: self.common.meta.name.to_string(), object: self.function.to_string(), py_error: Box::new(py_error), @@ -526,7 +525,7 @@ impl GeneralParameter for PyFuncParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.compute(timestep, scenario_index, network, state, internal_state) } @@ -546,7 +545,7 @@ impl GeneralParameter for PyFuncParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.compute(timestep, scenario_index, network, state, internal_state) } @@ -566,7 +565,7 @@ impl GeneralParameter for PyFuncParameter { network: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { self.compute(timestep, scenario_index, network, state, internal_state) } @@ -583,8 +582,7 @@ mod tests { use super::*; use crate::scenario::ScenarioIndexBuilder; use crate::state::StateBuilder; - use crate::test_utils::default_timestepper; - use crate::timestep::TimeDomain; + use crate::test_utils::default_time_domain_builder; use chrono::Datelike; use float_cmp::assert_approx_eq; use pyo3::ffi::c_str; @@ -676,8 +674,8 @@ class MyParameter: &HashMap::new(), &HashMap::new(), ); - let timestepper = default_timestepper(); - let time: TimeDomain = TimeDomain::try_from(timestepper).unwrap(); + + let time = default_time_domain_builder().build().unwrap(); let timesteps = time.timesteps(); let scenario_indices = [ @@ -781,8 +779,8 @@ class MyParameter: &HashMap::new(), &HashMap::new(), ); - let timestepper = default_timestepper(); - let time: TimeDomain = TimeDomain::try_from(timestepper).unwrap(); + + let time = default_time_domain_builder().build().unwrap(); let timesteps = time.timesteps(); let scenario_indices = [ @@ -849,8 +847,8 @@ def my_function(info, count, **kwargs): &HashMap::new(), &HashMap::new(), ); - let timestepper = default_timestepper(); - let time: TimeDomain = TimeDomain::try_from(timestepper).unwrap(); + + let time = default_time_domain_builder().build().unwrap(); let timesteps = time.timesteps(); let scenario_indices = [ diff --git a/pywr-core/src/parameters/rolling.rs b/pywr-core/src/parameters/rolling.rs index f2c54da3..fae0241c 100644 --- a/pywr-core/src/parameters/rolling.rs +++ b/pywr-core/src/parameters/rolling.rs @@ -1,14 +1,19 @@ use crate::agg_funcs::{AggFuncF64, AggFuncU64}; -use crate::metric::{MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64}; -use crate::network::Network; -use crate::parameters::errors::{ParameterCalculationError, ParameterSetupError, SimpleCalculationError}; +use crate::metric::{ + MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64, UnresolvedMetricF64, + UnresolvedMetricU64, +}; +use crate::network::{Network, ResolutionMaps}; +use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError, SimpleCalculationError}; use crate::parameters::{ - GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter, - downcast_internal_state_mut, downcast_internal_state_ref, + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, SimpleParameter, downcast_internal_state_mut, + downcast_internal_state_ref, }; use crate::scenario::ScenarioIndex; use crate::state::{SimpleParameterValues, State}; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_u64}; use std::collections::VecDeque; /// A rolling parameter that computes an aggregated value over a specified window of metric @@ -28,40 +33,6 @@ pub struct RollingParameter { agg_func: AF, } -impl RollingParameter -where - M: Send + Sync, - T: Send + Sync, - AF: Send + Sync, -{ - /// Creates a new `RollingParameter`. - /// - /// # Arguments - /// * `name` - The name of the parameter. - /// * `metric` - The metric to aggregate over. - /// * `window_size` - The size of the rolling window. - /// * `initial_value` - The initial value to return before enough values are collected. - /// * `min_values` - The minimum number of values required before aggregation starts. - /// * `agg_func` - The aggregation function to use (e.g., sum, mean). - pub fn new( - name: ParameterName, - metric: M, - window_size: usize, - initial_value: T, - min_values: usize, - agg_func: AF, - ) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - window_size, - initial_value, - min_values, - agg_func, - } - } -} - impl TryInto> for &RollingParameter { type Error = MetricF64Error; @@ -140,7 +111,7 @@ impl GeneralParameter for RollingParameter { _model: &Network, _state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_ref::>(internal_state); @@ -159,7 +130,7 @@ impl GeneralParameter for RollingParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_mut::>(internal_state); @@ -249,7 +220,7 @@ impl GeneralParameter for RollingParameter { _model: &Network, _state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_ref::>(internal_state); @@ -268,7 +239,7 @@ impl GeneralParameter for RollingParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let memory = downcast_internal_state_mut::>(internal_state); @@ -350,26 +321,117 @@ impl SimpleParameter for RollingParameter } } +pub struct RollingParameterBuilder { + meta: ParameterMeta, + metric: M, + window_size: usize, + initial_value: T, + min_values: usize, + agg_func: AF, +} + +impl RollingParameterBuilder { + /// Creates a new `RollingParameter`. + /// + /// # Arguments + /// * `name` - The name of the parameter. + /// * `metric` - The metric to aggregate over. + /// * `window_size` - The size of the rolling window. + /// * `initial_value` - The initial value to return before enough values are collected. + /// * `min_values` - The minimum number of values required before aggregation starts. + /// * `agg_func` - The aggregation function to use (e.g., sum, mean). + pub fn new( + name: ParameterName, + metric: M, + window_size: usize, + initial_value: T, + min_values: usize, + agg_func: AF, + ) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + window_size, + initial_value, + min_values, + agg_func, + } + } +} + +impl ParameterBuilder for RollingParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = RollingParameter { + meta: self.meta, + metric, + window_size: self.window_size, + initial_value: self.initial_value, + min_values: self.min_values, + agg_func: self.agg_func, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(MaybeBuiltParameter::Built(bp)) + } +} + +impl ParameterBuilder for RollingParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_u64!(self, self.metric, resolution_maps, "metric"); + + let p = RollingParameter { + meta: self.meta, + metric, + window_size: self.window_size, + initial_value: self.initial_value, + min_values: self.min_values, + agg_func: self.agg_func, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(MaybeBuiltParameter::Built(bp)) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::parameters::Array1Parameter; + use crate::parameters::Array1ParameterBuilder; use crate::test_utils::{run_and_assert_parameter, run_and_assert_parameter_u64, simple_model}; use ndarray::{Array1, Array2, Axis}; #[test] /// Test `RollingParameter` returns the correct f64 value. fn test_rolling_f64() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); - let metric = Array1Parameter::new("my-metric".into(), Array1::from(Array1::linspace(1.0, 21.0, 21)), None); - let metric_idx: MetricF64 = model - .network_mut() - .add_simple_parameter(Box::new(metric)) - .unwrap() - .into_metric_f64_before(); + let metric = Array1ParameterBuilder::new("my-metric".into(), Array1::from(Array1::linspace(1.0, 21.0, 21))); + model_builder.network_builder().parameters().f64(Box::new(metric)); - let parameter = RollingParameter::new("my-parameter".into(), metric_idx, 3, 0.0, 3, AggFuncF64::Mean); + let parameter = RollingParameterBuilder::new( + "my-parameter".into(), + UnresolvedMetricF64::new_parameter_before("my-metric"), + 3, + 0.0, + 3, + AggFuncF64::Mean, + ); // Before the first three values are collected, the parameter should return the initial value. let expected_values: Array1 = [ @@ -382,24 +444,27 @@ mod tests { let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter(&mut model, Box::new(parameter), expected_values, None, Some(1e-12)); + run_and_assert_parameter(model_builder, Box::new(parameter), expected_values, None, Some(1e-12)); } #[test] /// Test `RollingParameter` returns the correct u64 value. fn test_rolling_u64() { - let mut model = simple_model(1, None); + let mut model_builder = simple_model(1, None); let values: Array1 = Array1::from(Array1::linspace(1.0, 21.0, 21).map(|x| *x as u64)); - let metric = Array1Parameter::new("my-metric".into(), values, None); - let metric_idx: MetricU64 = model - .network_mut() - .add_simple_index_parameter(Box::new(metric)) - .unwrap() - .into_metric_u64_before(); + let metric = Array1ParameterBuilder::new("my-metric".into(), values.clone()); + model_builder.network_builder().parameters().u64(Box::new(metric)); - let parameter = RollingParameter::new("my-parameter".into(), metric_idx, 3, 0, 3, AggFuncU64::Max); + let parameter = RollingParameterBuilder::new( + "my-parameter".into(), + UnresolvedMetricU64::new_index_parameter_before("my-metric"), + 3, + 0, + 3, + AggFuncU64::Max, + ); // Before the first three values are collected, the parameter should return the initial value. let expected_values: Array1 = [ @@ -412,6 +477,6 @@ mod tests { let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - run_and_assert_parameter_u64(&mut model, Box::new(parameter), expected_values); + run_and_assert_parameter_u64(model_builder, Box::new(parameter), expected_values); } } diff --git a/pywr-core/src/parameters/threshold.rs b/pywr-core/src/parameters/threshold.rs index 986f7681..4feb344f 100644 --- a/pywr-core/src/parameters/threshold.rs +++ b/pywr-core/src/parameters/threshold.rs @@ -1,6 +1,6 @@ use crate::metric::MetricF64; use crate::network::Network; -use crate::parameters::errors::{ParameterCalculationError, ParameterSetupError}; +use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, downcast_internal_state_mut, }; @@ -79,7 +79,7 @@ impl GeneralParameter for ThresholdParameter { model: &Network, state: &State, internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Downcast the internal state to the correct type let previously_activated = downcast_internal_state_mut::(internal_state); diff --git a/pywr-core/src/parameters/vector.rs b/pywr-core/src/parameters/vector.rs index 7b261be6..985b95f7 100644 --- a/pywr-core/src/parameters/vector.rs +++ b/pywr-core/src/parameters/vector.rs @@ -1,5 +1,5 @@ use crate::network::Network; -use crate::parameters::errors::ParameterCalculationError; +use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -33,10 +33,10 @@ impl GeneralParameter for VectorParameter { _model: &Network, _state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { match self.values.get(timestep.index) { Some(v) => Ok(Some(*v)), - None => Err(ParameterCalculationError::OutOfBoundsError { + None => Err(GeneralCalculationError::OutOfBoundsError { index: timestep.index, length: self.values.len(), axis: 0, diff --git a/pywr-core/src/recorders/memory.rs b/pywr-core/src/recorders/memory.rs index ae7eb7e4..c334b0e1 100644 --- a/pywr-core/src/recorders/memory.rs +++ b/pywr-core/src/recorders/memory.rs @@ -403,11 +403,11 @@ impl Recorder for MemoryRecorder { mod tests { use super::{Aggregation, InternalState, MemoryRecorderResult}; use crate::agg_funcs::AggFuncF64; - use crate::models::ModelDomain; + use crate::models::ModelDomainBuilder; use crate::recorders::RecorderMeta; use crate::recorders::aggregator::PeriodValue; use crate::scenario::{ScenarioDomainBuilder, ScenarioGroupBuilder}; - use crate::test_utils::default_timestepper; + use crate::test_utils::default_time_domain_builder; use float_cmp::assert_approx_eq; use rand::{RngExt, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -419,7 +419,10 @@ mod tests { let scenario_group = ScenarioGroupBuilder::new("test-scenario", 2).build().unwrap(); scenario_builder = scenario_builder.with_group(scenario_group).unwrap(); - let domain = ModelDomain::try_from(default_timestepper(), scenario_builder).unwrap(); + let mut domain_builder = ModelDomainBuilder::new(default_time_domain_builder()); + domain_builder.scenario(scenario_builder); + + let domain = domain_builder.build().unwrap(); let num_metrics = 3; let mut state = InternalState::new(domain.scenarios().len()); diff --git a/pywr-core/src/recorders/mod.rs b/pywr-core/src/recorders/mod.rs index 15c6e7aa..4b692b24 100644 --- a/pywr-core/src/recorders/mod.rs +++ b/pywr-core/src/recorders/mod.rs @@ -7,9 +7,12 @@ mod memory; mod metric_set; mod py; -use crate::metric::{MetricF64, MetricF64Error, MetricU64, MetricU64Error}; +use crate::metric::{ + MetricF64, MetricF64Error, MetricF64ResolutionError, MetricU64, MetricU64Error, MetricU64ResolutionError, + UnresolvedMetricF64, UnresolvedMetricU64, +}; use crate::models::ModelDomain; -use crate::network::Network; +use crate::network::{Network, ResolutionMaps}; use crate::recorders::csv::CsvError; #[cfg(feature = "hdf5")] use crate::recorders::hdf::Hdf5Error; @@ -27,34 +30,8 @@ use ndarray::Array2; use ndarray::prelude::*; use polars::prelude::PolarsError; use std::any::Any; -use std::fmt; -use std::fmt::{Display, Formatter}; -use std::ops::Deref; use thiserror::Error; -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct RecorderIndex(usize); - -impl RecorderIndex { - pub fn new(idx: usize) -> Self { - Self(idx) - } -} - -impl Deref for RecorderIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for RecorderIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - /// Meta data common to all parameters. #[derive(Clone, Debug)] pub struct RecorderMeta { @@ -236,20 +213,31 @@ pub trait Recorder: Send + Sync { } } +#[derive(Debug, Error)] +pub enum RecorderBuilderError { + #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] + ResolveMetricF64Error { + attr: String, + #[source] + source: MetricF64ResolutionError, + }, + #[error("Could not resolve u64 metric for `{attr}` attribute: {source}")] + ResolveMetricU64Error { + attr: String, + #[source] + source: MetricU64ResolutionError, + }, +} + +pub trait RecorderBuilder { + fn name(&self) -> &str; + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError>; +} pub struct Array2Recorder { meta: RecorderMeta, metric: MetricF64, } -impl Array2Recorder { - pub fn new(name: &str, metric: MetricF64) -> Self { - Self { - meta: RecorderMeta::new(name), - metric, - } - } -} - impl Recorder for Array2Recorder { fn meta(&self) -> &RecorderMeta { &self.meta @@ -288,32 +276,50 @@ impl Recorder for Array2Recorder { } } -pub struct AssertionF64Recorder { +pub struct Array2RecorderBuilder { meta: RecorderMeta, - expected_values: Array2, - metric: MetricF64, - ulps: i64, - epsilon: f64, + metric: UnresolvedMetricF64, } -impl AssertionF64Recorder { - pub fn new( - name: &str, - metric: MetricF64, - expected_values: Array2, - ulps: Option, - epsilon: Option, - ) -> Self { +impl Array2RecorderBuilder { + pub fn new(name: &str, metric: UnresolvedMetricF64) -> Self { Self { meta: RecorderMeta::new(name), - expected_values, metric, - ulps: ulps.unwrap_or(5), - epsilon: epsilon.unwrap_or(1e-6), } } } +impl RecorderBuilder for Array2RecorderBuilder { + fn name(&self) -> &str { + self.meta.name.as_str() + } + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric = + self.metric + .resolve(resolution_maps) + .map_err(|source| RecorderBuilderError::ResolveMetricF64Error { + attr: "metric".to_string(), + source, + })?; + + let r = Array2Recorder { + meta: self.meta, + metric, + }; + + Ok(Box::new(r)) + } +} + +pub struct AssertionF64Recorder { + meta: RecorderMeta, + expected_values: Array2, + metric: MetricF64, + ulps: i64, + epsilon: f64, +} + impl Recorder for AssertionF64Recorder { fn meta(&self) -> &RecorderMeta { &self.meta @@ -369,21 +375,67 @@ expected: `{:?}`"#, } } -pub struct AssertionU64Recorder { +pub struct AssertionF64RecorderBuilder { meta: RecorderMeta, - expected_values: Array2, - metric: MetricU64, + expected_values: Array2, + metric: UnresolvedMetricF64, + ulps: i64, + epsilon: f64, } -impl AssertionU64Recorder { - pub fn new(name: &str, metric: MetricU64, expected_values: Array2) -> Self { +impl AssertionF64RecorderBuilder { + pub fn new(name: &str, metric: UnresolvedMetricF64, expected_values: Array2) -> Self { Self { meta: RecorderMeta::new(name), expected_values, metric, + ulps: 5, + epsilon: 1e-6, } } + + pub fn ulps(&mut self, ulps: i64) -> &mut Self { + self.ulps = ulps; + self + } + + pub fn epsilon(&mut self, epsilon: f64) -> &mut Self { + self.epsilon = epsilon; + self + } +} + +impl RecorderBuilder for AssertionF64RecorderBuilder { + fn name(&self) -> &str { + &self.meta.name + } + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric = + self.metric + .resolve(resolution_maps) + .map_err(|source| RecorderBuilderError::ResolveMetricF64Error { + attr: "metric".to_string(), + source, + })?; + + let r = AssertionF64Recorder { + meta: self.meta, + expected_values: self.expected_values, + metric, + ulps: self.ulps, + epsilon: self.epsilon, + }; + + Ok(Box::new(r)) + } } + +pub struct AssertionU64Recorder { + meta: RecorderMeta, + expected_values: Array2, + metric: MetricU64, +} + impl Recorder for AssertionU64Recorder { fn meta(&self) -> &RecorderMeta { &self.meta @@ -433,29 +485,53 @@ expected: `{:?}`"#, } } -pub struct AssertionFnRecorder { +pub struct AssertionU64RecorderBuilder { meta: RecorderMeta, - expected_func: F, - metric: MetricF64, - ulps: i64, - epsilon: f64, + expected_values: Array2, + metric: UnresolvedMetricU64, } -impl AssertionFnRecorder -where - F: Fn(&Timestep, &ScenarioIndex) -> f64, -{ - pub fn new(name: &str, metric: MetricF64, expected_func: F, ulps: Option, epsilon: Option) -> Self { +impl AssertionU64RecorderBuilder { + pub fn new(name: &str, metric: UnresolvedMetricU64, expected_values: Array2) -> Self { Self { meta: RecorderMeta::new(name), - expected_func, + expected_values, metric, - ulps: ulps.unwrap_or(2), - epsilon: epsilon.unwrap_or(f64::EPSILON * 2.0), } } } +impl RecorderBuilder for AssertionU64RecorderBuilder { + fn name(&self) -> &str { + &self.meta.name + } + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric = + self.metric + .resolve(resolution_maps) + .map_err(|source| RecorderBuilderError::ResolveMetricU64Error { + attr: "metric".to_string(), + source, + })?; + + let r = AssertionU64Recorder { + meta: self.meta, + expected_values: self.expected_values, + metric, + }; + + Ok(Box::new(r)) + } +} + +pub struct AssertionFnRecorder { + meta: RecorderMeta, + expected_func: F, + metric: MetricF64, + ulps: i64, + epsilon: f64, +} + impl Recorder for AssertionFnRecorder where F: Send + Sync + Fn(&Timestep, &ScenarioIndex) -> f64, @@ -498,68 +574,64 @@ where } } -pub struct IndexAssertionRecorder { +pub struct AssertionFnRecorderBuilder { meta: RecorderMeta, - expected_values: Array2, - metric: MetricU64, + expected_func: F, + metric: UnresolvedMetricF64, + ulps: i64, + epsilon: f64, } -impl IndexAssertionRecorder { - pub fn new(name: &str, metric: MetricU64, expected_values: Array2) -> Self { +impl AssertionFnRecorderBuilder +where + F: Fn(&Timestep, &ScenarioIndex) -> f64, +{ + pub fn new(name: &str, metric: UnresolvedMetricF64, expected_func: F) -> Self { Self { meta: RecorderMeta::new(name), - expected_values, + expected_func, metric, + ulps: 2, + epsilon: f64::EPSILON * 2.0, } } -} -impl Recorder for IndexAssertionRecorder { - fn meta(&self) -> &RecorderMeta { - &self.meta + pub fn ulps(&mut self, ulps: i64) -> &mut Self { + self.ulps = ulps; + self } - fn save( - &self, - timestep: &Timestep, - scenario_indices: &[ScenarioIndex], - network: &Network, - state: &[State], - _metric_set_states: &[Vec], - _internal_state: &mut Option>, - ) -> Result<(), RecorderSaveError> { - // This panics if out-of-bounds - - for scenario_index in scenario_indices { - let expected_value = match self - .expected_values - .get([timestep.index, scenario_index.simulation_id()]) - { - Some(v) => *v, - None => panic!("Simulation produced results out of range."), - }; - - let actual_value = self.metric.get_value(network, &state[scenario_index.simulation_id()])?; + pub fn epsilon(&mut self, epsilon: f64) -> &mut Self { + self.epsilon = epsilon; + self + } +} - if actual_value != expected_value { - panic!( - r#"assertion failed: (actual eq expected) -recorder: `{}` -timestep: `{:?}` ({}) -scenario: `{:?}` -actual: `{:?}` -expected: `{:?}`"#, - self.meta.name, - timestep.date, - timestep.index, - scenario_index.simulation_id(), - actual_value, - expected_value - ) - } - } +impl RecorderBuilder for AssertionFnRecorderBuilder +where + F: Send + Sync + Fn(&Timestep, &ScenarioIndex) -> f64 + 'static, +{ + fn name(&self) -> &str { + &self.meta.name + } + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric = + self.metric + .resolve(resolution_maps) + .map_err(|source| RecorderBuilderError::ResolveMetricF64Error { + attr: "metric".to_string(), + source, + })?; + + let r = AssertionFnRecorder { + meta: self.meta, + expected_func: self.expected_func, + metric, + ulps: self.ulps, + epsilon: self.epsilon, + }; - Ok(()) + Ok(Box::new(r)) } } @@ -570,13 +642,14 @@ mod tests { #[test] fn test_array2_recorder() { - let mut model = simple_model(2, None); + let mut model_builder = simple_model(2, None); + + let rec = Array2RecorderBuilder::new("test", UnresolvedMetricF64::NodeOutFlow("input".into())); - let node_idx = model.network().get_node_index_by_name("input", None).unwrap(); + model_builder.network_builder().recorder(Box::new(rec)); - let rec = Array2Recorder::new("test", MetricF64::NodeOutFlow(node_idx)); + let model = model_builder.build().unwrap(); - let _idx = model.network_mut().add_recorder(Box::new(rec)).unwrap(); // Test all solvers run_all_solvers(&model, &[], &[], &[]); diff --git a/pywr-core/src/scenario.rs b/pywr-core/src/scenario.rs index cfd79051..f30a7e7f 100644 --- a/pywr-core/src/scenario.rs +++ b/pywr-core/src/scenario.rs @@ -1,12 +1,12 @@ #![warn(clippy::pedantic)] #[cfg(feature = "pyo3")] use pyo3::{pyclass, pymethods}; -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; use thiserror::Error; /// Errors that can occur when building a [`ScenarioDomain`]. #[derive(Error, Debug)] -pub enum ScenarioError { +pub enum ScenarioDomainBuilderError { #[error("Scenario group name `{0}` already exists")] DuplicateGroupName(String), #[error("Scenario group name `{0}` not found")] @@ -86,18 +86,20 @@ impl ScenarioGroup { /// /// # Errors /// - /// - [`ScenarioError::NoLabels`] if no labels are defined for the group - /// - [`ScenarioError::LabelNotFound`] if the label is not found in the labels for the group - fn label_position(&self, label: &str) -> Result { + /// - [`ScenarioDomainBuilderError::NoLabels`] if no labels are defined for the group + /// - [`ScenarioDomainBuilderError::LabelNotFound`] if the label is not found in the labels for the group + fn label_position(&self, label: &str) -> Result { match &self.labels { - Some(labels) => labels - .iter() - .position(|l| l == label) - .ok_or_else(|| ScenarioError::LabelNotFound { - group: self.name.clone(), - label: label.to_string(), - }), - None => Err(ScenarioError::NoLabels { + Some(labels) => { + labels + .iter() + .position(|l| l == label) + .ok_or_else(|| ScenarioDomainBuilderError::LabelNotFound { + group: self.name.clone(), + label: label.to_string(), + }) + } + None => Err(ScenarioDomainBuilderError::NoLabels { group: self.name.clone(), }), } @@ -174,15 +176,15 @@ impl ScenarioGroupBuilder { /// /// # Errors /// - /// - [`ScenarioError::InvalidSlice`] if the slice is invalid (e.g. start >= end or end > size) - /// - [`ScenarioError::NoLabels`] if a label subset is defined but no labels are provided - /// - [`ScenarioError::LabelNotFound`] if a label in the subset is not found in the labels for the group - /// - [`ScenarioError::IncorrectNumberOfLabels`] if the number of labels provided does not match the group size - pub fn build(self) -> Result { + /// - [`ScenarioDomainBuilderError::InvalidSlice`] if the slice is invalid (e.g. start >= end or end > size) + /// - [`ScenarioDomainBuilderError::NoLabels`] if a label subset is defined but no labels are provided + /// - [`ScenarioDomainBuilderError::LabelNotFound`] if a label in the subset is not found in the labels for the group + /// - [`ScenarioDomainBuilderError::IncorrectNumberOfLabels`] if the number of labels provided does not match the group size + pub fn build(self) -> Result { let subset = match self.subset { Some(ScenarioGroupSubsetBuilder::Slice { start, end }) => { if start >= end || end > self.size { - return Err(ScenarioError::InvalidSlice { + return Err(ScenarioDomainBuilderError::InvalidSlice { group: self.name.clone(), size: self.size, start, @@ -198,19 +200,18 @@ impl ScenarioGroupBuilder { let indices: Vec = subset_labels .iter() .map(|l| { - labels - .iter() - .position(|label| label == l) - .ok_or_else(|| ScenarioError::LabelNotFound { + labels.iter().position(|label| label == l).ok_or_else(|| { + ScenarioDomainBuilderError::LabelNotFound { label: l.clone(), group: self.name.clone(), - }) + } + }) }) - .collect::, ScenarioError>>()?; + .collect::, ScenarioDomainBuilderError>>()?; Some(ScenarioGroupSubset::Indices(indices)) } else { - return Err(ScenarioError::NoLabels { + return Err(ScenarioDomainBuilderError::NoLabels { group: self.name.clone(), }); } @@ -220,7 +221,7 @@ impl ScenarioGroupBuilder { if let Some(labels) = &self.labels { if labels.len() != self.size { - return Err(ScenarioError::IncorrectNumberOfLabels { + return Err(ScenarioDomainBuilderError::IncorrectNumberOfLabels { group: self.name, found: labels.len(), expected: self.size, @@ -269,15 +270,24 @@ pub struct ScenarioDomainBuilder { } impl ScenarioDomainBuilder { + #[must_use] + pub fn group_map(&self) -> HashMap { + self.groups + .iter() + .enumerate() + .map(|(i, g)| (g.name.clone(), i)) + .collect() + } + /// Add a [`ScenarioGroup`] to the collection /// /// # Errors /// - /// - [`ScenarioError::DuplicateGroupName`] if a group with the same name already exists in the builder - pub fn with_group(mut self, group: ScenarioGroup) -> Result { + /// - [`ScenarioDomainBuilderError::DuplicateGroupName`] if a group with the same name already exists in the builder + pub fn with_group(mut self, group: ScenarioGroup) -> Result { for g in &self.groups { if g.name == group.name { - return Err(ScenarioError::DuplicateGroupName(group.name.clone())); + return Err(ScenarioDomainBuilderError::DuplicateGroupName(group.name.clone())); } } @@ -363,9 +373,9 @@ impl ScenarioDomainBuilder { /// /// # Errors /// - /// A [`ScenarioError::CombinationsAndSlices`] will be returned if the combinations and slices + /// A [`ScenarioDomainBuilderError::CombinationsAndSlices`] will be returned if the combinations and slices /// are both defined as this is not currently supported. - /// Other [`ScenarioError`] variants may be returned from the group builders if the groups are + /// Other [`ScenarioDomainBuilderError`] variants may be returned from the group builders if the groups are /// not valid (e.g. invalid slices or labels). /// /// # Panics @@ -374,7 +384,7 @@ impl ScenarioDomainBuilder { /// corresponding group labels, or if the number of labels does not match the group size. /// However, these cases should be caught by the group builders when building the groups. #[allow(clippy::too_many_lines)] - pub fn build(self) -> Result { + pub fn build(self) -> Result { let (indices, groups, scenario_map) = if self.groups.is_empty() { // Default to a single scenario if no groups are defined. ( @@ -387,7 +397,7 @@ impl ScenarioDomainBuilder { let mut scenario_indices: Vec = Vec::with_capacity(num); if self.groups.iter().any(|grp| grp.subset.is_some()) && self.combinations.is_some() { - return Err(ScenarioError::CombinationsAndSlices); + return Err(ScenarioDomainBuilderError::CombinationsAndSlices); } // Handle the case where there are specific combinations of scenarios @@ -403,9 +413,9 @@ impl ScenarioDomainBuilder { ScenarioLabelOrIndex::Label(label) => group.label_position(label), ScenarioLabelOrIndex::Index(index) => Ok(*index), }) - .collect::, ScenarioError>>() + .collect::, ScenarioDomainBuilderError>>() }) - .collect::>, ScenarioError>>()?; + .collect::>, ScenarioDomainBuilderError>>()?; let scenario_map_from_combinations = self.build_scenario_map_from_combinations(&combinations); @@ -445,7 +455,7 @@ impl ScenarioDomainBuilder { let is_sliced = scenario_map_from_slices.iter().any(Option::is_some); if is_sliced && self.combinations.is_some() { - return Err(ScenarioError::CombinationsAndSlices); + return Err(ScenarioDomainBuilderError::CombinationsAndSlices); } for scenario_id in 0..num { @@ -699,12 +709,12 @@ impl ScenarioDomain { /// /// # Errors /// - /// - [`ScenarioError::GroupNameNotFound`] if the group name is not found in the domain. - pub fn group_index(&self, name: &str) -> Result { + /// - [`ScenarioDomainBuilderError::GroupNameNotFound`] if the group name is not found in the domain. + pub fn group_index(&self, name: &str) -> Result { self.groups .iter() .position(|g| g.name == name) - .ok_or_else(|| ScenarioError::GroupNameNotFound(name.to_string())) + .ok_or_else(|| ScenarioDomainBuilderError::GroupNameNotFound(name.to_string())) } /// Return the scenario groups for the domain. @@ -717,8 +727,8 @@ impl ScenarioDomain { /// /// # Errors /// - /// - [`ScenarioError::GroupNameNotFound`] if the group name is not found - pub fn group_scenario_subset(&self, name: &str) -> Result, ScenarioError> { + /// - [`ScenarioDomainBuilderError::GroupNameNotFound`] if the group name is not found + pub fn group_scenario_subset(&self, name: &str) -> Result, ScenarioDomainBuilderError> { let group_index = self.group_index(name)?; Ok(self.scenario_map[group_index].as_deref()) } @@ -727,8 +737,8 @@ impl ScenarioDomain { /// /// # Errors /// - /// - [`ScenarioError::GroupNameNotFound`] if the group name is not found - pub fn group_size(&self, name: &str) -> Result { + /// - [`ScenarioDomainBuilderError::GroupNameNotFound`] if the group name is not found + pub fn group_size(&self, name: &str) -> Result { let group_index = self.group_index(name)?; Ok(self.groups[group_index].size()) } @@ -736,7 +746,7 @@ impl ScenarioDomain { #[cfg(test)] mod tests { - use super::{ScenarioDomain, ScenarioDomainBuilder, ScenarioError, ScenarioGroupBuilder}; + use super::{ScenarioDomain, ScenarioDomainBuilder, ScenarioDomainBuilderError, ScenarioGroupBuilder}; #[test] fn test_group_builder() { @@ -759,7 +769,7 @@ mod tests { assert!(group.is_err()); assert!(matches!( group.err().unwrap(), - ScenarioError::IncorrectNumberOfLabels { + ScenarioDomainBuilderError::IncorrectNumberOfLabels { group: _, found: 4, expected: 3 @@ -774,7 +784,10 @@ mod tests { .build(); assert!(group.is_err()); - assert!(matches!(group.err().unwrap(), ScenarioError::NoLabels { group: _ })); + assert!(matches!( + group.err().unwrap(), + ScenarioDomainBuilderError::NoLabels { group: _ } + )); } #[test] @@ -784,7 +797,7 @@ mod tests { assert!(group.is_err()); assert!(matches!( group.err().unwrap(), - ScenarioError::InvalidSlice { + ScenarioDomainBuilderError::InvalidSlice { group: _, size: 3, start: 0, @@ -805,7 +818,10 @@ mod tests { .with_group(group_b); assert!(result.is_err()); - assert!(matches!(result.err().unwrap(), ScenarioError::DuplicateGroupName(_))); + assert!(matches!( + result.err().unwrap(), + ScenarioDomainBuilderError::DuplicateGroupName(_) + )); } #[test] @@ -829,7 +845,10 @@ mod tests { .build(); assert!(result.is_err()); - assert!(matches!(result.err().unwrap(), ScenarioError::CombinationsAndSlices)); + assert!(matches!( + result.err().unwrap(), + ScenarioDomainBuilderError::CombinationsAndSlices + )); } #[test] diff --git a/pywr-core/src/solvers/builder.rs b/pywr-core/src/solvers/builder.rs index 34f8c9e9..c5a04f25 100644 --- a/pywr-core/src/solvers/builder.rs +++ b/pywr-core/src/solvers/builder.rs @@ -1,14 +1,11 @@ -use crate::aggregated_node::AggregatedNodeIndex; -use crate::edge::EdgeIndex; -use crate::network::Network; -use crate::node::{Node, NodeBounds, NodeIndex, NodeType}; +use crate::network::{AggregatedNodeIndex, EdgeIndex, Network, NodeIndex}; +use crate::node::{Node, NodeBounds, NodeType}; use crate::solvers::col_edge_map::{ColumnEdgeMap, ColumnEdgeMapBuilder}; use crate::solvers::{SolverSetupError, SolverSolveError, SolverTimings}; use crate::state::{ConstParameterValues, State}; use crate::timestep::Timestep; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Debug; -use std::ops::Deref; use std::time::Instant; enum Bounds { @@ -444,7 +441,7 @@ where /// Update edge objective coefficients fn update_edge_objectives(&mut self, network: &Network, state: &State) -> Result<(), SolverSolveError> { self.builder.zero_obj_coefficients(); - for edge in network.edges().deref() { + for edge in network.edges() { let obj_coef: f64 = edge.cost(network.nodes(), network, state).map_err(|source| { let from_node = match network.get_node(&edge.from_node_index()) { Some(n) => n, @@ -544,7 +541,7 @@ where // TODO error handling? let nodes = network.nodes(); for node0_idx in node_pair.node0_indices() { - let node0 = nodes.get(node0_idx).expect("Node index not found!"); + let node0 = nodes.get(**node0_idx).expect("Node index not found!"); self.builder.update_row_coefficients( *row_idx, node0, @@ -554,7 +551,7 @@ where } for node1_idx in node_pair.node1_indices() { - let node1 = nodes.get(node1_idx).expect("Node index not found!"); + let node1 = nodes.get(**node1_idx).expect("Node index not found!"); self.builder.update_row_coefficients( *row_idx, node1, @@ -582,11 +579,7 @@ where network: &Network, state: &State, ) -> Result<(), SolverSolveError> { - for (row_id, agg_node) in self - .agg_node_constraint_row_ids - .iter() - .zip(network.aggregated_nodes().deref()) - { + for (row_id, agg_node) in self.agg_node_constraint_row_ids.iter().zip(network.aggregated_nodes()) { let (lb, ub): (f64, f64) = agg_node .get_flow_bounds(network, state) @@ -612,7 +605,7 @@ where for (row_id, node) in self .virtual_storage_constraint_row_ids .iter() - .zip(network.virtual_storage_nodes().deref()) + .zip(network.virtual_storage_nodes()) { let (lb, ub) = if node.is_active(timestep) { let (avail, missing) = node.get_available_volume_bounds(state)?; @@ -733,7 +726,7 @@ where // majority of cases the nodes will only be in one set. However, if a node is in different // sets then we need to create separate binary variables and associated them with that node. let mut node_sets_in_a_mutual_exclusivity = HashSet::new(); - for agg_node in network.aggregated_nodes().deref() { + for agg_node in network.aggregated_nodes() { if agg_node.has_exclusivity() { for node_set in agg_node.iter_nodes() { node_sets_in_a_mutual_exclusivity.insert(node_set); @@ -756,7 +749,7 @@ where /// Create mass balance constraints for each edge fn create_mass_balance_constraints(&mut self, network: &Network) { - for node in network.nodes().deref() { + for node in network.nodes() { // Only link nodes create mass-balance constraints if let NodeType::Link = node.node_type() { @@ -849,7 +842,7 @@ where ) -> Result>, SolverSetupError> { let mut row_ids = Vec::with_capacity(network.nodes().len()); - for node in network.nodes().deref() { + for node in network.nodes() { // Get the node's flow bounds if they are constants // Storage nodes cannot have constant bounds let bounds = match node.get_const_bounds(values)? { @@ -950,7 +943,7 @@ where ) -> Vec> { let mut row_ids = Vec::new(); - for agg_node in network.aggregated_nodes().deref() { + for agg_node in network.aggregated_nodes() { // Only create row for nodes that have factors if let Some(Ok(node_pairs)) = agg_node.get_const_norm_factor_pairs(values) { let mut row_indices_for_agg_node = Vec::with_capacity(node_pairs.len()); @@ -966,14 +959,14 @@ where let f0 = node_pair.node0_factor(); for node0_idx in node_pair.node0_indices() { - let node0 = nodes.get(node0_idx).expect("Node index not found!"); + let node0 = nodes.get(**node0_idx).expect("Node index not found!"); self.add_node(node0, f0.unwrap_or(1.0), &mut row); } let f1 = node_pair.node1_factor(); for node1_idx in node_pair.node1_indices() { - let node1 = nodes.get(node1_idx).expect("Node index not found!"); + let node1 = nodes.get(**node1_idx).expect("Node index not found!"); self.add_node(node1, f1.unwrap_or(1.0), &mut row); } @@ -1012,14 +1005,14 @@ where fn create_aggregated_node_constraints(&mut self, network: &Network) -> Vec { let mut row_ids = Vec::with_capacity(network.aggregated_nodes().len()); - for agg_node in network.aggregated_nodes().deref() { + for agg_node in network.aggregated_nodes() { // Create empty arrays to store the matrix data let mut row: RowBuilder = RowBuilder::default(); for node_indices in agg_node.iter_nodes() { // TODO error handling? for node_idx in node_indices { - let node = network.nodes().get(node_idx).expect("Node index not found!"); + let node = network.nodes().get(**node_idx).expect("Node index not found!"); self.add_node(node, 1.0, &mut row); } } @@ -1035,7 +1028,7 @@ where fn create_virtual_storage_constraints(&mut self, network: &Network) -> Vec { let mut row_ids = Vec::with_capacity(network.virtual_storage_nodes().len()); - for virtual_storage in network.virtual_storage_nodes().deref() { + for virtual_storage in network.virtual_storage_nodes() { // Create empty arrays to store the matrix data let mut row: RowBuilder = RowBuilder::default(); @@ -1046,7 +1039,7 @@ where virtual_storage.full_name() ); } - let node = network.nodes().get(node_index).expect("Node index not found!"); + let node = network.nodes().get(**node_index).expect("Node index not found!"); self.add_node(node, -factor, &mut row); } let row_id = self.builder.add_variable_row(row); diff --git a/pywr-core/src/solvers/col_edge_map.rs b/pywr-core/src/solvers/col_edge_map.rs index 458b09bb..189272d0 100644 --- a/pywr-core/src/solvers/col_edge_map.rs +++ b/pywr-core/src/solvers/col_edge_map.rs @@ -1,4 +1,4 @@ -use crate::edge::EdgeIndex; +use crate::network::EdgeIndex; use std::collections::BTreeMap; use std::ops::Deref; diff --git a/pywr-core/src/solvers/highs/mod.rs b/pywr-core/src/solvers/highs/mod.rs index 89964e80..358749ad 100644 --- a/pywr-core/src/solvers/highs/mod.rs +++ b/pywr-core/src/solvers/highs/mod.rs @@ -23,7 +23,6 @@ use libc::c_void; pub use settings::build_highs_settings_py; pub use settings::{HighsSolverSettings, HighsSolverSettingsBuilder}; use std::ffi::CString; -use std::ops::Deref; use std::ptr::null; use std::time::Instant; use thiserror::Error; @@ -426,7 +425,7 @@ impl Solver for HighsSolver { network_state.reset(); let start_save_solution = Instant::now(); - for edge in network.edges().deref() { + for edge in network.edges() { let col = self.builder.col_for_edge(&edge.index()) as usize; let flow = solution[col]; network_state.add_flow(edge, timestep, flow)?; diff --git a/pywr-core/src/solvers/ipm_ocl/mod.rs b/pywr-core/src/solvers/ipm_ocl/mod.rs index 4dce2ff7..ce59633a 100644 --- a/pywr-core/src/solvers/ipm_ocl/mod.rs +++ b/pywr-core/src/solvers/ipm_ocl/mod.rs @@ -1,7 +1,6 @@ mod settings; -use crate::edge::EdgeIndex; -use crate::network::Network; +use crate::network::{EdgeIndex, Network}; use crate::node::{Node, NodeBounds, NodeType}; use crate::solvers::col_edge_map::{ColumnEdgeMap, ColumnEdgeMapBuilder}; use crate::solvers::{MultiStateSolver, SolverFeatures, SolverSetupError, SolverSolveError, SolverTimings}; @@ -15,7 +14,6 @@ pub use settings::{ClIpmSolverSettings, ClIpmSolverSettingsBuilder}; use std::collections::BTreeMap; use std::f64; use std::num::NonZeroUsize; -use std::ops::Deref; use std::time::Instant; const B_MAX: f64 = 999999.0; @@ -301,7 +299,7 @@ impl BuiltSolver { /// Update edge objective coefficients fn update_edge_objectives(&mut self, network: &Network, states: &[State]) -> Result<(), SolverSolveError> { self.lp.zero_obj_coefficients(); - for edge in network.edges().deref() { + for edge in network.edges() { // Collect all of the costs for all states together let cost = states .iter() @@ -347,7 +345,7 @@ impl BuiltSolver { let dt = timestep.days(); - for node in network.nodes().deref() { + for node in network.nodes() { match node.node_type() { NodeType::Input | NodeType::Output | NodeType::Link => { if !node.is_max_flow_unconstrained().unwrap() { @@ -482,7 +480,7 @@ impl SolverBuilder { /// Create mass balance constraints for each edge fn create_mass_balance_constraints(&mut self, network: &Network) { - for node in network.nodes().deref() { + for node in network.nodes() { // Only link nodes create mass-balance constraints if let NodeType::Link = node.node_type() { @@ -552,7 +550,7 @@ impl SolverBuilder { fn create_node_constraints(&mut self, network: &Network) -> Vec { let mut row_ids = Vec::with_capacity(network.nodes().len()); - for node in network.nodes().deref() { + for node in network.nodes() { match node.node_type() { NodeType::Input | NodeType::Output | NodeType::Link => { // Only create node constraints for nodes that could become constrained @@ -693,7 +691,7 @@ impl MultiStateSolver for ClIpmF32Solver { let network_state = state.get_mut_network_state(); network_state.reset(); - for edge in network.edges().deref() { + for edge in network.edges() { let col = built.col_for_edge(&edge.index()); let flow = solution[col * num_states + i]; network_state.add_flow(edge, timestep, flow as f64).unwrap(); @@ -819,7 +817,7 @@ impl MultiStateSolver for ClIpmF64Solver { let network_state = state.get_mut_network_state(); network_state.reset(); - for edge in network.edges().deref() { + for edge in network.edges() { let col = built.col_for_edge(&edge.index()); let flow = solution[col * num_states + i]; network_state.add_flow(edge, timestep, flow).unwrap(); diff --git a/pywr-core/src/solvers/ipm_simd/mod.rs b/pywr-core/src/solvers/ipm_simd/mod.rs index d71e9958..a2e28ad7 100644 --- a/pywr-core/src/solvers/ipm_simd/mod.rs +++ b/pywr-core/src/solvers/ipm_simd/mod.rs @@ -1,7 +1,6 @@ mod settings; -use crate::edge::EdgeIndex; -use crate::network::Network; +use crate::network::{EdgeIndex, Network}; use crate::node::{Node, NodeBounds, NodeType}; use crate::solvers::col_edge_map::{ColumnEdgeMap, ColumnEdgeMapBuilder}; use crate::solvers::{MultiStateSolver, SolverFeatures, SolverSetupError, SolverSolveError, SolverTimings}; @@ -16,7 +15,6 @@ pub use settings::build_ipm_simd_settings_py; pub use settings::{SimdIpmSolverSettings, SimdIpmSolverSettingsBuilder}; use std::collections::BTreeMap; use std::num::NonZeroUsize; -use std::ops::Deref; use std::time::Instant; use wide::f64x4; @@ -316,7 +314,7 @@ impl BuiltSolver { /// Update edge objective coefficients fn update_edge_objectives(&mut self, network: &Network, states: &[State]) -> Result<(), SolverSolveError> { self.lp.zero_obj_coefficients(); - for edge in network.edges().deref() { + for edge in network.edges() { // Collect all of the costs for all states together let cost = states .iter() @@ -362,7 +360,7 @@ impl BuiltSolver { let dt = timestep.days(); - for node in network.nodes().deref() { + for node in network.nodes() { match node.node_type() { NodeType::Input | NodeType::Output | NodeType::Link => { if !node.is_max_flow_unconstrained().unwrap() { @@ -497,7 +495,7 @@ impl SolverBuilder { /// Create mass balance constraints for each edge fn create_mass_balance_constraints(&mut self, network: &Network) { - for node in network.nodes().deref() { + for node in network.nodes() { // Only link nodes create mass-balance constraints if let NodeType::Link = node.node_type() { @@ -567,7 +565,7 @@ impl SolverBuilder { fn create_node_constraints(&mut self, network: &Network) -> Vec { let mut row_ids = Vec::with_capacity(network.nodes().len()); - for node in network.nodes().deref() { + for node in network.nodes() { match node.node_type() { NodeType::Input | NodeType::Output | NodeType::Link => { // Only create node constraints for nodes that could become constrained @@ -690,7 +688,7 @@ impl MultiStateSolver for SimdIpmF64Solver { state.get_mut_network_state().reset(); } - for edge in network.edges().deref() { + for edge in network.edges() { let col = built.col_for_edge(&edge.index()); let flows = solution[col]; diff --git a/pywr-core/src/solvers/mod.rs b/pywr-core/src/solvers/mod.rs index 26875eda..82792e7e 100644 --- a/pywr-core/src/solvers/mod.rs +++ b/pywr-core/src/solvers/mod.rs @@ -1,4 +1,4 @@ -use crate::network::Network; +use crate::network::{AggregatedNodeIndex, Network}; use crate::state::{ConstParameterValues, State}; use crate::timestep::Timestep; use std::ops::{Add, AddAssign}; @@ -38,8 +38,7 @@ pub use self::ipm_ocl::{ClIpmF32Solver, ClIpmF64Solver, ClIpmSolverSettings, ClI pub use self::ipm_simd::build_ipm_simd_settings_py; #[cfg(feature = "ipm-simd")] pub use self::ipm_simd::{SimdIpmF64Solver, SimdIpmSolverSettings, SimdIpmSolverSettingsBuilder}; -use crate::aggregated_node::AggregatedNodeIndex; -use crate::node::NodeIndex; +use crate::NodeIndex; #[cfg(all(feature = "cbc", feature = "pyo3"))] pub use cbc::build_cbc_settings_py; #[cfg(feature = "cbc")] diff --git a/pywr-core/src/state/mod.rs b/pywr-core/src/state/mod.rs index 5aca3742..ea782473 100644 --- a/pywr-core/src/state/mod.rs +++ b/pywr-core/src/state/mod.rs @@ -1,16 +1,15 @@ mod flow; mod storage; -use crate::edge::{Edge, EdgeIndex}; +use crate::edge::Edge; use crate::metric::SimpleMetricF64Error; use crate::models::MultiNetworkTransferIndex; -use crate::network::Network; -use crate::node::{Node, NodeIndex}; +use crate::network::{EdgeIndex, Network, NodeIndex, VirtualStorageIndex}; +use crate::node::Node; use crate::parameters::{ ConstParameterIndex, GeneralParameterIndex, ParameterCollection, ParameterCollectionSize, SimpleParameterIndex, }; use crate::timestep::Timestep; -use crate::virtual_storage::VirtualStorageIndex; use flow::FlowState; #[cfg(feature = "pyo3")] use pyo3::{ @@ -481,7 +480,7 @@ impl NetworkState { Some(s) => { let node = model .nodes() - .get(idx) + .get(*idx.deref()) .ok_or(NetworkStateError::NodeIndexNotFound(*idx))?; match node { Node::Input(_) => Ok(factor * s.get_out_flow()), @@ -772,33 +771,33 @@ impl State { &mut self.network } - pub fn get_parameter_value( + pub fn get_general_parameter_value( &self, idx: GeneralParameterIndex, return_value: ParameterReturnValue, ) -> Result { match return_value { - ParameterReturnValue::Before => self.get_parameter_value_before(idx), - ParameterReturnValue::After => self.get_parameter_value_after(idx), - ParameterReturnValue::BeforeOrElseAfter => match self.get_parameter_value_before(idx) { + ParameterReturnValue::Before => self.get_general_parameter_value_before(idx), + ParameterReturnValue::After => self.get_general_parameter_value_after(idx), + ParameterReturnValue::BeforeOrElseAfter => match self.get_general_parameter_value_before(idx) { Ok(v) => Ok(v), - Err(_) => self.get_parameter_value_after(idx), + Err(_) => self.get_general_parameter_value_after(idx), }, - ParameterReturnValue::AfterOrElseBefore => match self.get_parameter_value_after(idx) { + ParameterReturnValue::AfterOrElseBefore => match self.get_general_parameter_value_after(idx) { Ok(v) => Ok(v), - Err(_) => self.get_parameter_value_before(idx), + Err(_) => self.get_general_parameter_value_before(idx), }, } } - fn get_parameter_value_before(&self, idx: GeneralParameterIndex) -> Result { + fn get_general_parameter_value_before(&self, idx: GeneralParameterIndex) -> Result { self.parameters_before .general .get_value(*idx) .ok_or(StateError::GeneralParameterIndexNotFound(idx)) } - fn get_parameter_value_after(&self, idx: GeneralParameterIndex) -> Result { + fn get_general_parameter_value_after(&self, idx: GeneralParameterIndex) -> Result { self.parameters_after .general .get_value(*idx) @@ -806,7 +805,7 @@ impl State { } /// Set the "before" value of a general parameter. - pub fn set_parameter_value_before( + pub fn set_general_parameter_value_before( &mut self, idx: GeneralParameterIndex, value: Option, @@ -827,7 +826,7 @@ impl State { } /// Set the "after" value of a general parameter. - pub fn set_parameter_value_after( + pub fn set_general_parameter_value_after( &mut self, idx: GeneralParameterIndex, value: Option, @@ -886,40 +885,40 @@ impl State { Ok(()) } - pub fn get_parameter_index( + pub fn get_general_parameter_index( &self, idx: GeneralParameterIndex, return_value: ParameterReturnValue, ) -> Result { match return_value { - ParameterReturnValue::Before => self.get_parameter_index_before(idx), - ParameterReturnValue::After => self.get_parameter_index_after(idx), - ParameterReturnValue::BeforeOrElseAfter => match self.get_parameter_index_before(idx) { + ParameterReturnValue::Before => self.get_general_parameter_index_before(idx), + ParameterReturnValue::After => self.get_general_parameter_index_after(idx), + ParameterReturnValue::BeforeOrElseAfter => match self.get_general_parameter_index_before(idx) { Ok(v) => Ok(v), - Err(_) => self.get_parameter_index_after(idx), + Err(_) => self.get_general_parameter_index_after(idx), }, - ParameterReturnValue::AfterOrElseBefore => match self.get_parameter_index_after(idx) { + ParameterReturnValue::AfterOrElseBefore => match self.get_general_parameter_index_after(idx) { Ok(v) => Ok(v), - Err(_) => self.get_parameter_index_before(idx), + Err(_) => self.get_general_parameter_index_before(idx), }, } } - fn get_parameter_index_before(&self, idx: GeneralParameterIndex) -> Result { + fn get_general_parameter_index_before(&self, idx: GeneralParameterIndex) -> Result { self.parameters_before .general .get_index(*idx) .ok_or(StateError::GeneralIndexParameterIndexNotFound(idx)) } - fn get_parameter_index_after(&self, idx: GeneralParameterIndex) -> Result { + fn get_general_parameter_index_after(&self, idx: GeneralParameterIndex) -> Result { self.parameters_after .general .get_index(*idx) .ok_or(StateError::GeneralIndexParameterIndexNotFound(idx)) } - pub fn set_parameter_index_before( + pub fn set_general_parameter_index_before( &mut self, idx: GeneralParameterIndex, value: Option, @@ -935,7 +934,7 @@ impl State { Ok(()) } - pub fn set_parameter_index_after( + pub fn set_general_parameter_index_after( &mut self, idx: GeneralParameterIndex, value: Option, @@ -998,26 +997,26 @@ impl State { Ok(()) } - pub fn get_multi_parameter_value( + pub fn get_general_multi_parameter_value( &self, idx: GeneralParameterIndex, key: &str, return_value: ParameterReturnValue, ) -> Result { match return_value { - ParameterReturnValue::Before => self.get_multi_parameter_value_before(idx, key), - ParameterReturnValue::After => self.get_multi_parameter_value_after(idx, key), - ParameterReturnValue::BeforeOrElseAfter => match self.get_multi_parameter_value_before(idx, key) { + ParameterReturnValue::Before => self.get_general_multi_parameter_value_before(idx, key), + ParameterReturnValue::After => self.get_general_multi_parameter_value_after(idx, key), + ParameterReturnValue::BeforeOrElseAfter => match self.get_general_multi_parameter_value_before(idx, key) { Ok(v) => Ok(v), - Err(_) => self.get_multi_parameter_value_after(idx, key), + Err(_) => self.get_general_multi_parameter_value_after(idx, key), }, - ParameterReturnValue::AfterOrElseBefore => match self.get_multi_parameter_value_after(idx, key) { + ParameterReturnValue::AfterOrElseBefore => match self.get_general_multi_parameter_value_after(idx, key) { Ok(v) => Ok(v), - Err(_) => self.get_multi_parameter_value_before(idx, key), + Err(_) => self.get_general_multi_parameter_value_before(idx, key), }, } } - fn get_multi_parameter_value_before( + fn get_general_multi_parameter_value_before( &self, idx: GeneralParameterIndex, key: &str, @@ -1036,7 +1035,7 @@ impl State { .copied() } - fn get_multi_parameter_value_after( + fn get_general_multi_parameter_value_after( &self, idx: GeneralParameterIndex, key: &str, @@ -1055,27 +1054,27 @@ impl State { .copied() } - pub fn get_multi_parameter_index( + pub fn get_general_multi_parameter_index( &self, idx: GeneralParameterIndex, key: &str, return_value: ParameterReturnValue, ) -> Result { match return_value { - ParameterReturnValue::Before => self.get_multi_parameter_index_before(idx, key), - ParameterReturnValue::After => self.get_multi_parameter_index_after(idx, key), - ParameterReturnValue::BeforeOrElseAfter => match self.get_multi_parameter_index_before(idx, key) { + ParameterReturnValue::Before => self.get_general_multi_parameter_index_before(idx, key), + ParameterReturnValue::After => self.get_general_multi_parameter_index_after(idx, key), + ParameterReturnValue::BeforeOrElseAfter => match self.get_general_multi_parameter_index_before(idx, key) { Ok(v) => Ok(v), - Err(_) => self.get_multi_parameter_index_after(idx, key), + Err(_) => self.get_general_multi_parameter_index_after(idx, key), }, - ParameterReturnValue::AfterOrElseBefore => match self.get_multi_parameter_index_after(idx, key) { + ParameterReturnValue::AfterOrElseBefore => match self.get_general_multi_parameter_index_after(idx, key) { Ok(v) => Ok(v), - Err(_) => self.get_multi_parameter_index_before(idx, key), + Err(_) => self.get_general_multi_parameter_index_before(idx, key), }, } } - fn get_multi_parameter_index_before( + fn get_general_multi_parameter_index_before( &self, idx: GeneralParameterIndex, key: &str, @@ -1094,7 +1093,7 @@ impl State { .copied() } - fn get_multi_parameter_index_after( + fn get_general_multi_parameter_index_after( &self, idx: GeneralParameterIndex, key: &str, @@ -1113,7 +1112,7 @@ impl State { .copied() } - pub fn set_multi_parameter_value_before( + pub fn set_general_multi_parameter_value_before( &mut self, idx: GeneralParameterIndex, value: Option, @@ -1133,7 +1132,7 @@ impl State { Ok(()) } - pub fn set_multi_parameter_value_after( + pub fn set_general_multi_parameter_value_after( &mut self, idx: GeneralParameterIndex, value: Option, diff --git a/pywr-core/src/test_utils.rs b/pywr-core/src/test_utils.rs index fa00f429..923f1525 100644 --- a/pywr-core/src/test_utils.rs +++ b/pywr-core/src/test_utils.rs @@ -1,12 +1,15 @@ -use crate::agg_funcs::AggFuncF64; -use crate::metric::MetricF64; -use crate::models::{Model, ModelDomain}; /// Utilities for unit tests. /// TODO move this to its own local crate ("test-utilities") as part of a workspace. -use crate::network::{Network, NetworkError}; -use crate::node::StorageInitialVolume; -use crate::parameters::{AggregatedParameter, Array2Parameter, ConstantParameter, GeneralParameter}; -use crate::recorders::{AssertionF64Recorder, AssertionU64Recorder}; +/// +use crate::agg_funcs::AggFuncF64; +use crate::metric::{UnresolvedMetricF64, UnresolvedMetricU64}; +use crate::models::{Model, ModelBuilder, ModelDomain, ModelDomainBuilder}; +use crate::network::NetworkBuilder; +use crate::node::{NodeBuilder, NodeType, UnresolvedNode, UnresolvedStorageInitialVolume}; +use crate::parameters::{ + AggregatedParameterBuilder, Array2ParameterBuilder, ConstantParameterBuilder, ParameterBuilder, ParameterName, +}; +use crate::recorders::{AssertionF64RecorderBuilder, AssertionU64RecorderBuilder}; use crate::scenario::{ScenarioDomainBuilder, ScenarioGroupBuilder}; #[cfg(feature = "cbc")] use crate::solvers::CbcSolver; @@ -31,7 +34,7 @@ use crate::solvers::Solver; feature = "microlp" ))] use crate::solvers::SolverSettings; -use crate::timestep::{TimeDomain, TimestepDuration, Timestepper}; +use crate::timestep::{TimeDomainBuilder, TimestepDuration}; use chrono::{Days, NaiveDate}; use csv::{Reader, ReaderBuilder}; use float_cmp::{F64Margin, approx_eq}; @@ -41,7 +44,7 @@ use rand_distr::{Distribution, Normal}; use std::num::NonZeroU64; use std::path::PathBuf; -pub fn default_timestepper() -> Timestepper { +pub fn default_time_domain_builder() -> TimeDomainBuilder { let start = NaiveDate::from_ymd_opt(2020, 1, 1) .unwrap() .and_hms_opt(0, 0, 0) @@ -51,115 +54,101 @@ pub fn default_timestepper() -> Timestepper { .and_hms_opt(0, 0, 0) .unwrap(); let duration = TimestepDuration::Days(NonZeroU64::new(1).unwrap()); - Timestepper::new(start, end, duration) + TimeDomainBuilder::new(start, end, duration) } -pub fn default_time_domain() -> TimeDomain { - default_timestepper().try_into().unwrap() +pub fn default_domain_builder() -> ModelDomainBuilder { + ModelDomainBuilder::new(default_time_domain_builder()) } pub fn default_domain() -> ModelDomain { - default_time_domain().into() + default_domain_builder().build().unwrap() } -pub fn default_model() -> Model { - let domain = default_domain(); - let network = Network::default(); - Model::new(domain, network) -} +/// Add a simple test network with three nodes. +pub fn simple_network(builder: &mut NetworkBuilder, inflow_scenario: &str, num_inflow_scenarios: usize) { + let mut input_node = NodeBuilder::input("input"); + input_node.max_flow(UnresolvedMetricF64::new_parameter_before("inflow")); -/// Create a simple test network with three nodes. -pub fn simple_network(network: &mut Network, inflow_scenario_index: usize, num_inflow_scenarios: usize) { - let input_node = network.add_input_node("input", None).unwrap(); - let link_node = network.add_link_node("link", None).unwrap(); - let output_node = network.add_output_node("output", None).unwrap(); + builder.node(input_node); + builder.node(NodeBuilder::link("link")); - network.connect_nodes(input_node, link_node).unwrap(); - network.connect_nodes(link_node, output_node).unwrap(); + let mut output_node = NodeBuilder::output("output"); + output_node + .max_flow(UnresolvedMetricF64::new_parameter_before("total-demand")) + .cost(UnresolvedMetricF64::new_parameter_before("demand-cost")); + builder.node(output_node); - let inflow = Array::from_shape_fn((366, num_inflow_scenarios), |(i, j)| 1.0 + i as f64 + j as f64); - let inflow = Array2Parameter::new("inflow".into(), inflow, inflow_scenario_index, None); + builder.connect("input", None, "link", None); + builder.connect("link", None, "output", None); - let inflow = network.add_simple_parameter(Box::new(inflow)).unwrap(); + let inflow = Array::from_shape_fn((366, num_inflow_scenarios), |(i, j)| 1.0 + i as f64 + j as f64); + let inflow = Array2ParameterBuilder::new("inflow".into(), inflow, inflow_scenario); - let input_node = network.get_mut_node_by_name("input", None).unwrap(); - input_node - .set_max_flow_constraint(Some(inflow.into_metric_f64_before())) - .unwrap(); + builder.parameters().f64(Box::new(inflow)); let base_demand = 10.0; + let demand_factor = ConstantParameterBuilder::new("demand-factor".into(), 1.2); + builder.parameters().f64(Box::new(demand_factor)); - let demand_factor = ConstantParameter::new("demand-factor".into(), 1.2); - let demand_factor = network.add_const_parameter(Box::new(demand_factor)).unwrap(); - - let total_demand: AggregatedParameter = AggregatedParameter::new( - "total-demand".into(), - &[base_demand.into(), demand_factor.into_metric_f64_before()], - AggFuncF64::Product, - ); - let total_demand = network.add_parameter(Box::new(total_demand)).unwrap(); + let mut total_demand = AggregatedParameterBuilder::new("total-demand".into(), AggFuncF64::Product); + total_demand.metric(base_demand.into()); + total_demand.metric(UnresolvedMetricF64::new_parameter_before("demand-factor")); - let demand_cost = ConstantParameter::new("demand-cost".into(), -10.0); - let demand_cost = network.add_const_parameter(Box::new(demand_cost)).unwrap(); + builder.parameters().f64(Box::new(total_demand)); - let output_node = network.get_mut_node_by_name("output", None).unwrap(); - output_node - .set_max_flow_constraint(Some(total_demand.into_metric_f64_before())) - .unwrap(); - output_node.set_cost(Some(demand_cost.into_metric_f64_before())); + let demand_cost = ConstantParameterBuilder::new("demand-cost".into(), -10.0); + builder.parameters().f64(Box::new(demand_cost)); } -/// Create a simple test model with three nodes. -pub fn simple_model(num_scenarios: usize, timestepper: Option) -> Model { - let mut scenario_builder = ScenarioDomainBuilder::default(); - let scenario_group = ScenarioGroupBuilder::new("test-scenario", num_scenarios) - .build() - .unwrap(); - scenario_builder = scenario_builder.with_group(scenario_group).unwrap(); - let domain = ModelDomain::try_from(timestepper.unwrap_or_else(default_timestepper), scenario_builder).unwrap(); - let mut network = Network::default(); +/// Create a simple test model builder +pub fn simple_model(num_scenarios: usize, time_builder: Option) -> ModelBuilder { + let scenario = "test-scenario"; - let idx = domain - .scenarios() - .group_index("test-scenario") - .expect("Could not find scenario group"); + let mut network_builder = NetworkBuilder::default(); + simple_network(&mut network_builder, scenario, num_scenarios); + + let mut scenario_builder = ScenarioDomainBuilder::default(); + let scenario_group = ScenarioGroupBuilder::new(scenario, num_scenarios).build().unwrap(); + scenario_builder = scenario_builder.with_group(scenario_group).unwrap(); - simple_network(&mut network, idx, num_scenarios); + let mut domain_builder = ModelDomainBuilder::new(time_builder.unwrap_or_else(default_time_domain_builder)); + domain_builder.scenario(scenario_builder); - Model::new(domain, network) + ModelBuilder::new(domain_builder, network_builder) } /// A test model with a single storage node. -pub fn simple_storage_model() -> Model { - let mut network = Network::default(); - let storage_node = network - .add_storage_node( - "reservoir", - None, - StorageInitialVolume::Absolute(100.0), - None, - Some(100.0.into()), - ) - .unwrap(); - let output_node = network.add_output_node("output", None).unwrap(); +pub fn simple_storage_network() -> NetworkBuilder { + let mut builder = NetworkBuilder::default(); - network.connect_nodes(storage_node, output_node).unwrap(); + let mut storage_node = NodeBuilder::storage("reservoir"); + storage_node.initial_volume(UnresolvedStorageInitialVolume::Absolute(100.0)); + storage_node.max_volume(100.0.into()); + builder.node(storage_node); + + let mut output_node = NodeBuilder::output("output"); + output_node + .max_flow(UnresolvedMetricF64::new_parameter_before("demand")) + .cost(UnresolvedMetricF64::new_parameter_before("demand-cost")); + builder.node(output_node); + + builder.connect("reservoir", None, "output", None); // Apply demand to the model // TODO convenience function for adding a constant constraint. - let demand = ConstantParameter::new("demand".into(), 10.0); - let demand = network.add_const_parameter(Box::new(demand)).unwrap(); + let demand = ConstantParameterBuilder::new("demand".into(), 10.0); + builder.parameters().f64(Box::new(demand)); - let demand_cost = ConstantParameter::new("demand-cost".into(), -10.0); - let demand_cost = network.add_const_parameter(Box::new(demand_cost)).unwrap(); + let demand_cost = ConstantParameterBuilder::new("demand-cost".into(), -10.0); + builder.parameters().f64(Box::new(demand_cost)); - let output_node = network.get_mut_node_by_name("output", None).unwrap(); - output_node - .set_max_flow_constraint(Some(demand.into_metric_f64_before())) - .unwrap(); - output_node.set_cost(Some(demand_cost.into_metric_f64_before())); + builder +} - Model::new(default_time_domain().into(), network) +pub fn simple_storage_model() -> ModelBuilder { + let network = simple_storage_network(); + ModelBuilder::new(default_domain_builder(), network) } /// Add the given parameter to the given model along with an assertion recorder that asserts @@ -170,13 +159,14 @@ pub fn simple_storage_model() -> Model { /// /// See [`AssertionF64Recorder`] for more information. pub fn run_and_assert_parameter( - model: &mut Model, - parameter: Box>, + mut model_builder: ModelBuilder, + parameter: Box>, expected_values: Array2, ulps: Option, epsilon: Option, ) { - let p_idx = model.network_mut().add_parameter(parameter).unwrap(); + let p_name = parameter.name().clone(); + model_builder.network_builder().parameters().f64(parameter); let start = NaiveDate::from_ymd_opt(2020, 1, 1) .unwrap() @@ -186,10 +176,23 @@ pub fn run_and_assert_parameter( .checked_add_days(Days::new(expected_values.nrows() as u64 - 1)) .unwrap(); - let rec = AssertionF64Recorder::new("assert", p_idx.into_metric_f64_before(), expected_values, ulps, epsilon); + let mut rec = AssertionF64RecorderBuilder::new( + "assert", + UnresolvedMetricF64::new_parameter_before(p_name), + expected_values, + ); - model.network_mut().add_recorder(Box::new(rec)).unwrap(); - run_all_solvers(model, &[], &[], &[]) + if let Some(ulps) = ulps { + rec.ulps(ulps); + } + if let Some(eps) = epsilon { + rec.epsilon(eps); + } + + model_builder.network_builder().recorder(Box::new(rec)); + + let model = model_builder.build().unwrap(); + run_all_solvers(&model, &[], &[], &[]) } /// Add the given parameter to the given model along with an assertion recorder that asserts @@ -200,11 +203,12 @@ pub fn run_and_assert_parameter( /// /// See [`AssertionU64Recorder`] for more information. pub fn run_and_assert_parameter_u64( - model: &mut Model, - parameter: Box>, + mut model_builder: ModelBuilder, + parameter: Box>, expected_values: Array2, ) { - let p_idx = model.network_mut().add_index_parameter(parameter).unwrap(); + let p_name = parameter.name().clone(); + model_builder.network_builder().parameters().u64(parameter); let start = NaiveDate::from_ymd_opt(2020, 1, 1) .unwrap() @@ -214,10 +218,15 @@ pub fn run_and_assert_parameter_u64( .checked_add_days(Days::new(expected_values.nrows() as u64 - 1)) .unwrap(); - let rec = AssertionU64Recorder::new("assert", p_idx.into_metric_u64_before(), expected_values); + let rec = AssertionU64RecorderBuilder::new( + "assert", + UnresolvedMetricU64::new_index_parameter_before(p_name), + expected_values, + ); + model_builder.network_builder().recorder(Box::new(rec)); - model.network_mut().add_recorder(Box::new(rec)).unwrap(); - run_all_solvers(model, &[], &[], &[]) + let model = model_builder.build().unwrap(); + run_all_solvers(&model, &[], &[], &[]) } /// A trait with a verify method for checking model outputs. @@ -538,60 +547,63 @@ where /// Make a simple system with random inputs. fn make_simple_system( - network: &mut Network, + builder: &mut NetworkBuilder, suffix: &str, num_timesteps: usize, num_inflow_scenarios: usize, - inflow_scenario_group_index: usize, + inflow_scenario: &str, rng: &mut R, -) -> Result<(), NetworkError> { - let input_idx = network.add_input_node("input", Some(suffix))?; - let link_idx = network.add_link_node("link", Some(suffix))?; - let output_idx = network.add_output_node("output", Some(suffix))?; +) { + let inflow_parameter_name = ParameterName::new("inflow", Some(suffix)); + let input_cost = rng.random_range(-20.0..-5.00); + let mut input_node = NodeBuilder::input("input"); + input_node + .sub_name(suffix) + .max_flow(UnresolvedMetricF64::new_parameter_before(inflow_parameter_name.clone())) + .cost(input_cost.into()); - network.connect_nodes(input_idx, link_idx)?; - network.connect_nodes(link_idx, output_idx)?; + builder.node(input_node); - let inflow_distr: Normal = Normal::new(9.0, 1.0).unwrap(); + let mut link_node = NodeBuilder::link("link"); + link_node.sub_name(suffix); + builder.node(link_node); - let mut inflow = ndarray::Array2::zeros((num_timesteps, num_inflow_scenarios)); + let outflow_distr = Normal::new(8.0, 3.0).unwrap(); + let mut outflow: f64 = outflow_distr.sample(rng); + outflow = outflow.max(0.0); - for x in inflow.iter_mut() { - *x = inflow_distr.sample(rng).max(0.0); - } - let inflow = Array2Parameter::new( - format!("inflow-{suffix}").as_str().into(), - inflow, - inflow_scenario_group_index, - None, - ); - let idx = network.add_simple_parameter(Box::new(inflow))?; + let mut output_node = NodeBuilder::output("output"); + output_node + .sub_name(suffix) + .max_flow(outflow.into()) + .cost((-500.0).into()); + builder.node(output_node); - network.set_node_max_flow("input", Some(suffix), Some(idx.into_metric_f64_before()))?; + builder.connect("input", Some(suffix), "link", Some(suffix)); + builder.connect("link", Some(suffix), "output", Some(suffix)); - let input_cost = rng.random_range(-20.0..-5.00); - network.set_node_cost("input", Some(suffix), Some(input_cost.into()))?; + let inflow_distr: Normal = Normal::new(9.0, 1.0).unwrap(); - let outflow_distr = Normal::new(8.0, 3.0).unwrap(); - let mut outflow: f64 = outflow_distr.sample(rng); - outflow = outflow.max(0.0); + let mut inflow = Array2::zeros((num_timesteps, num_inflow_scenarios)); - network.set_node_max_flow("output", Some(suffix), Some(outflow.into()))?; + for x in inflow.iter_mut() { + *x = inflow_distr.sample(rng).max(0.0); + } - network.set_node_cost("output", Some(suffix), Some((-500.0).into()))?; + let inflow = Array2ParameterBuilder::new(inflow_parameter_name, inflow, inflow_scenario); - Ok(()) + builder.parameters().f64(Box::new(inflow)); } /// Make a simple connections between random systems /// /// fn make_simple_connections( - model: &mut Network, + network_builder: &mut NetworkBuilder, num_systems: usize, density: usize, rng: &mut R, -) -> Result<(), NetworkError> { +) { let num_connections = (num_systems.pow(2) * density / 100 / 2).max(1); let mut connections_added: usize = 0; @@ -604,37 +616,34 @@ fn make_simple_connections( continue; } - let name = format!("{i:04}->{j:04}"); + let sub_name = format!("{i:04}->{j:04}"); + let name = UnresolvedNode::new("transfer", Some(&sub_name)); + + let transfer_already_exists = network_builder.node_builder(&name).is_some(); - if let Ok(idx) = model.add_link_node("transfer", Some(&name)) { + if !transfer_already_exists { + // Add a new transfers if it doesn't already exist let transfer_cost = rng.random_range(0.0..1.0); - model.set_node_cost("transfer", Some(&name), Some(transfer_cost.into()))?; - let from_suffix = format!("sys-{i:04}"); - let from_idx = model - .get_node_index_by_name("link", Some(&from_suffix)) - .expect("missing link node"); - let to_suffix = format!("sys-{j:04}"); - let to_idx = model - .get_node_index_by_name("link", Some(&to_suffix)) - .expect("missing link node"); + let mut node_builder = NodeBuilder::new("transfer", NodeType::Link); + node_builder.sub_name(&sub_name).cost(transfer_cost.into()); - model.connect_nodes(from_idx, idx)?; - model.connect_nodes(idx, to_idx)?; + network_builder.node(node_builder); + + network_builder.connect("link", Some(&format!("sys-{i:04}")), "transfer", Some(&sub_name)); + network_builder.connect("transfer", Some(&sub_name), "link", Some(&format!("sys-{j:04}"))); connections_added += 1; } } - - Ok(()) } -pub fn make_random_model( +pub fn make_random_model_builder( num_systems: usize, density: usize, num_scenarios: usize, rng: &mut R, -) -> Result { +) -> ModelBuilder { let start = NaiveDate::from_ymd_opt(2020, 1, 1) .unwrap() .and_hms_opt(0, 0, 0) @@ -644,7 +653,7 @@ pub fn make_random_model( .and_hms_opt(0, 0, 0) .unwrap(); let duration = TimestepDuration::Days(NonZeroU64::new(1).unwrap()); - let timestepper = Timestepper::new(start, end, duration); + let time_builder = TimeDomainBuilder::new(start, end, duration); let mut scenario_builder = ScenarioDomainBuilder::default(); let scenario_group = ScenarioGroupBuilder::new("test-scenario", num_scenarios) @@ -654,38 +663,33 @@ pub fn make_random_model( .with_group(scenario_group) .expect("Could not add scenario group"); - let domain = ModelDomain::try_from(timestepper, scenario_builder).expect("Could not create model domain"); + let mut domain_builder = ModelDomainBuilder::new(time_builder); + domain_builder.scenario(scenario_builder); - let inflow_scenario_group_index = domain - .scenarios() - .group_index("test-scenario") - .expect("Could not find scenario group."); + // Quickly build the domain to determine its shape so we can setup the correct input data. + let (num_timesteps, num_inflow_scenarios) = domain_builder.clone().build().unwrap().shape(); - let (num_timesteps, num_inflow_scenarios) = domain.shape(); - - let mut network = Network::default(); + let mut network_builder = NetworkBuilder::default(); for i in 0..num_systems { let suffix = format!("sys-{i:04}"); make_simple_system( - &mut network, + &mut network_builder, &suffix, num_timesteps, num_inflow_scenarios, - inflow_scenario_group_index, + "test-scenario", rng, - )?; + ); } - make_simple_connections(&mut network, num_systems, density, rng)?; - - let model = Model::new(domain, network); + make_simple_connections(&mut network_builder, num_systems, density, rng); - Ok(model) + ModelBuilder::new(domain_builder, network_builder) } #[cfg(all(test, feature = "ipm-simd"))] mod tests { - use super::make_random_model; + use super::make_random_model_builder; use crate::solvers::{SimdIpmF64Solver, SimdIpmSolverSettings}; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; @@ -699,7 +703,9 @@ mod tests { // Make a consistent random number generator // ChaCha8 should be consistent across builds and platforms let mut rng = ChaCha8Rng::seed_from_u64(0); - let model = make_random_model(n_sys, density, n_sc, &mut rng).unwrap(); + let model = make_random_model_builder(n_sys, density, n_sc, &mut rng) + .build() + .expect("Failed to builder random model."); let settings = SimdIpmSolverSettings::default(); model diff --git a/pywr-core/src/timestep.rs b/pywr-core/src/timestep.rs index d384ff20..2cd4d20a 100644 --- a/pywr-core/src/timestep.rs +++ b/pywr-core/src/timestep.rs @@ -263,7 +263,7 @@ impl Add for Timestep { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum TimestepDuration { Hours(NonZeroU64), Days(NonZeroU64), @@ -271,7 +271,7 @@ pub enum TimestepDuration { } #[derive(Debug, Error)] -pub enum TimestepError { +pub enum TimeDomainBuilderError { #[error("Could not create timestep range: {source}")] RangeGenerationError { #[source] @@ -281,22 +281,24 @@ pub enum TimestepError { GenerationError(String), #[error("Pywr does not currently support timesteps of varying duration")] DurationMismatch, + #[error("The time domain defined no timesteps.")] + NoTimesteps, } -#[derive(Debug)] -pub struct Timestepper { +#[derive(Debug, Clone)] +pub struct TimeDomainBuilder { start: NaiveDateTime, end: NaiveDateTime, timestep: TimestepDuration, } -impl Timestepper { +impl TimeDomainBuilder { pub fn new(start: NaiveDateTime, end: NaiveDateTime, timestep: TimestepDuration) -> Self { Self { start, end, timestep } } /// Create a vector of `Timestep`s between the start and end dates at the given duration. - fn timesteps(&self) -> Result, TimestepError> { + fn timesteps(&self) -> Result, TimeDomainBuilderError> { match &self.timestep { TimestepDuration::Hours(hours) => { Ok(self.generate_timesteps_from_fixed_duration(PywrDuration::from_hours(hours.get() as i64))) @@ -324,7 +326,7 @@ impl Timestepper { /// Creates a vector of `Timestep`s between the start and end dates for a given frequency `&str`. /// /// Valid frequency strings are those that can be parsed by `polars::time::Duration::parse`. See: [https://docs.rs/polars-time/latest/polars_time/struct.Duration.html#method.parse] - fn generate_timesteps_from_frequency(&self, frequency: &str) -> Result, TimestepError> { + fn generate_timesteps_from_frequency(&self, frequency: &str) -> Result, TimeDomainBuilderError> { let duration = polars::time::Duration::parse(frequency); // Need to add an extra day to the end date so that the duration of the last timestep can be calculated. @@ -353,10 +355,10 @@ impl Timestepper { TimeUnit::Milliseconds, None, ) - .map_err(|source| TimestepError::RangeGenerationError { source })? + .map_err(|source| TimeDomainBuilderError::RangeGenerationError { source })? .as_datetime_iter() - .map(|x| x.ok_or(TimestepError::GenerationError(frequency.to_string()))) - .collect::, TimestepError>>()?; + .map(|x| x.ok_or(TimeDomainBuilderError::GenerationError(frequency.to_string()))) + .collect::, TimeDomainBuilderError>>()?; let timesteps = dates .windows(2) @@ -369,6 +371,18 @@ impl Timestepper { Ok(timesteps) } + + pub fn build(&self) -> Result { + let timesteps = self.timesteps()?; + let duration = timesteps + .first() + .ok_or_else(|| TimeDomainBuilderError::NoTimesteps)? + .duration; + match timesteps.iter().all(|t| t.duration == duration) { + true => Ok(TimeDomain { timesteps, duration }), + false => Err(TimeDomainBuilderError::DurationMismatch), + } + } } /// The time domain that a model will be simulated over. @@ -406,19 +420,6 @@ impl TimeDomain { } } -impl TryFrom for TimeDomain { - type Error = TimestepError; - - fn try_from(value: Timestepper) -> Result { - let timesteps = value.timesteps()?; - let duration = timesteps.first().expect("No time-steps defined.").duration; - match timesteps.iter().all(|t| t.duration == duration) { - true => Ok(Self { timesteps, duration }), - false => Err(TimestepError::DurationMismatch), - } - } -} - #[cfg(test)] mod test { use chrono::{NaiveDateTime, TimeDelta}; @@ -426,7 +427,7 @@ mod test { use crate::timestep::{PywrDuration, SECS_IN_DAY, is_leap_year}; - use super::{TimestepDuration, Timestepper}; + use super::{TimeDomainBuilder, TimestepDuration}; #[test] fn test_days() { @@ -434,7 +435,7 @@ mod test { let end = NaiveDateTime::parse_from_str("2021-01-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Days(NonZeroU64::new(1).unwrap()); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let timesteps = timestepper.timesteps().unwrap(); assert!(timesteps.len() == 10); assert_eq!(timesteps.first().unwrap().duration, TimeDelta::days(1)); @@ -442,7 +443,7 @@ mod test { let timestep = TimestepDuration::Frequency(String::from("1d")); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let timesteps = timestepper.timesteps().unwrap(); assert!(timesteps.len() == 10); assert_eq!(timesteps.first().unwrap().duration, TimeDelta::days(1)); @@ -455,7 +456,7 @@ mod test { let end = NaiveDateTime::parse_from_str("2021-01-22 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Frequency(String::from("1w")); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let timesteps = timestepper.timesteps().unwrap(); assert!(timesteps.len() == 4); @@ -469,7 +470,7 @@ mod test { let end = NaiveDateTime::parse_from_str("2021-04-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Frequency(String::from("1mo")); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let timesteps = timestepper.timesteps().unwrap(); assert!(timesteps.len() == 4); assert_eq!(timesteps[0].duration, TimeDelta::days(31)); @@ -484,7 +485,7 @@ mod test { let end = NaiveDateTime::parse_from_str("2021-01-01 16:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Frequency(String::from("1h")); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let timesteps = timestepper.timesteps().unwrap(); assert!(timesteps.len() == 5); assert_eq!(timesteps.first().unwrap().duration, TimeDelta::hours(1)); diff --git a/pywr-core/src/virtual_storage.rs b/pywr-core/src/virtual_storage.rs index 91105408..3cbf0370 100644 --- a/pywr-core/src/virtual_storage.rs +++ b/pywr-core/src/virtual_storage.rs @@ -1,159 +1,291 @@ use crate::NodeIndex; -use crate::metric::{MetricF64, MetricF64Error, SimpleMetricF64, SimpleMetricF64Error}; -use crate::network::{Network, NetworkError}; -use crate::node::{NodeMeta, StorageConstraints, StorageInitialVolume}; +use crate::aggregated_node::RelationshipBuildError; +use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, SimpleMetricF64Error, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps, VirtualStorageIndex}; +use crate::node::{NodeMeta, StorageConstraints, StorageInitialVolume, UnresolvedNode, UnresolvedStorageInitialVolume}; use crate::state::{NetworkStateError, State, StateError, VirtualStorageState}; use crate::timestep::Timestep; use chrono::{Datelike, Month, NaiveDate, NaiveDateTime}; -use std::fmt; -use std::fmt::{Display, Formatter}; use std::num::NonZeroUsize; -use std::ops::{Deref, DerefMut}; use thiserror::Error; -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug, Hash)] -pub struct VirtualStorageIndex(usize); - -impl Deref for VirtualStorageIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for VirtualStorageIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Default)] -pub struct VirtualStorageVec { - nodes: Vec, -} - -impl Deref for VirtualStorageVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.nodes - } -} - -impl DerefMut for VirtualStorageVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nodes - } -} - -impl VirtualStorageVec { - pub fn get(&self, index: &VirtualStorageIndex) -> Option<&VirtualStorage> { - self.nodes.get(index.0) - } - - pub fn get_mut(&mut self, index: &VirtualStorageIndex) -> Option<&mut VirtualStorage> { - self.nodes.get_mut(index.0) - } - - pub fn push_new(&mut self, builder: VirtualStorageBuilder) -> Result { - if self.nodes.iter().any(|n| n.name() == builder.name) { - return Err(NetworkError::NodeAlreadyExists { - name: builder.name.clone(), - sub_name: builder.sub_name.clone(), - }); - } - - let node_index = VirtualStorageIndex(self.nodes.len()); - let node = builder.build(node_index); - self.nodes.push(node); - Ok(node_index) - } +#[derive(Debug, Error)] +pub enum VirtualStorageNodeBuilderError { + #[error("Index not found in resolution map.")] + IndexNotFound, + #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] + ResolveMetricF64Error { + attr: String, + #[source] + source: MetricF64ResolutionError, + }, + #[error("Could not simplify f64 metric for `{attr}`: {source}")] + CouldNotSimplifyMetricF64 { + attr: String, + #[source] + source: MetricF64Error, + }, + #[error("Reference to node not found.")] + NodeIndexNotFound { node: UnresolvedNode }, + #[error("Error building relationship: {0}")] + RelationshipBuildError(#[from] RelationshipBuildError), } -/// Builder for creating a [`VirtualStorage`] node. -pub struct VirtualStorageBuilder { - name: String, - sub_name: Option, - nodes: Vec, +/// Builder for creating a [`VirtualStorageNode`] node. +pub struct VirtualStorageNodeBuilder { + name: UnresolvedNode, + nodes: Vec, factors: Option>, - initial_volume: StorageInitialVolume, + initial_volume: UnresolvedStorageInitialVolume, reset: VirtualStorageReset, reset_volume: VirtualStorageResetVolume, rolling_window: Option, active_period: VirtualStorageActivePeriod, + cost: Option, + max_volume: Option, + min_volume: Option, } -impl VirtualStorageBuilder { - pub fn new(name: &str, nodes: &[NodeIndex]) -> Self { +impl VirtualStorageNodeBuilder { + pub fn new(name: &str, nodes: &[UnresolvedNode]) -> Self { + let name = UnresolvedNode::new(name, None); Self { - name: name.to_string(), - sub_name: None, + name, nodes: nodes.to_vec(), factors: None, - initial_volume: StorageInitialVolume::Absolute(0.0), + initial_volume: UnresolvedStorageInitialVolume::Absolute(0.0), reset: VirtualStorageReset::Never, reset_volume: VirtualStorageResetVolume::Initial, rolling_window: None, active_period: VirtualStorageActivePeriod::Always, + cost: None, + max_volume: None, + min_volume: None, } } - pub fn sub_name(mut self, sub_name: &str) -> Self { - self.sub_name = Some(sub_name.to_string()); + pub fn name(&self) -> &UnresolvedNode { + &self.name + } + + /// The slice of regular node names linked to the virtual storage builder. + pub fn nodes(&self) -> &[UnresolvedNode] { + &self.nodes + } + + pub fn sub_name(&mut self, sub_name: &str) -> &mut Self { + self.name.set_sub_name(Some(sub_name)); self } - pub fn factors(mut self, factors: &[f64]) -> Self { + pub fn factors(&mut self, factors: &[f64]) -> &mut Self { self.factors = Some(factors.to_vec()); self } - pub fn initial_volume(mut self, initial_volume: StorageInitialVolume) -> Self { + pub fn initial_volume(&mut self, initial_volume: UnresolvedStorageInitialVolume) -> &mut Self { self.initial_volume = initial_volume; self } - pub fn reset(mut self, reset: VirtualStorageReset) -> Self { + pub fn cost(&mut self, cost: UnresolvedMetricF64) -> &mut Self { + self.cost = Some(cost); + self + } + + pub fn max_volume(&mut self, max_volume: UnresolvedMetricF64) -> &mut Self { + self.max_volume = Some(max_volume); + self + } + + pub fn min_volume(&mut self, min_volume: UnresolvedMetricF64) -> &mut Self { + self.min_volume = Some(min_volume); + self + } + + pub fn reset(&mut self, reset: VirtualStorageReset) -> &mut Self { self.reset = reset; self } - pub fn reset_volume(mut self, reset_volume: VirtualStorageResetVolume) -> Self { + pub fn reset_volume(&mut self, reset_volume: VirtualStorageResetVolume) -> &mut Self { self.reset_volume = reset_volume; self } - pub fn rolling_window(mut self, rolling_window: NonZeroUsize) -> Self { + pub fn rolling_window(&mut self, rolling_window: NonZeroUsize) -> &mut Self { self.rolling_window = Some(rolling_window); self } - pub fn active_period(mut self, active_period: VirtualStorageActivePeriod) -> Self { + pub fn active_period(&mut self, active_period: VirtualStorageActivePeriod) -> &mut Self { self.active_period = active_period; self } - pub fn build(self, index: VirtualStorageIndex) -> VirtualStorage { - // Default to unit factors if none provided - let factors = self.factors.unwrap_or(vec![1.0; self.nodes.len()]); + /// Build a [`StorageConstraints`] from the builder. + fn build_storage_constraints( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + let min_volume = self + .min_volume + .as_ref() + .map(|min_volume| { + min_volume + .resolve(resolution_maps) + .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { + attr: "min_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| VirtualStorageNodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "max_volume".to_string(), + source, + }) + }) + .transpose()?; + + let max_volume = self + .max_volume + .as_ref() + .map(|max_volume| { + max_volume + .resolve(resolution_maps) + .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { + attr: "max_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| VirtualStorageNodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "max_volume".to_string(), + source, + }) + }) + .transpose()?; + + let storage_constraints = StorageConstraints::new(min_volume, max_volume); + + Ok(storage_constraints) + } + + fn build_storage_initial_volume( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + match &self.initial_volume { + UnresolvedStorageInitialVolume::Absolute(iv) => Ok(StorageInitialVolume::Absolute(*iv)), + UnresolvedStorageInitialVolume::Proportional(iv) => Ok(StorageInitialVolume::Proportional(*iv)), + UnresolvedStorageInitialVolume::DistributedAbsolute { + absolute, + prior_max_volume, + } => { + let prior_max_volume = prior_max_volume + .resolve(resolution_maps) + .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { + attr: "prior_max_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| VirtualStorageNodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "prior_max_volume".to_string(), + source, + })?; + Ok(StorageInitialVolume::DistributedAbsolute { + absolute: *absolute, + prior_max_volume, + }) + } + UnresolvedStorageInitialVolume::DistributedProportional { + proportion, + total_volume, + prior_max_volume, + } => { + let total_volume = total_volume + .resolve(resolution_maps) + .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { + attr: "total_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| VirtualStorageNodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "total_volume".to_string(), + source, + })?; + let prior_max_volume = prior_max_volume + .resolve(resolution_maps) + .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { + attr: "prior_max_volume".to_string(), + source, + })? + .try_into() + .map_err(|source| VirtualStorageNodeBuilderError::CouldNotSimplifyMetricF64 { + attr: "prior_max_volume".to_string(), + source, + })?; + + Ok(StorageInitialVolume::DistributedProportional { + total_volume, + proportion: *proportion, + prior_max_volume, + }) + } + } + } + + pub fn build( + &self, + resolution_maps: &ResolutionMaps, + ) -> Result { + let index = resolution_maps + .virtual_storage_node + .get(&self.name) + .ok_or(VirtualStorageNodeBuilderError::IndexNotFound)?; + let meta = NodeMeta::from_unresolved_name(self.name.clone(), *index); - VirtualStorage { - meta: NodeMeta::new(index, &self.name, self.sub_name.as_deref()), - nodes: self.nodes, + // Default to unit factors if none provided + let factors = self.factors.clone().unwrap_or_else(|| vec![1.0; self.nodes.len()]); + let nodes = self + .nodes + .iter() + .map(|unresolved| { + resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { + VirtualStorageNodeBuilderError::NodeIndexNotFound { + node: unresolved.clone(), + } + }) + }) + .collect::, _>>()?; + + let cost = self + .cost + .as_ref() + .map(|cost| cost.resolve(resolution_maps)) + .transpose() + .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { + attr: "cost".to_string(), + source, + })?; + + let vs = VirtualStorageNode { + meta, + nodes, factors, - initial_volume: self.initial_volume, - storage_constraints: StorageConstraints::new(None, None), - reset: self.reset, - reset_volume: self.reset_volume, + initial_volume: self.build_storage_initial_volume(resolution_maps)?, + storage_constraints: self.build_storage_constraints(resolution_maps)?, + reset: self.reset.clone(), + reset_volume: self.reset_volume.clone(), rolling_window: self.rolling_window, - active_period: self.active_period, - cost: None, - } + active_period: self.active_period.clone(), + cost, + }; + + Ok(vs) } } /// Defines when the virtual storage volume should be reset. +#[derive(Clone)] pub enum VirtualStorageReset { Never, DayOfYear { day: u32, month: Month }, @@ -161,12 +293,14 @@ pub enum VirtualStorageReset { } /// When resetting the virtual storage volume, this enum defines how much volume to set. +#[derive(Clone)] pub enum VirtualStorageResetVolume { Initial, Max, } /// Active periods for a virtual storage node. +#[derive(Clone)] pub enum VirtualStorageActivePeriod { Always, Period { @@ -229,7 +363,7 @@ pub enum VirtualStorageError { /// for the choices. In addition, a rolling window can be provided as a number of time-steps. /// Volume is recovered into the virtual storage after this number of time-steps once per time-step /// with the oldest value added back to the volume. -pub struct VirtualStorage { +pub struct VirtualStorageNode { meta: NodeMeta, nodes: Vec, factors: Vec, @@ -242,7 +376,7 @@ pub struct VirtualStorage { cost: Option, } -impl VirtualStorage { +impl VirtualStorageNode { pub fn name(&self) -> &str { self.meta.name() } @@ -272,18 +406,6 @@ impl VirtualStorage { } } - pub fn set_cost(&mut self, cost: Option) { - self.cost = cost; - } - - pub fn set_min_volume_constraint(&mut self, min_volume: Option) { - self.storage_constraints.min_volume = min_volume; - } - - pub fn set_max_volume_constraint(&mut self, max_volume: Option) { - self.storage_constraints.max_volume = max_volume; - } - pub fn before(&self, timestep: &Timestep, state: &mut State) -> Result<(), VirtualStorageError> { let do_reset = if timestep.is_first() { // Set the initial volume if it is the first timestep. @@ -381,17 +503,17 @@ fn months_since_last_reset(current: &NaiveDateTime, last_reset: &NaiveDateTime) #[cfg(test)] mod tests { - use crate::metric::MetricF64; - use crate::models::Model; - use crate::network::Network; - use crate::node::{CostAggFunc, StorageInitialVolume}; - use crate::parameters::ControlCurveInterpolatedParameter; - use crate::recorders::{AssertionF64Recorder, AssertionFnRecorder}; + use crate::metric::UnresolvedMetricF64; + use crate::models::ModelBuilder; + use crate::network::NetworkBuilder; + use crate::node::{CostAggFunc, NodeBuilder, NodeType, UnresolvedNode, UnresolvedStorageInitialVolume}; + use crate::parameters::ControlCurveInterpolatedParameterBuilder; + use crate::recorders::{AssertionF64RecorderBuilder, AssertionFnRecorderBuilder}; use crate::scenario::ScenarioIndex; - use crate::test_utils::{default_timestepper, run_all_solvers, simple_model}; - use crate::timestep::{Timestep, TimestepDuration, Timestepper}; + use crate::test_utils::{default_domain_builder, run_all_solvers, simple_model}; + use crate::timestep::{TimeDomainBuilder, Timestep, TimestepDuration}; use crate::virtual_storage::{ - VirtualStorageActivePeriod, VirtualStorageBuilder, VirtualStorageReset, months_since_last_reset, + VirtualStorageActivePeriod, VirtualStorageNodeBuilder, VirtualStorageReset, months_since_last_reset, }; use chrono::{Datelike, Month, NaiveDate}; use ndarray::Array; @@ -444,68 +566,85 @@ mod tests { /// Test the virtual storage constraints #[test] fn test_basic_virtual_storage() { - let mut network = Network::default(); + let mut network_builder = NetworkBuilder::default(); + + let input_node = NodeBuilder::new("input", NodeType::Input); + network_builder.node(input_node); - let input_node = network.add_input_node("input", None).unwrap(); - let link_node0 = network.add_link_node("link", Some("0")).unwrap(); - let output_node0 = network.add_output_node("output", Some("0")).unwrap(); + let mut link_node0 = NodeBuilder::new("link", NodeType::Link); + link_node0.sub_name("0"); + network_builder.node(link_node0); - network.connect_nodes(input_node, link_node0).unwrap(); - network.connect_nodes(link_node0, output_node0).unwrap(); + let mut output_node0 = NodeBuilder::new("output", NodeType::Output); + output_node0.sub_name("0").max_flow(10.0.into()).cost((-10.0).into()); + network_builder.node(output_node0); - let link_node1 = network.add_link_node("link", Some("1")).unwrap(); - let output_node1 = network.add_output_node("output", Some("1")).unwrap(); + network_builder.connect("input", None, "link", Some("0")); + network_builder.connect("link", Some("0"), "output", Some("0")); - network.connect_nodes(input_node, link_node1).unwrap(); - network.connect_nodes(link_node1, output_node1).unwrap(); + let mut link_node1 = NodeBuilder::new("link", NodeType::Link); + link_node1.sub_name("1"); + network_builder.node(link_node1); + + let mut output_node1 = NodeBuilder::new("output", NodeType::Output); + output_node1.sub_name("1").max_flow(10.0.into()).cost((-10.0).into()); + network_builder.node(output_node1); + + network_builder.connect("input", None, "link", Some("1")); + network_builder.connect("link", Some("1"), "output", Some("1")); // Virtual storage with contributions from link-node0 than link-node1 - let vs_builder = VirtualStorageBuilder::new("virtual-storage", &[link_node0, link_node1]) - .factors(&[2.0, 1.0]) - .initial_volume(StorageInitialVolume::Absolute(100.0)) - .reset(VirtualStorageReset::Never); + let mut vs_builder = VirtualStorageNodeBuilder::new( + "virtual-storage", + &[ + UnresolvedNode::new("link", Some("0")), + UnresolvedNode::new("link", Some("1")), + ], + ); - let vs_idx = network.add_virtual_storage_node(vs_builder).unwrap(); - network - .set_virtual_storage_max_volume("virtual-storage", None, Some(100.0.into())) - .unwrap(); + vs_builder + .factors(&[2.0, 1.0]) + .initial_volume(UnresolvedStorageInitialVolume::Absolute(100.0)) + .reset(VirtualStorageReset::Never) + .max_volume(100.0.into()); - // Setup a demand on output-0 and output-1 - for sub_name in &["0", "1"] { - let output_node = network.get_mut_node_by_name("output", Some(sub_name)).unwrap(); - output_node.set_max_flow_constraint(Some(10.0.into())).unwrap(); - output_node.set_cost(Some((-10.0).into())); - } + network_builder.virtual_storage_node(vs_builder); // With a demand of 10 on each link node. The virtual storage will deplete at a rate of // 30 per day. let expected_vol = |ts: &Timestep, _si: &ScenarioIndex| (70.0 - ts.index as f64 * 30.0).max(0.0); - let recorder = AssertionFnRecorder::new( + let recorder = AssertionFnRecorderBuilder::new( "vs-volume", - MetricF64::VirtualStorageVolume(vs_idx), + UnresolvedMetricF64::VirtualStorageVolume("virtual-storage".into()), expected_vol, - None, - None, ); - network.add_recorder(Box::new(recorder)).unwrap(); + + network_builder.recorder(Box::new(recorder)); + // Set-up assertion for "link" node - let idx = network.get_node_by_name("link", Some("0")).unwrap().index(); let expected = |ts: &Timestep, _si: &ScenarioIndex| { if ts.index < 3 { 10.0 } else { 0.0 } }; - let recorder = AssertionFnRecorder::new("link-0-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionFnRecorderBuilder::new( + "link-0-flow", + UnresolvedMetricF64::NodeOutFlow(UnresolvedNode::new("link", Some("0"))), + expected, + ); + network_builder.recorder(Box::new(recorder)); // Set-up assertion for "input" node - let idx = network.get_node_by_name("link", Some("1")).unwrap().index(); let expected = |ts: &Timestep, _si: &ScenarioIndex| { if ts.index < 4 { 10.0 } else { 0.0 } }; - let recorder = AssertionFnRecorder::new("link-1-flow", MetricF64::NodeOutFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionFnRecorderBuilder::new( + "link-1-flow", + UnresolvedMetricF64::NodeOutFlow(UnresolvedNode::new("link", Some("1"))), + expected, + ); + network_builder.recorder(Box::new(recorder)); - let domain = default_timestepper().try_into().unwrap(); - let model = Model::new(domain, network); + let domain = default_domain_builder(); + let model = ModelBuilder::new(domain, network_builder).build().unwrap(); // Test all solvers run_all_solvers(&model, &["ipm-ocl", "ipm-simd"], &[], &[]); } @@ -513,31 +652,36 @@ mod tests { #[test] /// Test virtual storage node costs fn test_virtual_storage_node_costs() { - let mut model = simple_model(1, None); - let network = model.network_mut(); + let mut model_builder = simple_model(1, None); + let network_builder = model_builder.network_builder(); // Make the input use any VS costs - let node = network.get_mut_node_by_name("input", None).unwrap(); - node.set_cost_agg_func(Some(CostAggFunc::Max)).unwrap(); + let name = "input".into(); + let node = network_builder.node_builder(&name).unwrap(); + node.cost_agg_func(CostAggFunc::Max); - let nodes = vec![network.get_node_index_by_name("input", None).unwrap()]; + let nodes = vec!["input".into()]; // Virtual storage node cost is high enough to prevent any flow - let vs_builder = VirtualStorageBuilder::new("vs", &nodes) - .initial_volume(StorageInitialVolume::Proportional(1.0)) - .reset(VirtualStorageReset::Never); + let mut vs_builder = VirtualStorageNodeBuilder::new("vs", &nodes); + vs_builder + .initial_volume(UnresolvedStorageInitialVolume::Proportional(1.0)) + .reset(VirtualStorageReset::Never) + .cost(20.0.into()) + .max_volume(100.0.into()); - network.add_virtual_storage_node(vs_builder).unwrap(); - network.set_virtual_storage_cost("vs", None, Some(20.0.into())).unwrap(); - network - .set_virtual_storage_max_volume("vs", None, Some(100.0.into())) - .unwrap(); + network_builder.virtual_storage_node(vs_builder); let expected = Array::zeros((366, 1)); - let idx = network.get_node_by_name("output", None).unwrap().index(); - let recorder = AssertionF64Recorder::new("output-flow", MetricF64::NodeInFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + let recorder = AssertionF64RecorderBuilder::new( + "output-flow", + UnresolvedMetricF64::NodeInFlow("output".into()), + expected, + ); + network_builder.recorder(Box::new(recorder)); + + let model = model_builder.build().unwrap(); // Test all solvers run_all_solvers(&model, &["ipm-ocl", "ipm-simd"], &[], &[]); @@ -558,38 +702,32 @@ mod tests { .unwrap(); let duration = TimestepDuration::Days(NonZeroU64::new(1).unwrap()); - let mut model = simple_model(1, Some(Timestepper::new(start, end, duration))); - let network = model.network_mut(); + let mut model_builder = simple_model(1, Some(TimeDomainBuilder::new(start, end, duration))); + let network_builder = model_builder.network_builder(); // Make the input use any VS costs - let node = network.get_mut_node_by_name("input", None).unwrap(); - node.set_cost_agg_func(Some(CostAggFunc::Max)).unwrap(); - - let nodes = vec![network.get_node_index_by_name("input", None).unwrap()]; + let name = "input".into(); + let node = network_builder.node_builder(&name).unwrap(); + node.cost_agg_func(CostAggFunc::Max); - let vs_builder = VirtualStorageBuilder::new("vs", &nodes) - .initial_volume(StorageInitialVolume::Proportional(1.0)) - .reset(VirtualStorageReset::NumberOfMonths { months: 1 }); + let mut vs_builder = VirtualStorageNodeBuilder::new("vs", &["input".into()]); + vs_builder + .initial_volume(UnresolvedStorageInitialVolume::Proportional(1.0)) + .reset(VirtualStorageReset::NumberOfMonths { months: 1 }) + .cost(UnresolvedMetricF64::new_parameter_before("cost")) + .max_volume(100.0.into()); - let vs_idx = network.add_virtual_storage_node(vs_builder).unwrap(); + network_builder.virtual_storage_node(vs_builder); // Virtual storage node cost increases with decreasing volume - let cost_param = ControlCurveInterpolatedParameter::new( + let mut cost_param = ControlCurveInterpolatedParameterBuilder::new( "cost".into(), - MetricF64::VirtualStorageProportionalVolume(vs_idx), - vec![], - vec![0.0.into(), 20.0.into()], + UnresolvedMetricF64::VirtualStorageProportionalVolume("vs".into()), ); - let cost_param = network.add_parameter(Box::new(cost_param)).unwrap(); + cost_param.value(0.0.into()).value(20.0.into()); - network - .set_virtual_storage_node_cost("vs", None, Some(cost_param.into_metric_f64_before())) - .unwrap(); - - network - .set_virtual_storage_max_volume("vs", None, Some(100.0.into())) - .unwrap(); + network_builder.parameters().f64(Box::new(cost_param)); let expected = |ts: &Timestep, _si: &ScenarioIndex| { // Calculate the current volume within each month @@ -607,9 +745,15 @@ mod tests { 0.0 } }; - let idx = network.get_node_by_name("output", None).unwrap().index(); - let recorder = AssertionFnRecorder::new("output-flow", MetricF64::NodeInFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + + let recorder = AssertionFnRecorderBuilder::new( + "output-flow", + UnresolvedMetricF64::NodeInFlow("output".into()), + expected, + ); + network_builder.recorder(Box::new(recorder)); + + let model = model_builder.build().unwrap(); // Test all solvers run_all_solvers(&model, &["ipm-ocl", "ipm-simd"], &[], &[]); @@ -618,22 +762,20 @@ mod tests { #[test] /// Test virtual storage rolling window constraint fn test_virtual_storage_node_rolling_constraint() { - let mut model = simple_model(1, None); - let network = model.network_mut(); - - let nodes = vec![network.get_node_index_by_name("input", None).unwrap()]; + let mut model_builder = simple_model(1, None); + let network_builder = model_builder.network_builder(); // Virtual storage with contributions from input // Max volume is 2.5 and is assumed to start full - let vs_builder = VirtualStorageBuilder::new("virtual-storage", &nodes) + let mut vs_builder = VirtualStorageNodeBuilder::new("virtual-storage", &["input".into()]); + vs_builder .factors(&[1.0]) - .initial_volume(StorageInitialVolume::Absolute(2.5)) + .initial_volume(UnresolvedStorageInitialVolume::Absolute(2.5)) .reset(VirtualStorageReset::Never) - .rolling_window(NonZeroUsize::new(5).unwrap()); - let _vs = network.add_virtual_storage_node(vs_builder); - network - .set_virtual_storage_max_volume("virtual-storage", None, Some(2.5.into())) - .unwrap(); + .rolling_window(NonZeroUsize::new(5).unwrap()) + .max_volume(2.5.into()); + + network_builder.virtual_storage_node(vs_builder); // Expected values will follow a pattern set by the first few time-steps let expected = |ts: &Timestep, _si: &ScenarioIndex| { @@ -647,9 +789,15 @@ mod tests { _ => panic!("Unexpected timestep index"), } }; - let idx = network.get_node_by_name("output", None).unwrap().index(); - let recorder = AssertionFnRecorder::new("output-flow", MetricF64::NodeInFlow(idx), expected, None, None); - network.add_recorder(Box::new(recorder)).unwrap(); + + let recorder = AssertionFnRecorderBuilder::new( + "output-flow", + UnresolvedMetricF64::NodeInFlow("output".into()), + expected, + ); + network_builder.recorder(Box::new(recorder)); + + let model = model_builder.build().unwrap(); // Test all solvers run_all_solvers(&model, &["ipm-ocl", "ipm-simd"], &[], &[]); diff --git a/pywr-schema/src/error.rs b/pywr-schema/src/error.rs index 2b8feaf8..ea37ad47 100644 --- a/pywr-schema/src/error.rs +++ b/pywr-schema/src/error.rs @@ -80,7 +80,7 @@ pub enum SchemaError { RbfEpsilonEstimation, #[error("Scenario error: {0}")] #[cfg(feature = "core")] - Scenario(#[from] pywr_core::scenario::ScenarioError), + Scenario(#[from] pywr_core::scenario::ScenarioDomainBuilderError), #[error("Inter-network transfer with name {0} not found")] InterNetworkTransferNotFound(String), #[error("Invalid rolling window definition on parameter {name}. Must convert to a positive integer.")] diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index 89d57d90..ce8a47f9 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -19,7 +19,7 @@ use pyo3::Python; use pyo3::{Bound, PyErr, PyResult, exceptions::PyRuntimeError, pyclass, pymethods, types::PyType}; #[cfg(feature = "core")] use pywr_core::{ - models::{Model, ModelDomain, MultiNetworkModel, MultiNetworkModelError}, + models::{Model, ModelBuilder, ModelDomain, MultiModelBuilder, MultiModelBuilderError, MultiNetworkModel}, timestep::TimestepDuration, }; use pywr_schema_macros::skip_serializing_none; @@ -144,7 +144,7 @@ impl From for Timestepper { } #[cfg(feature = "core")] -impl From for pywr_core::timestep::Timestepper { +impl From for pywr_core::timestep::TimeDomainBuilder { fn from(ts: Timestepper) -> Self { let timestep = match ts.timestep { Timestep::Hours { hours } => TimestepDuration::Hours(hours), @@ -213,7 +213,7 @@ pub struct ScenarioGroup { #[cfg(feature = "core")] impl TryInto for ScenarioGroup { - type Error = pywr_core::scenario::ScenarioError; + type Error = pywr_core::scenario::ScenarioDomainBuilderError; fn try_into(self) -> Result { let mut builder = pywr_core::scenario::ScenarioGroupBuilder::new(&self.name, self.size); @@ -365,7 +365,7 @@ impl TryFrom> for ScenarioDomain { #[cfg(feature = "core")] impl TryInto for ScenarioDomain { - type Error = pywr_core::scenario::ScenarioError; + type Error = pywr_core::scenario::ScenarioDomainBuilderError; fn try_into(self) -> Result { let mut builder = pywr_core::scenario::ScenarioDomainBuilder::default(); @@ -402,7 +402,7 @@ impl From for PyErr { #[cfg(feature = "core")] pub enum ModelSchemaBuildError { #[error("Failed to construct scenario builder: {0}")] - ScenarioBuilderError(#[from] pywr_core::scenario::ScenarioError), + ScenarioBuilderError(#[from] pywr_core::scenario::ScenarioDomainBuilderError), #[error("Failed to construct model domain: {0}")] CoreModelDomainError(#[from] pywr_core::models::ModelDomainError), #[error("Failed to construct the network: {source}")] @@ -652,7 +652,7 @@ pub struct MultiNetworkEntry { #[cfg(feature = "core")] pub enum MultiNetworkModelSchemaBuildError { #[error("Failed to construct scenario builder: {0}")] - ScenarioBuilderError(#[from] pywr_core::scenario::ScenarioError), + ScenarioBuilderError(#[from] pywr_core::scenario::ScenarioDomainBuilderError), #[error("Failed to construct model domain: {0}")] CoreModelDomainError(#[from] pywr_core::models::ModelDomainError), #[error("Failed to construct the network `{name}`: {source}")] diff --git a/pywr-schema/src/nodes/piecewise_storage.rs b/pywr-schema/src/nodes/piecewise_storage.rs index 339a04f0..ed9b99a8 100644 --- a/pywr-schema/src/nodes/piecewise_storage.rs +++ b/pywr-schema/src/nodes/piecewise_storage.rs @@ -401,7 +401,7 @@ impl PiecewiseStorageNode { })?; let metric = match attr { - PiecewiseStorageNodeAttribute::Volume => MetricF64::AggregatedNodeVolume(idx), + PiecewiseStorageNodeAttribute::Volume => MetricF64::AggregatedStorageNodeVolume(idx), PiecewiseStorageNodeAttribute::ProportionalVolume => MetricF64::AggregatedNodeProportionalVolume(idx), }; diff --git a/pywr-schema/src/nodes/virtual_nodes/aggregated.rs b/pywr-schema/src/nodes/virtual_nodes/aggregated.rs index d012ab78..11f40b40 100644 --- a/pywr-schema/src/nodes/virtual_nodes/aggregated.rs +++ b/pywr-schema/src/nodes/virtual_nodes/aggregated.rs @@ -381,7 +381,7 @@ impl AggregatedStorageNode { })?; let metric = match attr { - AggregatedStorageNodeAttribute::Volume => MetricF64::AggregatedNodeVolume(idx), + AggregatedStorageNodeAttribute::Volume => MetricF64::AggregatedStorageNodeVolume(idx), AggregatedStorageNodeAttribute::ProportionalVolume => MetricF64::AggregatedNodeProportionalVolume(idx), }; diff --git a/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs b/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs index 465fe9d2..ea2f6044 100644 --- a/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs +++ b/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs @@ -11,7 +11,7 @@ use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_initial_storage, try_convert_node_attr, try_convert_node_meta}; use crate::{ConversionError, node_attribute_subset_enum}; #[cfg(feature = "core")] -use pywr_core::{metric::MetricF64, timestep::TimeDomain, virtual_storage::VirtualStorageBuilder}; +use pywr_core::{metric::MetricF64, timestep::TimeDomain, virtual_storage::VirtualStorageNodeBuilder}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::{ @@ -251,8 +251,8 @@ impl VirtualStorageNode { pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { let node_idxs = self.node_indices_for_flow_constraints(network, args)?; - let mut builder = - VirtualStorageBuilder::new(self.meta.name.as_str(), &node_idxs).initial_volume(self.initial_volume.into()); + let mut builder = VirtualStorageNodeBuilder::new(self.meta.name.as_str(), &node_idxs) + .initial_volume(self.initial_volume.into()); if let Some(r) = self.reset.clone() { let reset = r.try_into()?; diff --git a/pywr-schema/src/timeseries/align_and_resample.rs b/pywr-schema/src/timeseries/align_and_resample.rs index bc7a7edb..894a74d9 100644 --- a/pywr-schema/src/timeseries/align_and_resample.rs +++ b/pywr-schema/src/timeseries/align_and_resample.rs @@ -116,7 +116,7 @@ mod tests { use pywr_core::{ models::ModelDomain, scenario::{ScenarioDomain, ScenarioDomainBuilder}, - timestep::{TimeDomain, TimestepDuration, Timestepper}, + timestep::{TimeDomain, TimeDomainBuilder, TimestepDuration}, }; use std::num::NonZeroU64; @@ -127,7 +127,7 @@ mod tests { let start = NaiveDateTime::parse_from_str("2021-01-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let end = NaiveDateTime::parse_from_str("2021-01-20 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Days(NonZeroU64::new(7).unwrap()); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let time_domain = TimeDomain::try_from(timestepper).unwrap(); let scenario_domain: ScenarioDomain = ScenarioDomainBuilder::default().build().unwrap(); @@ -182,7 +182,7 @@ mod tests { let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let end = NaiveDateTime::parse_from_str("2021-01-14 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Days(NonZeroU64::new(1).unwrap()); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let time_domain = TimeDomain::try_from(timestepper).unwrap(); let scenario_domain: ScenarioDomain = ScenarioDomainBuilder::default().build().unwrap(); @@ -222,7 +222,7 @@ mod tests { let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let end = NaiveDateTime::parse_from_str("2021-01-03 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); let timestep = TimestepDuration::Days(NonZeroU64::new(1).unwrap()); - let timestepper = Timestepper::new(start, end, timestep); + let timestepper = TimeDomainBuilder::new(start, end, timestep); let time_domain = TimeDomain::try_from(timestepper).unwrap(); let scenario_domain: ScenarioDomain = ScenarioDomainBuilder::default().build().unwrap(); diff --git a/pywr-schema/src/timeseries/mod.rs b/pywr-schema/src/timeseries/mod.rs index 29af6589..75cfdbdf 100644 --- a/pywr-schema/src/timeseries/mod.rs +++ b/pywr-schema/src/timeseries/mod.rs @@ -26,7 +26,7 @@ use pyo3::{PyErr, pyclass}; #[cfg(feature = "core")] use pywr_core::{ models::ModelDomain, - parameters::{Array1Parameter, Array2Parameter, ParameterIndex, ParameterName}, + parameters::{Array1ParameterBuilder, Array2ParameterBuilder, ParameterIndex, ParameterName}, }; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::parameters::DataFrameParameter as DataFrameParameterV1; @@ -72,7 +72,7 @@ pub enum TimeseriesError { PythonNotEnabled, #[cfg(feature = "core")] #[error("Scenario error: {0}")] - Scenario(#[from] pywr_core::scenario::ScenarioError), + Scenario(#[from] pywr_core::scenario::ScenarioDomainBuilderError), #[cfg(feature = "core")] #[error("Shape error: {0}")] NdarrayShape(#[from] ShapeError), From c708238dea6d298756cfed042a2cae7c19b7514f Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Tue, 9 Jun 2026 20:47:51 +0100 Subject: [PATCH 02/12] feat: Port pywr-schema to new builder pattern. # Conflicts: # pywr-schema/src/nodes/core.rs # pywr-schema/src/nodes/delay.rs # pywr-schema/src/nodes/loss_link.rs # pywr-schema/src/nodes/piecewise_link.rs # pywr-schema/src/nodes/river.rs # pywr-schema/src/nodes/river_gauge.rs # pywr-schema/src/parameters/discount_factor.rs # pywr-schema/src/parameters/hydropower.rs # pywr-schema/src/parameters/indexed_array.rs # pywr-schema/src/parameters/interpolated.rs # pywr-schema/src/parameters/polynomial.rs # pywr-schema/src/parameters/tables.rs --- .../listings/adding-a-parameter/src/main.rs | 83 +- pywr-cli/src/main.rs | 9 +- pywr-core/src/aggregated_node.rs | 109 +-- pywr-core/src/aggregated_storage_node.rs | 18 +- pywr-core/src/lib.rs | 6 + pywr-core/src/metric.rs | 152 ++-- pywr-core/src/models/mod.rs | 6 +- pywr-core/src/models/multi.rs | 75 +- pywr-core/src/models/simple.rs | 13 +- pywr-core/src/network.rs | 189 ++++- pywr-core/src/node.rs | 20 +- pywr-core/src/parameters/aggregated.rs | 35 +- pywr-core/src/parameters/aggregated_index.rs | 66 +- pywr-core/src/parameters/array.rs | 67 +- pywr-core/src/parameters/asymmetric.rs | 56 +- pywr-core/src/parameters/constant.rs | 11 +- pywr-core/src/parameters/constant_scenario.rs | 82 +- .../parameters/control_curves/apportion.rs | 61 +- .../src/parameters/control_curves/index.rs | 63 +- .../parameters/control_curves/interpolated.rs | 11 +- .../src/parameters/control_curves/mod.rs | 12 +- .../parameters/control_curves/piecewise.rs | 11 +- .../src/parameters/control_curves/simple.rs | 75 +- .../control_curves/volume_between.rs | 107 ++- pywr-core/src/parameters/deficit.rs | 56 +- pywr-core/src/parameters/delay.rs | 20 +- pywr-core/src/parameters/difference.rs | 89 ++- pywr-core/src/parameters/division.rs | 54 +- pywr-core/src/parameters/hydropower.rs | 106 ++- pywr-core/src/parameters/indexed_array.rs | 63 +- pywr-core/src/parameters/interpolated.rs | 76 +- pywr-core/src/parameters/max.rs | 57 +- pywr-core/src/parameters/min.rs | 57 +- pywr-core/src/parameters/mod.rs | 484 +++++++++--- pywr-core/src/parameters/multi_threshold.rs | 2 +- pywr-core/src/parameters/muskingum.rs | 87 ++- pywr-core/src/parameters/negative.rs | 53 +- pywr-core/src/parameters/negativemax.rs | 57 +- pywr-core/src/parameters/negativemin.rs | 57 +- pywr-core/src/parameters/offset.rs | 56 +- pywr-core/src/parameters/polynomial.rs | 75 +- pywr-core/src/parameters/profiles/daily.rs | 47 +- pywr-core/src/parameters/profiles/diurnal.rs | 47 +- pywr-core/src/parameters/profiles/mod.rs | 12 +- pywr-core/src/parameters/profiles/monthly.rs | 8 +- pywr-core/src/parameters/profiles/rbf.rs | 51 +- .../parameters/profiles/uniform_drawdown.rs | 61 +- pywr-core/src/parameters/profiles/weekly.rs | 56 +- pywr-core/src/parameters/py.rs | 345 +++++++-- pywr-core/src/parameters/rolling.rs | 2 +- pywr-core/src/parameters/threshold.rs | 81 +- pywr-core/src/parameters/vector.rs | 49 +- pywr-core/src/recorders/csv.rs | 123 ++- pywr-core/src/recorders/hdf.rs | 59 +- pywr-core/src/recorders/memory.rs | 60 +- pywr-core/src/recorders/metric_set.rs | 127 +++- pywr-core/src/recorders/mod.rs | 15 +- pywr-core/src/scenario.rs | 23 +- pywr-core/src/test_utils.rs | 28 +- pywr-core/src/virtual_storage.rs | 14 +- pywr-schema/src/edge.rs | 78 +- pywr-schema/src/error.rs | 12 - pywr-schema/src/metric.rs | 139 ++-- pywr-schema/src/metric_sets/mod.rs | 25 +- pywr-schema/src/model.rs | 286 +++---- pywr-schema/src/network.rs | 161 +--- pywr-schema/src/nodes/abstraction.rs | 220 ++---- pywr-schema/src/nodes/core.rs | 717 ++++++++---------- pywr-schema/src/nodes/delay.rs | 147 ++-- pywr-schema/src/nodes/loss_link.rs | 234 +++--- pywr-schema/src/nodes/mod.rs | 292 ++++--- pywr-schema/src/nodes/piecewise_link.rs | 121 ++- pywr-schema/src/nodes/piecewise_storage.rs | 396 +++++----- pywr-schema/src/nodes/placeholder.rs | 30 +- pywr-schema/src/nodes/reservoir.rs | 478 +++++------- pywr-schema/src/nodes/river.rs | 382 ++++------ pywr-schema/src/nodes/river_gauge.rs | 136 ++-- .../src/nodes/river_split_with_gauge.rs | 225 ++---- pywr-schema/src/nodes/turbine.rs | 130 ++-- .../src/nodes/virtual_nodes/aggregated.rs | 209 +++-- pywr-schema/src/nodes/virtual_nodes/mod.rs | 35 +- .../nodes/virtual_nodes/virtual_storage.rs | 98 ++- .../src/nodes/water_treatment_works.rs | 253 +++--- pywr-schema/src/outputs/csv.rs | 38 +- pywr-schema/src/outputs/hdf.rs | 13 +- pywr-schema/src/outputs/memory.rs | 15 +- pywr-schema/src/outputs/mod.rs | 2 +- pywr-schema/src/parameters/aggregated.rs | 50 +- .../src/parameters/asymmetric_switch.rs | 14 +- pywr-schema/src/parameters/control_curves.rs | 128 ++-- pywr-schema/src/parameters/core.rs | 133 ++-- pywr-schema/src/parameters/delay.rs | 29 +- pywr-schema/src/parameters/discount_factor.rs | 16 +- pywr-schema/src/parameters/hydropower.rs | 16 +- pywr-schema/src/parameters/indexed_array.rs | 26 +- pywr-schema/src/parameters/interpolated.rs | 19 +- pywr-schema/src/parameters/mod.rs | 136 +--- pywr-schema/src/parameters/offset.rs | 20 +- pywr-schema/src/parameters/placeholder.rs | 6 +- pywr-schema/src/parameters/polynomial.rs | 22 +- pywr-schema/src/parameters/profiles.rs | 114 ++- pywr-schema/src/parameters/python.rs | 75 +- pywr-schema/src/parameters/rolling.rs | 27 +- pywr-schema/src/parameters/tables.rs | 51 +- pywr-schema/src/parameters/thresholds.rs | 103 ++- .../src/timeseries/align_and_resample.rs | 40 +- pywr-schema/src/timeseries/mod.rs | 133 ++-- pywr-schema/tests/30-day-licence.json | 2 +- pywr-schema/tests/abstraction1.json | 2 +- pywr-schema/tests/abstraction2.json | 2 +- pywr-schema/tests/agg-storage1.json | 2 +- pywr-schema/tests/agg-storage2.json | 2 +- pywr-schema/tests/agg-storage3.json | 2 +- pywr-schema/tests/csv1.json | 2 +- pywr-schema/tests/csv2.json | 2 +- pywr-schema/tests/csv3.json | 2 +- pywr-schema/tests/daily-profile1.json | 2 +- pywr-schema/tests/delay1.json | 2 +- pywr-schema/tests/hdf1.json | 2 +- .../invalid/agg-storage-with-flow-node.json | 2 +- pywr-schema/tests/link_with_soft_max.json | 2 +- pywr-schema/tests/link_with_soft_min.json | 2 +- pywr-schema/tests/local-parameter1.json | 2 +- pywr-schema/tests/local-parameter2.json | 2 +- pywr-schema/tests/loss_link1.json | 2 +- pywr-schema/tests/loss_link2.json | 2 +- pywr-schema/tests/memory1.json | 2 +- pywr-schema/tests/multi1/model.json | 2 +- pywr-schema/tests/multi2/model.json | 2 +- pywr-schema/tests/muskingum1.json | 2 +- pywr-schema/tests/mutual-exclusivity1.json | 2 +- pywr-schema/tests/mutual-exclusivity2.json | 2 +- pywr-schema/tests/mutual-exclusivity3.json | 2 +- pywr-schema/tests/mutual-exclusivity4.json | 2 +- pywr-schema/tests/piecewise_link1.json | 2 +- pywr-schema/tests/piecewise_storage1.json | 2 +- pywr-schema/tests/piecewise_storage2.json | 2 +- pywr-schema/tests/piecewise_storage3.json | 2 +- pywr-schema/tests/piecewise_storage4.json | 2 +- pywr-schema/tests/python-agg-func1.json | 2 +- pywr-schema/tests/python-parameter1.json | 2 +- pywr-schema/tests/python-parameter2.json | 2 +- .../tests/reservoir-failure-levels1.json | 2 +- pywr-schema/tests/reservoir_with_river1.json | 2 +- pywr-schema/tests/reservoir_with_river2.json | 2 +- pywr-schema/tests/reservoir_with_spill1.json | 2 +- pywr-schema/tests/river_gauge1.json | 2 +- pywr-schema/tests/river_loss1.json | 2 +- .../tests/river_split_with_gauge1.json | 2 +- .../tests/river_split_with_gauge2.json | 2 +- pywr-schema/tests/seasonal-vs1.json | 2 +- pywr-schema/tests/seasonal-vs2.json | 2 +- pywr-schema/tests/simple1.json | 2 +- pywr-schema/tests/storage_max_volumes.json | 2 +- pywr-schema/tests/tbl-formats1.json | 2 +- pywr-schema/tests/test_invalid.rs | 2 +- pywr-schema/tests/test_models.rs | 8 +- pywr-schema/tests/timeseries.json | 2 +- pywr-schema/tests/timeseries2-hourly.json | 2 +- pywr-schema/tests/timeseries2.json | 2 +- pywr-schema/tests/timeseries3.json | 2 +- pywr-schema/tests/timeseries4.json | 2 +- pywr-schema/tests/timeseries5.json | 2 +- pywr-schema/tests/timeseries_pandas.json | 2 +- pywr-schema/tests/turbine1.json | 2 +- pywr-schema/tests/v1/breaklink-converted.json | 2 +- .../tests/v1/inline-parameter-converted.json | 2 +- .../v1/river_split_with_gauge1-converted.json | 2 +- pywr-schema/tests/v1/scenarios-converted.json | 2 +- .../tests/v1/timeseries-converted.json | 2 +- pywr-schema/tests/vs-with-piecewise-link.json | 2 +- pywr-schema/tests/vs-with-wtw1.json | 2 +- pywr-schema/tests/vs-with-wtw2.json | 2 +- pywr-schema/tests/vs-with-wtw3.json | 2 +- pywr-schema/tests/wtw1.json | 2 +- pywr-schema/tests/wtw2.json | 2 +- 176 files changed, 5653 insertions(+), 4515 deletions(-) diff --git a/pywr-book/listings/adding-a-parameter/src/main.rs b/pywr-book/listings/adding-a-parameter/src/main.rs index 2ef0927b..f2a4f4aa 100644 --- a/pywr-book/listings/adding-a-parameter/src/main.rs +++ b/pywr-book/listings/adding-a-parameter/src/main.rs @@ -1,9 +1,11 @@ #![allow(dead_code)] -use pywr_core::metric::MetricF64; -use pywr_core::network::Network; +use pywr_core::metric::{MetricF64, UnresolvedMetricF64}; +use pywr_core::network::{Network, ResolutionMaps}; use pywr_core::parameters::{ - GeneralParameter, Parameter, ParameterCalculationError, ParameterMeta, ParameterName, ParameterState, + BuiltParameter, GeneralCalculationError, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, + ParameterBuilder, ParameterMeta, ParameterName, ParameterState, }; +use pywr_core::resolve_metric_f64; use pywr_core::scenario::ScenarioIndex; use pywr_core::state::State; use pywr_core::timestep::Timestep; @@ -15,17 +17,7 @@ pub struct MaxParameter { threshold: f64, } // ANCHOR_END: parameter -// ANCHOR: impl-new -impl MaxParameter { - pub fn new(name: ParameterName, metric: MetricF64, threshold: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - threshold, - } - } -} -// ANCHOR_END: impl-new + // ANCHOR: impl-parameter impl Parameter for MaxParameter { fn meta(&self) -> &ParameterMeta { @@ -41,7 +33,7 @@ impl GeneralParameter for MaxParameter { model: &Network, state: &State, _internal_state: &mut Option>, - ) -> Result, ParameterCalculationError> { + ) -> Result, GeneralCalculationError> { // Current value let x = self.metric.get_value(model, state)?; Ok(Some(x.max(self.threshold))) @@ -55,10 +47,49 @@ impl GeneralParameter for MaxParameter { } } +// ANCHOR: impl-builder +pub struct MaxParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + threshold: f64, +} + +impl MaxParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl ParameterBuilder for MaxParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = MaxParameter { + meta: self.meta, + metric, + threshold: self.threshold, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} +// ANCHOR_END: impl-builder + // ANCHOR_END: impl-parameter mod schema { #[cfg(feature = "core")] - use pywr_core::parameters::ParameterIndex; + use pywr_core::parameters::ParameterName; use pywr_schema::metric::Metric; use pywr_schema::parameters::ParameterMeta; #[cfg(feature = "core")] @@ -78,16 +109,24 @@ mod schema { // ANCHOR: schema-impl #[cfg(feature = "core")] impl MaxParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, - ) -> Result, SchemaError> { - let idx = self.parameter.load(network, args, Some(&self.meta.name))?; + parent: Option<&str>, + ) -> Result<(), SchemaError> { + let idx = self.parameter.load(network, args, None)?; let threshold = self.threshold.unwrap_or(0.0); - let p = pywr_core::parameters::MaxParameter::new(self.meta.name.as_str().into(), idx, threshold); - Ok(network.add_parameter(Box::new(p))?) + let p = pywr_core::parameters::MaxParameterBuilder::new( + ParameterName::new(&self.meta.name, parent), + idx, + threshold, + ); + + network.parameters().f64(Box::new(p)); + + Ok(()) } } // ANCHOR_END: schema-impl diff --git a/pywr-cli/src/main.rs b/pywr-cli/src/main.rs index 104899a7..859bce2a 100644 --- a/pywr-cli/src/main.rs +++ b/pywr-cli/src/main.rs @@ -278,7 +278,8 @@ fn run( let data_path = data_path.or_else(|| path.parent()); let schema_v2: ModelSchema = serde_json::from_str(data.as_str()).unwrap(); - let model = schema_v2.build_model(data_path, output_path).unwrap(); + let builder = schema_v2.create_model_builder(data_path, output_path).unwrap(); + let model = builder.build().unwrap(); match *solver { Solver::Clp => { @@ -384,7 +385,8 @@ fn run_multi(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path let schema_v2: MultiNetworkModelSchema = serde_json::from_str(data.as_str()).unwrap(); - let model = schema_v2.build_model(data_path, output_path).unwrap(); + let builder = schema_v2.create_model_builder(data_path, output_path).unwrap(); + let model = builder.build().unwrap(); match *solver { Solver::Clp => model.run::(&ClpSolverSettings::default()), @@ -406,7 +408,8 @@ fn run_multi(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path fn run_random(num_systems: usize, density: usize, num_scenarios: usize, solver: &Solver) { let mut rng = ChaCha8Rng::seed_from_u64(0); - let model = make_random_model_builder(num_systems, density, num_scenarios, &mut rng).unwrap(); + let builder = make_random_model_builder(num_systems, density, num_scenarios, &mut rng); + let model = builder.build().unwrap(); match *solver { Solver::Clp => model.run::(&ClpSolverSettings::default()), diff --git a/pywr-core/src/aggregated_node.rs b/pywr-core/src/aggregated_node.rs index 6e185918..54f60004 100644 --- a/pywr-core/src/aggregated_node.rs +++ b/pywr-core/src/aggregated_node.rs @@ -158,12 +158,21 @@ impl Exclusivity { } } -#[derive(Default, Clone)] +#[derive(Clone)] pub struct ExclusivityBuilder { min_active: u64, max_active: u64, } +impl Default for ExclusivityBuilder { + fn default() -> Self { + Self { + min_active: 0, + max_active: 1, + } + } +} + impl ExclusivityBuilder { pub fn min_active(&mut self, min_active: u64) -> &mut Self { self.min_active = min_active; @@ -194,35 +203,6 @@ pub enum Relationship { Exclusive(Exclusivity), } -impl Relationship { - #[must_use] - pub fn new_ratio_factors(factors: &[MetricF64]) -> Self { - Relationship::Factored(Factors::Ratio { - factors: factors.to_vec(), - }) - } - - #[must_use] - pub fn new_proportion_factors(factors: &[MetricF64]) -> Self { - Relationship::Factored(Factors::Proportion { - factors: factors.to_vec(), - }) - } - - #[must_use] - pub fn new_coefficient_factors(factors: &[MetricF64], rhs: Option) -> Self { - Relationship::Factored(Factors::Coefficients { - factors: factors.to_vec(), - rhs, - }) - } - - #[must_use] - pub fn new_exclusive(min_active: u64, max_active: u64) -> Self { - Relationship::Exclusive(Exclusivity { min_active, max_active }) - } -} - #[derive(Debug, Error)] pub enum RelationshipBuildError { #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] @@ -577,8 +557,8 @@ pub struct AggregatedNodeBuilder { impl AggregatedNodeBuilder { #[must_use] - pub fn new(name: &str) -> Self { - let name = UnresolvedNode::new(name, None); + pub fn new>(name: N) -> Self { + let name = name.into(); Self { name, @@ -1105,7 +1085,7 @@ mod tests { use crate::metric::UnresolvedMetricF64; use crate::models::ModelBuilder; use crate::network::NetworkBuilder; - use crate::node::NodeBuilder; + use crate::node::{NodeBuilder, UnresolvedNode}; use crate::parameters::{MonthlyProfileParameterBuilder, ParameterName}; use crate::recorders::AssertionF64RecorderBuilder; use crate::state::ParameterReturnValue; @@ -1132,8 +1112,11 @@ mod tests { output_node0.sub_name("0").max_flow(100.0.into()).cost((-10.0).into()); builder.node(output_node0); - builder.connect("input", None, "link", Some("0")); - builder.connect("link", Some("0"), "output", Some("0")); + builder.connect("input", UnresolvedNode::new("link", Some("0"))); + builder.connect( + UnresolvedNode::new("link", Some("0")), + UnresolvedNode::new("output", Some("0")), + ); let mut link_node1 = NodeBuilder::link("link"); link_node1.sub_name("1"); @@ -1144,8 +1127,11 @@ mod tests { output_node1.sub_name("1"); builder.node(output_node1); - builder.connect("input", None, "link", Some("1")); - builder.connect("link", Some("1"), "output", Some("1")); + builder.connect("input", UnresolvedNode::new("link", Some("1"))); + builder.connect( + UnresolvedNode::new("link", Some("1")), + UnresolvedNode::new("output", Some("1")), + ); let mut relationship = RatioFactorsBuilder::default(); relationship.factor(2.0.into()).factor(1.0.into()); @@ -1201,8 +1187,11 @@ mod tests { output_node0.sub_name("0").max_flow(100.0.into()).cost((-10.0).into()); builder.node(output_node0); - builder.connect("input", None, "link", Some("0")); - builder.connect("link", Some("0"), "output", Some("0")); + builder.connect("input", UnresolvedNode::new("link", Some("0"))); + builder.connect( + UnresolvedNode::new("link", Some("0")), + UnresolvedNode::new("output", Some("0")), + ); let mut link_node1 = NodeBuilder::link("link"); link_node1.sub_name("1"); @@ -1213,8 +1202,11 @@ mod tests { output_node1.sub_name("1"); builder.node(output_node1); - builder.connect("input", None, "link", Some("1")); - builder.connect("link", Some("1"), "output", Some("1")); + builder.connect("input", UnresolvedNode::new("link", Some("1"))); + builder.connect( + UnresolvedNode::new("link", Some("1")), + UnresolvedNode::new("output", Some("1")), + ); let factor_profile_name = ParameterName::new("factor-profile", None); let factor_profile = MonthlyProfileParameterBuilder::new(factor_profile_name.clone(), [2.0; 12]); @@ -1280,8 +1272,11 @@ mod tests { output_node0.sub_name("0").max_flow(100.0.into()).cost((-10.0).into()); builder.node(output_node0); - builder.connect("input", None, "link", Some("0")); - builder.connect("link", Some("0"), "output", Some("0")); + builder.connect("input", UnresolvedNode::new("link", Some("0"))); + builder.connect( + UnresolvedNode::new("link", Some("0")), + UnresolvedNode::new("output", Some("0")), + ); let mut link_node1 = NodeBuilder::link("link"); link_node1.sub_name("1"); @@ -1292,8 +1287,11 @@ mod tests { output_node1.sub_name("1").max_flow(100.0.into()).cost((-5.0).into()); builder.node(output_node1); - builder.connect("input", None, "link", Some("1")); - builder.connect("link", Some("1"), "output", Some("1")); + builder.connect("input", UnresolvedNode::new("link", Some("1"))); + builder.connect( + UnresolvedNode::new("link", Some("1")), + UnresolvedNode::new("output", Some("1")), + ); let mut relationship = ExclusivityBuilder::default(); relationship.min_active(0).max_active(1); @@ -1354,8 +1352,11 @@ mod tests { output_node0.sub_name("0").max_flow(100.0.into()).cost((-5.0).into()); builder.node(output_node0); - builder.connect("input", None, "link", Some("0")); - builder.connect("link", Some("0"), "output", Some("0")); + builder.connect("input", UnresolvedNode::new("link", Some("0"))); + builder.connect( + UnresolvedNode::new("link", Some("0")), + UnresolvedNode::new("output", Some("0")), + ); let mut link_node1 = NodeBuilder::link("link"); link_node1.sub_name("1"); @@ -1366,8 +1367,11 @@ mod tests { output_node1.sub_name("1").max_flow(100.0.into()).cost((-15.0).into()); builder.node(output_node1); - builder.connect("input", None, "link", Some("1")); - builder.connect("link", Some("1"), "output", Some("1")); + builder.connect("input", UnresolvedNode::new("link", Some("1"))); + builder.connect( + UnresolvedNode::new("link", Some("1")), + UnresolvedNode::new("output", Some("1")), + ); let mut link_node2 = NodeBuilder::link("link"); link_node2.sub_name("2"); @@ -1378,8 +1382,11 @@ mod tests { output_node2.sub_name("2").max_flow(100.0.into()).cost((-5.0).into()); builder.node(output_node2); - builder.connect("input", None, "link", Some("2")); - builder.connect("link", Some("2"), "output", Some("2")); + builder.connect("input", UnresolvedNode::new("link", Some("2"))); + builder.connect( + UnresolvedNode::new("link", Some("2")), + UnresolvedNode::new("output", Some("2")), + ); let mut relationship = ExclusivityBuilder::default(); relationship.min_active(0).max_active(1); diff --git a/pywr-core/src/aggregated_storage_node.rs b/pywr-core/src/aggregated_storage_node.rs index 3fed57d2..1a7a8d77 100644 --- a/pywr-core/src/aggregated_storage_node.rs +++ b/pywr-core/src/aggregated_storage_node.rs @@ -12,20 +12,24 @@ pub struct AggregatedStorageNode { } impl AggregatedStorageNode { + #[must_use] pub fn name(&self) -> &str { self.meta.name() } /// Get a node's sub-name + #[must_use] pub fn sub_name(&self) -> Option<&str> { self.meta.sub_name() } /// Get a node's full name + #[must_use] pub fn full_name(&self) -> (&str, Option<&str>) { self.meta.full_name() } + #[must_use] pub fn index(&self) -> AggregatedStorageNodeIndex { *self.meta.index() } @@ -34,6 +38,7 @@ impl AggregatedStorageNode { self.nodes.iter() } + #[must_use] pub fn default_metric(&self) -> Vec { self.nodes.iter().map(|n| MetricF64::NodeOutFlow(*n)).collect() } @@ -53,8 +58,8 @@ pub struct AggregatedStorageNodeBuilder { } impl AggregatedStorageNodeBuilder { - pub fn new(name: &str) -> Self { - let name = UnresolvedNode::new(name, None); + pub fn new>(name: N) -> Self { + let name = name.into(); Self { name, @@ -62,6 +67,7 @@ impl AggregatedStorageNodeBuilder { } } + #[must_use] pub fn name(&self) -> &UnresolvedNode { &self.name } @@ -76,6 +82,14 @@ impl AggregatedStorageNodeBuilder { self } + /// Try to construct an [`AggregatedStorageNode`] from this builder. + /// + /// # Errors + /// + /// The construction will fail if the name of the aggregated storage node is not found in the + /// resolution maps, or if any of the nodes referenced by this builder are not found in the + /// resolution maps. + /// pub fn build( &self, resolution_maps: &ResolutionMaps, diff --git a/pywr-core/src/lib.rs b/pywr-core/src/lib.rs index f167756e..7feebaa9 100644 --- a/pywr-core/src/lib.rs +++ b/pywr-core/src/lib.rs @@ -16,3 +16,9 @@ pub mod test_utils; pub mod timestep; pub mod utils; pub mod virtual_storage; + +pub use aggregated_node::{AggregatedNode, AggregatedNodeBuilder, AggregatedNodeBuilderError}; +pub use aggregated_storage_node::{ + AggregatedStorageNode, AggregatedStorageNodeBuilder, AggregatedStorageNodeBuilderError, +}; +pub use node::NodeBuilder; diff --git a/pywr-core/src/metric.rs b/pywr-core/src/metric.rs index 7d1d0b58..604cff72 100644 --- a/pywr-core/src/metric.rs +++ b/pywr-core/src/metric.rs @@ -179,7 +179,7 @@ pub enum MetricF64 { AggregatedNodeInFlow(AggregatedNodeIndex), AggregatedNodeOutFlow(AggregatedNodeIndex), AggregatedStorageNodeVolume(AggregatedStorageNodeIndex), - AggregatedNodeProportionalVolume(AggregatedStorageNodeIndex), + AggregatedStorageNodeProportionalVolume(AggregatedStorageNodeIndex), EdgeFlow(EdgeIndex), MultiEdgeFlow { indices: Vec, @@ -305,7 +305,7 @@ impl MetricF64 { Ok(volume) } - MetricF64::AggregatedNodeProportionalVolume(idx) => { + MetricF64::AggregatedStorageNodeProportionalVolume(idx) => { let node = network .get_aggregated_storage_node(*idx) .ok_or(MetricF64Error::AggregatedStorageNodeIndexNotFound(*idx))?; @@ -459,27 +459,27 @@ pub enum MetricF64ResolutionError { InterNetworkTransferNotFound { transfer: String }, } +#[derive(Debug, Clone, PartialEq)] pub enum UnresolvedMetricF64 { NodeInFlow(UnresolvedNode), NodeOutFlow(UnresolvedNode), NodeVolume(UnresolvedNode), + NodeProportionalVolume(UnresolvedNode), NodeMaxVolume(UnresolvedNode), + NodeMaxFlow(UnresolvedNode), AggregatedNodeInFlow(UnresolvedNode), AggregatedNodeOutFlow(UnresolvedNode), AggregatedStorageNodeVolume(UnresolvedNode), + AggregatedStorageNodeProportionalVolume(UnresolvedNode), EdgeFlow(UnresolvedEdge), MultiEdgeFlow { - indices: Vec, + edges: Vec, name: String, }, ParameterValue { name: ParameterName, return_value: ParameterReturnValue, }, - IndexParameterValue { - name: ParameterName, - return_value: ParameterReturnValue, - }, MultiParameterValue { name: ParameterName, key: String, @@ -488,25 +488,67 @@ pub enum UnresolvedMetricF64 { VirtualStorageVolume(UnresolvedNode), VirtualStorageProportionalVolume(UnresolvedNode), MultiNodeInFlow { - indices: Vec, + nodes: Vec, name: String, }, MultiNodeOutFlow { - indices: Vec, + nodes: Vec, name: String, }, - NodeProportionalVolume(UnresolvedNode), + InterNetworkTransfer(String), Constant(f64), } impl UnresolvedMetricF64 { + /// Create a new [`Self::ParameterValue`] variant with the given parameter name and a return + /// value of [`ParameterReturnValue::Before`]. pub fn new_parameter_before>(name: N) -> Self { Self::ParameterValue { name: name.into(), return_value: ParameterReturnValue::Before, } } + + /// Create a new [`Self::MultiParameterValue`] variant with the given parameter name and a return + /// value of [`ParameterReturnValue::Before`]. + pub fn new_parameter_before_key>(name: N, key: &str) -> Self { + Self::MultiParameterValue { + name: name.into(), + key: key.to_string(), + return_value: ParameterReturnValue::Before, + } + } + + /// Create a new [`Self::ParameterValue`] variant with the given parameter name and a return + /// value of [`ParameterReturnValue::After`]. + pub fn new_parameter_after>(name: N) -> Self { + Self::ParameterValue { + name: name.into(), + return_value: ParameterReturnValue::After, + } + } + + /// Create a new [`Self::MultiParameterValue`] variant with the given parameter name and a return + /// value of [`ParameterReturnValue::After`]. + pub fn new_parameter_after_key>(name: N, key: &str) -> Self { + Self::MultiParameterValue { + name: name.into(), + key: key.to_string(), + return_value: ParameterReturnValue::After, + } + } + + /// Returns true if the constant value is a [`UnresolvedMetricF64::Constant`]. + pub fn is_constant(&self) -> bool { + matches!(self, UnresolvedMetricF64::Constant(_)) + } + + /// Returns true if the constant value is a [`UnresolvedMetricF64::Constant`] with a value of zero. + pub fn is_constant_zero(&self) -> bool { + matches!(self, UnresolvedMetricF64::Constant(v) if *v == 0.0) + } + pub fn resolve(&self, resolution_maps: &ResolutionMaps) -> Result { let m = match self { @@ -534,6 +576,15 @@ impl UnresolvedMetricF64 { })?; MetricF64::NodeVolume(*idx) } + UnresolvedMetricF64::NodeProportionalVolume(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + + MetricF64::NodeProportionalVolume(*idx) + } UnresolvedMetricF64::NodeMaxVolume(unresolved) => { let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { MetricF64ResolutionError::NodeNotFound { @@ -542,6 +593,14 @@ impl UnresolvedMetricF64 { })?; MetricF64::NodeMaxVolume(*idx) } + UnresolvedMetricF64::NodeMaxFlow(unresolved) => { + let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), + } + })?; + MetricF64::NodeMaxFlow(*idx) + } UnresolvedMetricF64::AggregatedNodeInFlow(unresolved) => { let idx = resolution_maps.aggregated_nodes.get(unresolved).ok_or_else(|| { MetricF64ResolutionError::AggregatedNodeNotFound { @@ -567,6 +626,15 @@ impl UnresolvedMetricF64 { })?; MetricF64::AggregatedStorageNodeVolume(*idx) } + UnresolvedMetricF64::AggregatedStorageNodeProportionalVolume(unresolved) => { + let idx = resolution_maps + .aggregated_storage_nodes + .get(unresolved) + .ok_or_else(|| MetricF64ResolutionError::AggregatedNodeNotFound { + aggregated_node: unresolved.clone(), + })?; + MetricF64::AggregatedStorageNodeProportionalVolume(*idx) + } UnresolvedMetricF64::EdgeFlow(unresolved) => { let idx = resolution_maps.edges.get(unresolved).ok_or_else(|| { MetricF64ResolutionError::EdgeNotFound { @@ -575,8 +643,8 @@ impl UnresolvedMetricF64 { })?; MetricF64::EdgeFlow(*idx) } - UnresolvedMetricF64::MultiEdgeFlow { indices, name } => { - let resolved = indices + UnresolvedMetricF64::MultiEdgeFlow { edges, name } => { + let resolved = edges .iter() .map(|unresolved| { resolution_maps.edges.get(unresolved).copied().ok_or_else(|| { @@ -592,22 +660,19 @@ impl UnresolvedMetricF64 { } } UnresolvedMetricF64::ParameterValue { name, return_value } => { - let idx = resolution_maps.parameters_f64.get(name).ok_or_else(|| { - MetricF64ResolutionError::ParameterNotFound { - parameter: name.clone(), - } - })?; + match resolution_maps.parameters_f64.get(name) { + Some(idx) => idx.into_metric_f64(*return_value), + None => { + // Not found as a F64 parameter; try index parameter instead. + let idx = resolution_maps.parameters_u64.get(name).ok_or_else(|| { + MetricF64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; - idx.into_metric_f64(*return_value) - } - UnresolvedMetricF64::IndexParameterValue { name, return_value } => { - let idx = resolution_maps.parameters_u64.get(name).ok_or_else(|| { - MetricF64ResolutionError::ParameterNotFound { - parameter: name.clone(), + idx.into_metric_f64(*return_value) } - })?; - - idx.into_metric_f64(*return_value) + } } UnresolvedMetricF64::MultiParameterValue { name, @@ -640,7 +705,7 @@ impl UnresolvedMetricF64 { MetricF64::VirtualStorageProportionalVolume(*idx) } - UnresolvedMetricF64::MultiNodeInFlow { name, indices } => { + UnresolvedMetricF64::MultiNodeInFlow { name, nodes: indices } => { let resolved = indices .iter() .map(|unresolved| { @@ -657,7 +722,7 @@ impl UnresolvedMetricF64 { name: name.clone(), } } - UnresolvedMetricF64::MultiNodeOutFlow { name, indices } => { + UnresolvedMetricF64::MultiNodeOutFlow { name, nodes: indices } => { let resolved = indices .iter() .map(|unresolved| { @@ -674,15 +739,6 @@ impl UnresolvedMetricF64 { name: name.clone(), } } - UnresolvedMetricF64::NodeProportionalVolume(unresolved) => { - let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { - MetricF64ResolutionError::NodeNotFound { - node: unresolved.clone(), - } - })?; - - MetricF64::NodeProportionalVolume(*idx) - } UnresolvedMetricF64::InterNetworkTransfer(unresolved) => { let idx = resolution_maps.inter_network_transfers.get(unresolved).ok_or_else(|| { MetricF64ResolutionError::InterNetworkTransferNotFound { @@ -886,10 +942,12 @@ pub enum MetricU64ResolutionError { NodeNotFound { node: UnresolvedNode }, #[error("Parameter not found when resolving U64 metric: {parameter}")] ParameterNotFound { parameter: ParameterName }, + #[error("Inter-network transfer not found when resolving U64 metric: {transfer}")] + InterNetworkTransferNotFound { transfer: String }, } pub enum UnresolvedMetricU64 { - IndexParameterValue { + ParameterValue { name: ParameterName, return_value: ParameterReturnValue, }, @@ -898,20 +956,20 @@ pub enum UnresolvedMetricU64 { key: String, return_value: ParameterReturnValue, }, - InterNetworkTransfer(MultiNetworkTransferIndex), + InterNetworkTransfer(String), Constant(u64), } impl UnresolvedMetricU64 { - pub fn new_index_parameter_before>(name: N) -> Self { - Self::IndexParameterValue { + pub fn new_parameter_before>(name: N) -> Self { + Self::ParameterValue { name: name.into(), return_value: ParameterReturnValue::Before, } } pub fn resolve(&self, resolution_maps: &ResolutionMaps) -> Result { let m = match self { - UnresolvedMetricU64::IndexParameterValue { name, return_value } => { + UnresolvedMetricU64::ParameterValue { name, return_value } => { let idx = resolution_maps.parameters_u64.get(name).ok_or_else(|| { MetricU64ResolutionError::ParameterNotFound { parameter: name.clone(), @@ -933,8 +991,14 @@ impl UnresolvedMetricU64 { idx.clone().into_metric_u64(key, *return_value) } - UnresolvedMetricU64::InterNetworkTransfer(_) => { - todo!() + UnresolvedMetricU64::InterNetworkTransfer(unresolved) => { + let idx = resolution_maps.inter_network_transfers.get(unresolved).ok_or_else(|| { + MetricU64ResolutionError::InterNetworkTransferNotFound { + transfer: unresolved.clone(), + } + })?; + + MetricU64::InterNetworkTransfer(*idx) } UnresolvedMetricU64::Constant(value) => (*value).into(), }; diff --git a/pywr-core/src/models/mod.rs b/pywr-core/src/models/mod.rs index cd5d2008..1ce05b57 100644 --- a/pywr-core/src/models/mod.rs +++ b/pywr-core/src/models/mod.rs @@ -4,9 +4,9 @@ mod simple; use crate::scenario::{ScenarioDomain, ScenarioDomainBuilder, ScenarioDomainBuilderError}; use crate::timestep::{TimeDomain, TimeDomainBuilder, TimeDomainBuilderError}; pub use multi::{ - InterNetworkTransferError, MultiModelBuilder, MultiModelBuilderError, MultiNetworkEntryBuilder, MultiNetworkModel, - MultiNetworkModelFinaliseError, MultiNetworkModelResult, MultiNetworkModelRunError, MultiNetworkModelSetupError, - MultiNetworkModelTimings, MultiNetworkTransferIndex, + InterNetworkTransferError, MultiNetworkEntryBuilder, MultiNetworkModel, MultiNetworkModelBuilder, + MultiNetworkModelBuilderError, MultiNetworkModelFinaliseError, MultiNetworkModelResult, MultiNetworkModelRunError, + MultiNetworkModelSetupError, MultiNetworkModelTimings, MultiNetworkTransferBuilder, MultiNetworkTransferIndex, }; pub use simple::{ Model, ModelBuilder, ModelBuilderError, ModelFinaliseError, ModelResult, ModelRunError, ModelSetupError, diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index f1d03e36..1f69132f 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -83,7 +83,7 @@ struct MultiNetworkTransfer { initial_value: Option, } -pub struct UnresolvedMultiNetworkTransfer { +pub struct MultiNetworkTransferBuilder { /// Name of this transfer name: String, /// Name of the network to get the value from. @@ -94,6 +94,22 @@ pub struct UnresolvedMultiNetworkTransfer { initial_value: Option, } +impl MultiNetworkTransferBuilder { + pub fn new(name: &str, from_model: &str, from_metric: UnresolvedMetricF64) -> Self { + Self { + name: name.to_string(), + from_model: from_model.to_string(), + from_metric, + initial_value: None, + } + } + + pub fn initial_value(&mut self, initial_value: f64) -> &mut Self { + self.initial_value = Some(initial_value); + self + } +} + struct MultiNetworkEntry { name: String, network: Network, @@ -103,7 +119,7 @@ struct MultiNetworkEntry { pub struct MultiNetworkEntryBuilder { name: String, network: NetworkBuilder, - transfers: Vec, + transfers: Vec, } impl MultiNetworkEntryBuilder { @@ -116,8 +132,13 @@ impl MultiNetworkEntryBuilder { } } + /// Get a reference to the [`NetworkBuilder`] + pub fn network_builder(&mut self) -> &mut NetworkBuilder { + &mut self.network + } + /// Add a new unresolved transfer to the builder. - pub fn transfer(&mut self, transfer: UnresolvedMultiNetworkTransfer) -> &mut Self { + pub fn transfer(&mut self, transfer: MultiNetworkTransferBuilder) -> &mut Self { self.transfers.push(transfer); self } @@ -929,7 +950,7 @@ impl MultiNetworkModel { } #[derive(Debug, Error)] -pub enum MultiModelBuilderError { +pub enum MultiNetworkModelBuilderError { #[error("Error building model domain: {0}")] ModelDomainBuilderError(#[from] ModelDomainBuilderError), #[error("Error building network `{name}`: {source}")] @@ -950,12 +971,19 @@ pub enum MultiModelBuilderError { DuplicateNetworkName { name: String }, } -pub struct MultiModelBuilder { +#[cfg(feature = "pyo3")] +impl From for PyErr { + fn from(err: MultiNetworkModelBuilderError) -> PyErr { + PyRuntimeError::new_err(err.to_string()) + } +} + +pub struct MultiNetworkModelBuilder { domain: ModelDomainBuilder, networks: Vec, } -impl MultiModelBuilder { +impl MultiNetworkModelBuilder { pub fn new(domain: ModelDomainBuilder) -> Self { Self { domain, @@ -963,17 +991,17 @@ impl MultiModelBuilder { } } + /// Get a reference to the [`MultiNetworkEntryBuilder`] + pub fn entry_builder(&mut self, name: &str) -> Option<&mut MultiNetworkEntryBuilder> { + self.networks.iter_mut().find(|nb| nb.name == name) + } + pub fn network(&mut self, network: MultiNetworkEntryBuilder) -> &mut Self { self.networks.push(network); self } - pub fn build(self) -> Result { - let scenario_group_map = match &self.domain.scenario { - Some(scenario) => scenario.group_map(), - None => Default::default(), - }; - + pub fn build(self) -> Result { let domain = self.domain.build()?; let mut networks = Vec::with_capacity(self.networks.len()); @@ -993,19 +1021,18 @@ impl MultiModelBuilder { }) .collect(); - let (network, resolution_map) = entry_builder - .network - .build(&scenario_group_map, &transfers_map) - .map_err(|source| MultiModelBuilderError::NetworkBuilderError { + let (network, resolution_map) = entry_builder.network.build(&domain, &transfers_map).map_err(|source| { + MultiNetworkModelBuilderError::NetworkBuilderError { name: name.clone(), source: Box::new(source), - })?; + } + })?; networks.push((name.clone(), network, entry_builder.transfers)); resolution_maps.push(resolution_map); if network_map.contains_key(&name) { - return Err(MultiModelBuilderError::DuplicateNetworkName { name }); + return Err(MultiNetworkModelBuilderError::DuplicateNetworkName { name }); } network_map.insert(name.clone(), i); @@ -1017,7 +1044,7 @@ impl MultiModelBuilder { .iter() .map(|t| { let idx = network_map.get(&t.from_model).copied().ok_or_else(|| { - MultiModelBuilderError::NetworkNotFoundForTransfer { + MultiNetworkModelBuilderError::NetworkNotFoundForTransfer { name: t.name.clone(), network_name: t.from_model.clone(), } @@ -1026,7 +1053,7 @@ impl MultiModelBuilder { let r_map = &resolution_maps[idx]; let metric = t.from_metric.resolve(r_map).map_err(|source| { - MultiModelBuilderError::ResolveMetricF64ForTransferError { + MultiNetworkModelBuilderError::ResolveMetricF64ForTransferError { name: t.name.clone(), source, } @@ -1038,7 +1065,7 @@ impl MultiModelBuilder { initial_value: t.initial_value, }) }) - .collect::, MultiModelBuilderError>>()?; + .collect::, MultiNetworkModelBuilderError>>()?; entries.push({ MultiNetworkEntry { @@ -1124,7 +1151,7 @@ fn compute_inter_network_transfers( #[cfg(test)] mod tests { - use super::{MultiModelBuilder, MultiNetworkEntryBuilder, MultiNetworkModelTimings}; + use super::{MultiNetworkEntryBuilder, MultiNetworkModelBuilder, MultiNetworkModelTimings}; use crate::models::ModelDomainBuilder; use crate::network::NetworkBuilder; use crate::scenario::{ScenarioDomainBuilder, ScenarioGroupBuilder}; @@ -1144,7 +1171,7 @@ mod tests { let mut domain_builder = ModelDomainBuilder::new(time_builder); domain_builder.scenario(scenario_builder); - let mut builder = MultiModelBuilder::new(domain_builder); + let mut builder = MultiNetworkModelBuilder::new(domain_builder); let mut network1 = NetworkBuilder::default(); simple_network(&mut network1, "test-scenario", 2); @@ -1173,7 +1200,7 @@ mod tests { fn test_duplicate_network_names() { let time_builder = default_time_domain_builder(); - let mut builder = MultiModelBuilder::new(ModelDomainBuilder::new(time_builder)); + let mut builder = MultiNetworkModelBuilder::new(ModelDomainBuilder::new(time_builder)); builder.network(MultiNetworkEntryBuilder::new("network1", NetworkBuilder::default())); builder.network(MultiNetworkEntryBuilder::new("network1", NetworkBuilder::default())); diff --git a/pywr-core/src/models/simple.rs b/pywr-core/src/models/simple.rs index 31deb006..e3bd0325 100644 --- a/pywr-core/src/models/simple.rs +++ b/pywr-core/src/models/simple.rs @@ -609,6 +609,13 @@ pub enum ModelBuilderError { NetworkBuildError(#[from] NetworkBuildError), } +#[cfg(feature = "pyo3")] +impl From for PyErr { + fn from(err: ModelBuilderError) -> PyErr { + PyRuntimeError::new_err(err.to_string()) + } +} + pub struct ModelBuilder { domain: ModelDomainBuilder, network: NetworkBuilder, @@ -625,12 +632,8 @@ impl ModelBuilder { } pub fn build(self) -> Result { - let scenario_group_map = match &self.domain.scenario { - Some(scenario) => scenario.group_map(), - None => Default::default(), - }; let domain = self.domain.build()?; - let (network, _) = self.network.build(&scenario_group_map, &HashMap::new())?; + let (network, _) = self.network.build(&domain, &HashMap::new())?; Ok(Model { domain, network }) } } diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index fb04b178..6fcef754 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -13,9 +13,9 @@ use crate::parameters::{ ParameterTimings, VariableConfig, }; use crate::recorders::{ - MetricSet, MetricSetIndex, MetricSetSaveError, MetricSetState, RecorderAggregationError, RecorderBuilder, - RecorderBuilderError, RecorderFinalResult, RecorderFinaliseError, RecorderInternalState, RecorderSaveError, - RecorderSetupError, + MetricSet, MetricSetBuilder, MetricSetBuilderError, MetricSetSaveError, MetricSetState, RecorderAggregationError, + RecorderBuilder, RecorderBuilderError, RecorderFinalResult, RecorderFinaliseError, RecorderInternalState, + RecorderSaveError, RecorderSetupError, }; use crate::scenario::ScenarioIndex; use crate::solvers::{ @@ -1565,6 +1565,12 @@ pub struct UnresolvedEdge { to: UnresolvedNode, } +impl UnresolvedEdge { + pub fn new(from: UnresolvedNode, to: UnresolvedNode) -> Self { + Self { from, to } + } +} + impl Display for UnresolvedEdge { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{} -> {}", self.from, self.to) @@ -1680,9 +1686,31 @@ impl Display for RecorderIndex { } } +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub struct MetricSetIndex(usize); + +impl MetricSetIndex { + pub fn new(idx: usize) -> Self { + Self(idx) + } +} + +impl Deref for MetricSetIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for MetricSetIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + /// A helper struct for building a network. This struct contains look-ups and references /// for resolving names and other unresolved references during the build process. -#[derive(Default)] pub struct ResolutionMaps { pub nodes: HashMap, /// The edges incoming to each node. @@ -1703,12 +1731,37 @@ pub struct ResolutionMaps { pub aggregated_storage_nodes: HashMap, /// The index associated with each unresolved edge. pub edges: HashMap, - /// Scenario group index - pub scenario_group: HashMap, + /// The model domain + pub domain: ModelDomain, /// Inter-network transfer indices pub inter_network_transfers: HashMap, + /// The index associated wit heach metric set + pub metric_sets: HashMap, } +impl ResolutionMaps { + pub fn new(domain: ModelDomain) -> Self { + Self { + nodes: Default::default(), + incoming_edges: Default::default(), + outgoing_edges: Default::default(), + parameters_f64: Default::default(), + parameters_u64: Default::default(), + parameters_multi: Default::default(), + virtual_storage_node: Default::default(), + virtual_storage_associated_nodes: Default::default(), + aggregated_nodes: Default::default(), + aggregated_storage_nodes: Default::default(), + edges: Default::default(), + domain, + inter_network_transfers: Default::default(), + metric_sets: Default::default(), + } + } +} + +type NodeEdgeMap = HashMap>; + #[derive(Debug, Error)] pub enum NetworkBuildError { #[error("Duplicate node names found: {name}")] @@ -1717,6 +1770,10 @@ pub enum NetworkBuildError { DuplicateParameterName { name: ParameterName }, #[error("Duplicate aggregated node names found: {name}")] DuplicateAggregatedNodeName { name: UnresolvedNode }, + #[error("Duplicate metric set names found: {name}")] + DuplicateMetricSetName { name: String }, + #[error("Duplicate edge found: {edge}")] + DuplicateEdge { edge: Box }, #[error("Node `{name}` not found while resolving edge: {edge}")] NodeNotFoundForEdge { name: UnresolvedNode, @@ -1757,6 +1814,12 @@ pub enum NetworkBuildError { #[source] source: Box, }, + #[error("Error building metric set `{name}`: {source}")] + MetricSetBuilderError { + name: String, + #[source] + source: Box, + }, #[error("Could not load all parameters due to circulate reference(s).")] CircularParameterReference, #[error("Parameter collection build error: {0}")] @@ -1775,6 +1838,7 @@ pub struct NetworkBuilder { aggregated_nodes: Vec, aggregated_storage_nodes: Vec, recorders: Vec>, + metric_sets: Vec, } impl NetworkBuilder { @@ -1796,16 +1860,10 @@ impl NetworkBuilder { } /// Connect two nodes together - pub fn connect( - &mut self, - from_name: &str, - from_sub_name: Option<&str>, - to_name: &str, - to_sub_name: Option<&str>, - ) -> &mut Self { + pub fn connect, N2: Into>(&mut self, from: N1, to: N2) -> &mut Self { self.edges.push(UnresolvedEdge { - from: UnresolvedNode::new(from_name, from_sub_name), - to: UnresolvedNode::new(to_name, to_sub_name), + from: from.into(), + to: to.into(), }); self } @@ -1830,6 +1888,11 @@ impl NetworkBuilder { self } + pub fn metric_set(&mut self, metric_set: MetricSetBuilder) -> &mut Self { + self.metric_sets.push(metric_set); + self + } + fn node_index_map(&self) -> Result, NetworkBuildError> { // Build the NodeIndex map checking for any duplicate node names. let mut node_index_map: HashMap = HashMap::with_capacity(self.nodes.len()); @@ -1847,12 +1910,28 @@ impl NetworkBuilder { Ok(node_index_map) } + fn edge_index_map(&self) -> Result, NetworkBuildError> { + let mut edge_index_map: HashMap = HashMap::with_capacity(self.edges.len()); + + for (i, edge) in self.edges.iter().enumerate() { + let unresolved_edge = edge.clone(); + if edge_index_map.contains_key(&unresolved_edge) { + return Err(NetworkBuildError::DuplicateEdge { + edge: Box::new(unresolved_edge), + }); + } + edge_index_map.insert(unresolved_edge, EdgeIndex(i)); + } + + Ok(edge_index_map) + } + fn node_edge_maps( &self, node_indices: &HashMap, - ) -> Result<(HashMap>, HashMap>), NetworkBuildError> { - let mut incoming: HashMap> = HashMap::with_capacity(self.nodes.len()); - let mut outgoing: HashMap> = HashMap::with_capacity(self.nodes.len()); + ) -> Result<(NodeEdgeMap, NodeEdgeMap), NetworkBuildError> { + let mut incoming: NodeEdgeMap = HashMap::with_capacity(self.nodes.len()); + let mut outgoing: NodeEdgeMap = HashMap::with_capacity(self.nodes.len()); for (i, edge) in self.edges.iter().enumerate() { let ei = EdgeIndex(i); @@ -1957,17 +2036,35 @@ impl NetworkBuilder { Ok(vs_associated_nodes) } + fn metric_set_map(&self) -> Result, NetworkBuildError> { + let mut metric_set_map: HashMap = HashMap::with_capacity(self.nodes.len()); + for (i, ms) in self.metric_sets.iter().enumerate() { + let msi = MetricSetIndex(i); + + if metric_set_map.contains_key(ms.name()) { + return Err(NetworkBuildError::DuplicateMetricSetName { + name: ms.name().to_string(), + }); + } + metric_set_map.insert(ms.name().to_string(), msi); + } + + Ok(metric_set_map) + } + fn build_resolution_map( &self, - scenario_group_map: &HashMap, + domain: &ModelDomain, inter_network_transfer_map: &HashMap, ) -> Result { let nodes = self.node_index_map()?; + let edges = self.edge_index_map()?; let (incoming_edges, outgoing_edges) = self.node_edge_maps(&nodes)?; let aggregated_nodes = self.aggregated_node_map()?; let aggregated_storage_nodes = self.aggregated_storage_node_map()?; let virtual_storage_node = self.virtual_storage_node_map()?; let virtual_storage_associated_nodes = self.node_associated_vs_nodes(&nodes)?; + let metric_sets = self.metric_set_map()?; Ok(ResolutionMaps { nodes, @@ -1981,23 +2078,23 @@ impl NetworkBuilder { virtual_storage_associated_nodes, aggregated_nodes, aggregated_storage_nodes, - edges: Default::default(), - scenario_group: scenario_group_map.clone(), // TODO can this clone be removed; needs tying to the lifetime of the reference in `build`. + edges, + domain: domain.clone(), // TODO can this clone be removed; needs tying to the lifetime of the reference in `build`. inter_network_transfers: inter_network_transfer_map.clone(), + metric_sets, }) } /// Build the network. pub fn build( self, - scenario_group_map: &HashMap, + domain: &ModelDomain, inter_network_transfer_map: &HashMap, ) -> Result<(Network, ResolutionMaps), NetworkBuildError> { // Resolution map - let mut resolution_map = self.build_resolution_map(scenario_group_map, inter_network_transfer_map)?; + let mut resolution_map = self.build_resolution_map(domain, inter_network_transfer_map)?; // Iterative load the parameters - let parameters = self .parameters .build(&mut resolution_map) @@ -2084,6 +2181,19 @@ impl NetworkBuilder { edges.push(edge); } + let mut metric_sets = Vec::with_capacity(nodes.len()); + for metric_set_builder in self.metric_sets { + let name = metric_set_builder.name().to_string(); + let metric_set = metric_set_builder.build(&resolution_map).map_err(|source| { + NetworkBuildError::MetricSetBuilderError { + name, + source: Box::new(source), + } + })?; + + metric_sets.push(metric_set); + } + // Construct all recorders let mut recorders = Vec::with_capacity(self.recorders.len()); for recorder_builder in self.recorders.into_iter() { @@ -2103,10 +2213,10 @@ impl NetworkBuilder { nodes, edges, aggregated_nodes, - aggregated_storage_nodes: Default::default(), + aggregated_storage_nodes, virtual_storage_nodes, parameters, - metric_sets: vec![], + metric_sets, recorders, }; @@ -2123,7 +2233,8 @@ mod tests { use crate::recorders::AssertionF64RecorderBuilder; use crate::solvers::{ClpSolver, ClpSolverSettings}; use crate::test_utils::{ - default_domain_builder, run_all_solvers, simple_model, simple_storage_model, simple_storage_network, + default_domain, default_domain_builder, run_all_solvers, simple_model, simple_storage_model, + simple_storage_network, }; use float_cmp::assert_approx_eq; use ndarray::{Array, Array2}; @@ -2139,11 +2250,11 @@ mod tests { .node(NodeBuilder::link("link")) .node(NodeBuilder::output("output")); - builder.connect("input", None, "link", None); - builder.connect("link", None, "output", None); + builder.connect("input", "link"); + builder.connect("link", "output"); - let scenario_group_map = HashMap::new(); - let (network, _) = builder.build(&scenario_group_map, &HashMap::new()).unwrap(); + let domain = default_domain(); + let (network, _) = builder.build(&domain, &HashMap::new()).unwrap(); // Now assert the internal structure is as expected. let input_node = network.get_node_by_name("input", None).unwrap(); @@ -2164,13 +2275,13 @@ mod tests { /// Test the duplicate node names are not permitted. fn test_duplicate_node_name() { let mut builder = NetworkBuilder::default(); - let scenario_group_map = HashMap::new(); builder .node(NodeBuilder::input("my-node")) .node(NodeBuilder::link("my-node")); - let result = builder.build(&scenario_group_map, &HashMap::new()); + let domain = default_domain(); + let result = builder.build(&domain, &HashMap::new()); assert!( matches!(result, Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" ) @@ -2182,7 +2293,7 @@ mod tests { .node(NodeBuilder::input("my-node")); assert!( - matches!(builder.build(&scenario_group_map, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" + matches!(builder.build(&domain, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" ) ); @@ -2192,7 +2303,7 @@ mod tests { .node(NodeBuilder::link("my-other-node")) .node(NodeBuilder::output("my-node")); assert!( - matches!(builder.build(&scenario_group_map, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" + matches!(builder.build(&domain, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node" ) ); // Second add with the same name @@ -2204,7 +2315,7 @@ mod tests { let mut builder = NetworkBuilder::default(); builder.node(n1).node(n2); assert!( - matches!(builder.build(&scenario_group_map, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node[sub1]" + matches!(builder.build(&domain, &HashMap::new()), Err(NetworkBuildError::DuplicateNodeName { name, .. }) if name.to_string() == "my-node[sub1]" ) ); } @@ -2213,7 +2324,6 @@ mod tests { /// Test adding a constant parameter to a network. fn test_constant_parameter() { let mut builder = NetworkBuilder::default(); - let scenario_group_map = HashMap::new(); let mut input_node_builder = NodeBuilder::input("input"); // Add the reference to the constant parameter we have not yet added. @@ -2223,12 +2333,13 @@ mod tests { let output_node_builder = NodeBuilder::output("output"); builder.node(output_node_builder); - builder.connect("input", None, "output", None); + builder.connect("input", "output"); let input_max_flow_builder = parameters::ConstantParameterBuilder::new("my-constant".into(), 10.0); builder.parameters().f64(Box::new(input_max_flow_builder)); - builder.build(&scenario_group_map, &HashMap::new()).unwrap(); + let domain = default_domain(); + builder.build(&domain, &HashMap::new()).unwrap(); } #[test] diff --git a/pywr-core/src/node.rs b/pywr-core/src/node.rs index c2f15073..e0c51f7b 100644 --- a/pywr-core/src/node.rs +++ b/pywr-core/src/node.rs @@ -85,26 +85,23 @@ impl NodeBuilder { pub fn name(&self) -> &UnresolvedNode { &self.name } - pub fn input(name: &str) -> Self { + pub fn input>(name: N) -> Self { Self::new(name, NodeType::Input) } - pub fn output(name: &str) -> Self { + pub fn output>(name: N) -> Self { Self::new(name, NodeType::Output) } - pub fn link(name: &str) -> Self { + pub fn link>(name: N) -> Self { Self::new(name, NodeType::Link) } - pub fn storage(name: &str) -> Self { + pub fn storage>(name: N) -> Self { Self::new(name, NodeType::Storage) } - pub fn new(name: &str, node_type: NodeType) -> Self { - let meta = UnresolvedNode { - name: name.to_string(), - sub_name: None, - }; + pub fn new>(name: N, node_type: NodeType) -> Self { + let meta = name.into(); Self { name: meta, node_type, @@ -227,6 +224,7 @@ impl NodeBuilder { &self, resolution_maps: &ResolutionMaps, ) -> Result { + println!("Min volume: {:#?}", &self.min_volume); let min_volume = self .min_volume .as_ref() @@ -239,7 +237,7 @@ impl NodeBuilder { })? .try_into() .map_err(|source| NodeBuilderError::CouldNotSimplifyMetricF64 { - attr: "max_volume".to_string(), + attr: "min_volume".to_string(), source, }) }) @@ -407,7 +405,7 @@ impl NodeBuilder { Node::Link(LinkNode { meta, cost, - flow_constraints: Default::default(), + flow_constraints: self.build_flow_constraints(resolution_maps)?, incoming_edges, outgoing_edges, }) diff --git a/pywr-core/src/parameters/aggregated.rs b/pywr-core/src/parameters/aggregated.rs index 56925071..eba5fb70 100644 --- a/pywr-core/src/parameters/aggregated.rs +++ b/pywr-core/src/parameters/aggregated.rs @@ -3,10 +3,11 @@ use super::{ ParameterName, ParameterState, SimpleParameter, }; use crate::agg_funcs::AggFuncF64; -use crate::metric::{ConstantMetricF64, MetricF64, MetricF64ResolutionError, SimpleMetricF64, UnresolvedMetricF64}; +use crate::metric::{ConstantMetricF64, MetricF64, SimpleMetricF64, UnresolvedMetricF64}; use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::{ConstCalculationError, GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta}; +use crate::resolve_metric_f64_vec; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, SimpleParameterValues, State}; use crate::timestep::Timestep; @@ -163,37 +164,15 @@ impl ParameterBuilder for AggregatedParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let mut metrics = Vec::with_capacity(self.metrics.len()); - for m in &self.metrics { - match m.resolve(resolution_maps) { - Ok(m) => metrics.push(m), - Err(err) => { - return if let MetricF64ResolutionError::ParameterNotFound { .. } = err { - Ok(MaybeBuiltParameter::Retry(self)) - } else { - Err(ParameterBuildError::ResolveMetricF64Error { - attr: "metrics".to_string(), - source: err, - }) - }; - } - } - } + let metrics = resolve_metric_f64_vec!(self, &self.metrics, resolution_maps, "metrics"); let p = AggregatedParameter { - meta: self.meta.clone(), + meta: self.meta, metrics, - agg_func: self.agg_func.clone(), - }; - - let bp = match p.try_into_simple() { - None => BuiltParameter::General(Box::new(p)), - Some(sp) => match sp.try_into_const() { - None => BuiltParameter::Simple(sp), - Some(cp) => BuiltParameter::Const(cp), - }, + agg_func: self.agg_func, }; - Ok(MaybeBuiltParameter::Built(bp)) + let bp = BuiltParameter::General(Box::new(p)); + Ok(bp.into()) } } diff --git a/pywr-core/src/parameters/aggregated_index.rs b/pywr-core/src/parameters/aggregated_index.rs index 5b318077..18290c02 100644 --- a/pywr-core/src/parameters/aggregated_index.rs +++ b/pywr-core/src/parameters/aggregated_index.rs @@ -1,11 +1,15 @@ /// AggregatedIndexParameter /// -use super::{ConstParameter, Parameter, ParameterName, ParameterState, SimpleParameter}; +use super::{ + BuiltParameter, ConstParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterName, ParameterState, SimpleParameter, +}; use crate::agg_funcs::AggFuncU64; -use crate::metric::{ConstantMetricU64, MetricU64, SimpleMetricU64}; -use crate::network::Network; +use crate::metric::{ConstantMetricU64, MetricU64, SimpleMetricU64, UnresolvedMetricU64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::{ConstCalculationError, GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta}; +use crate::resolve_metric_u64_vec; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, SimpleParameterValues, State}; use crate::timestep::Timestep; @@ -16,19 +20,6 @@ pub struct AggregatedIndexParameter { agg_func: AggFuncU64, } -impl AggregatedIndexParameter -where - M: Send + Sync + Clone, -{ - pub fn new(name: ParameterName, metrics: &[M], agg_func: AggFuncU64) -> Self { - Self { - meta: ParameterMeta::new(name), - metrics: metrics.to_vec(), - agg_func, - } - } -} - impl Parameter for AggregatedIndexParameter where M: Send + Sync, @@ -144,3 +135,46 @@ impl ConstParameter for AggregatedIndexParameter { self } } + +pub struct AggregatedIndexParameterBuilder { + meta: ParameterMeta, + metrics: Vec, + agg_func: AggFuncU64, +} + +impl AggregatedIndexParameterBuilder { + pub fn new(name: ParameterName, agg_func: AggFuncU64) -> Self { + Self { + meta: ParameterMeta::new(name), + metrics: Vec::new(), + agg_func, + } + } + + /// Add a new metric to the builder + pub fn metric(&mut self, metric: UnresolvedMetricU64) -> &mut Self { + self.metrics.push(metric); + self + } +} + +impl ParameterBuilder for AggregatedIndexParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_u64_vec!(self, &self.metrics, resolution_maps, "metrics"); + + let p = AggregatedIndexParameter { + meta: self.meta, + metrics, + agg_func: self.agg_func, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(bp.into()) + } +} diff --git a/pywr-core/src/parameters/array.rs b/pywr-core/src/parameters/array.rs index ef588c04..511042a0 100644 --- a/pywr-core/src/parameters/array.rs +++ b/pywr-core/src/parameters/array.rs @@ -7,7 +7,7 @@ use crate::parameters::{ use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::{Timestep, TimestepIndex}; -use ndarray::{Array1, Array2, Axis}; +use ndarray::{Array1, Array2, Axis, s}; pub struct Array1Parameter { meta: ParameterMeta, @@ -297,17 +297,24 @@ impl ParameterBuilder for Array2ParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let scenario_group_index = resolution_maps - .scenario_group - .get(&self.scenario_group) - .ok_or_else(|| ParameterBuildError::ScenarioGroupNotFound { - name: self.scenario_group.clone(), - })?; + let scenario_group_index = resolution_maps.domain.scenarios().group_index(&self.scenario_group)?; + + let mut array = self.array; + + // If there is a scenario subset then we can reduce the data to align with the scenarios + // that are actually used in the model. + if let Some(subset) = resolution_maps + .domain + .scenarios() + .group_scenario_subset(&self.scenario_group)? + { + array = subset_array2(&array, subset)?; + } let p = Array2Parameter { meta: self.meta, - array: self.array, - scenario_group_index: *scenario_group_index, + array, + scenario_group_index, timestep_offset: self.timestep_offset, }; @@ -323,23 +330,49 @@ impl ParameterBuilder for Array2ParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let scenario_group_index = resolution_maps - .scenario_group - .get(&self.scenario_group) - .ok_or_else(|| ParameterBuildError::ScenarioGroupNotFound { - name: self.scenario_group.clone(), - })?; + let scenario_group_index = resolution_maps.domain.scenarios().group_index(&self.scenario_group)?; + + let mut array = self.array; + + // If there is a scenario subset then we can reduce the data to align with the scenarios + // that are actually used in the model. + if let Some(subset) = resolution_maps + .domain + .scenarios() + .group_scenario_subset(&self.scenario_group)? + { + array = subset_array2(&array, subset)?; + } let p = Array2Parameter { meta: self.meta, - array: self.array, - scenario_group_index: *scenario_group_index, + array, + scenario_group_index, timestep_offset: self.timestep_offset, }; Ok(BuiltParameter::Simple(Box::new(p)).into()) } } +/// Create a subset of a 2D array based on the column indices. +/// +/// This function is used to reduce the size of a timeseries dataframe to only include the columns +/// that are used in a simulation. +fn subset_array2(array: &Array2, subset: &[usize]) -> Result, ParameterBuildError> +where + T: Copy, +{ + // Slice the array to only include the columns that are used in the scenario + let slices = subset.iter().map(|c| array.slice(s![.., *c])).collect::>(); + // Stack the slices to create a new array; this should be infallible because + // the slices are all the same length. + ndarray::stack(ndarray::Axis(1), &slices).map_err(|source| ParameterBuildError::ArraySubSetError { + array_shape: array.shape().to_vec(), + subset: subset.to_vec(), + source, + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/pywr-core/src/parameters/asymmetric.rs b/pywr-core/src/parameters/asymmetric.rs index 75409cf0..1a6c41ae 100644 --- a/pywr-core/src/parameters/asymmetric.rs +++ b/pywr-core/src/parameters/asymmetric.rs @@ -1,9 +1,11 @@ -use crate::metric::MetricU64; -use crate::network::Network; +use crate::metric::{MetricU64, UnresolvedMetricU64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ - GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, downcast_internal_state_mut, + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, downcast_internal_state_mut, }; +use crate::resolve_metric_u64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -14,16 +16,6 @@ pub struct AsymmetricSwitchIndexParameter { off_parameter: MetricU64, } -impl AsymmetricSwitchIndexParameter { - pub fn new(name: ParameterName, on_parameter: MetricU64, off_parameter: MetricU64) -> Self { - Self { - meta: ParameterMeta::new(name), - on_parameter, - off_parameter, - } - } -} - impl Parameter for AsymmetricSwitchIndexParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -75,3 +67,41 @@ impl GeneralParameter for AsymmetricSwitchIndexParameter { self } } + +pub struct AsymmetricSwitchIndexParameterBuilder { + meta: ParameterMeta, + on_parameter: UnresolvedMetricU64, + off_parameter: UnresolvedMetricU64, +} + +impl AsymmetricSwitchIndexParameterBuilder { + pub fn new(name: ParameterName, on_parameter: UnresolvedMetricU64, off_parameter: UnresolvedMetricU64) -> Self { + Self { + meta: ParameterMeta::new(name), + on_parameter, + off_parameter, + } + } +} + +impl ParameterBuilder for AsymmetricSwitchIndexParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let on_parameter = resolve_metric_u64!(self, self.on_parameter, resolution_maps, "on_parameter"); + let off_parameter = resolve_metric_u64!(self, self.off_parameter, resolution_maps, "off_parameter"); + + let p = AsymmetricSwitchIndexParameter { + meta: self.meta, + on_parameter, + off_parameter, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/constant.rs b/pywr-core/src/parameters/constant.rs index 2fc8cd56..3c06b91f 100644 --- a/pywr-core/src/parameters/constant.rs +++ b/pywr-core/src/parameters/constant.rs @@ -117,19 +117,22 @@ impl VariableParameter for ConstantParameter { } pub struct ConstantParameterBuilder { - name: ParameterName, + meta: ParameterMeta, value: f64, } impl ConstantParameterBuilder { pub fn new(name: ParameterName, value: f64) -> Self { - Self { name, value } + Self { + meta: ParameterMeta::new(name), + value, + } } } impl ParameterBuilder for ConstantParameterBuilder { fn name(&self) -> &ParameterName { - &self.name + &self.meta.name } fn build( @@ -137,7 +140,7 @@ impl ParameterBuilder for ConstantParameterBuilder { _resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { Ok(BuiltParameter::Const(Box::new(ConstantParameter { - meta: ParameterMeta::new(self.name.clone()), + meta: self.meta, value: self.value, })) .into()) diff --git a/pywr-core/src/parameters/constant_scenario.rs b/pywr-core/src/parameters/constant_scenario.rs index 3318fd0f..443589fd 100644 --- a/pywr-core/src/parameters/constant_scenario.rs +++ b/pywr-core/src/parameters/constant_scenario.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::ConstCalculationError; -use crate::parameters::{ConstParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, ConstParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::ConstParameterValues; @@ -9,16 +13,6 @@ pub struct ConstantScenarioParameter { scenario_group_index: usize, } -impl ConstantScenarioParameter { - pub fn new(name: ParameterName, values: Vec, scenario_group_index: usize) -> Self { - Self { - meta: ParameterMeta::new(name), - values, - scenario_group_index, - } - } -} - impl Parameter for ConstantScenarioParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -44,10 +38,56 @@ impl ConstParameter for ConstantScenarioParameter { } } +pub struct ConstantScenarioParameterBuilder { + meta: ParameterMeta, + values: Vec, + scenario_group: String, +} + +impl ConstantScenarioParameterBuilder { + pub fn new(name: ParameterName, values: Vec, scenario_group: &str) -> Self { + Self { + meta: ParameterMeta::new(name), + values, + scenario_group: scenario_group.to_string(), + } + } +} + +impl ParameterBuilder for ConstantScenarioParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let scenario_group_index = resolution_maps.domain.scenarios().group_index(&self.scenario_group)?; + + let scenario_group_size = resolution_maps.domain.scenarios().group_size(&self.scenario_group)?; + if self.values.len() != scenario_group_size { + return Err(ParameterBuildError::ScenarioValuesLengthMismatch { + values: self.values.len(), + scenarios: scenario_group_size, + group: self.scenario_group.clone(), + }); + } + + let p = ConstantScenarioParameter { + meta: self.meta, + values: self.values, + scenario_group_index, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Const(Box::new(p)))) + } +} + #[cfg(test)] mod tests { - use crate::parameters::ConstParameter; use crate::parameters::constant_scenario::ConstantScenarioParameter; + use crate::parameters::{ConstParameter, ParameterMeta}; use crate::scenario::{self, ScenarioGroupBuilder}; use crate::state::ConstParameterValues; use float_cmp::assert_approx_eq; @@ -62,7 +102,11 @@ mod tests { .build() .unwrap(); - let p = ConstantScenarioParameter::new("my-parameter".into(), vec![3.0, 2.0, 1.0], 0); + let p = ConstantScenarioParameter { + meta: ParameterMeta::new("my-parameter".into()), + values: vec![3.0, 2.0, 1.0], + scenario_group_index: 0, + }; let const_scenario_values = ConstParameterValues::default(); @@ -87,7 +131,11 @@ mod tests { .build() .unwrap(); - let p = ConstantScenarioParameter::new("my-parameter".into(), vec![5.0, 4.0, 3.0, 2.0, 1.0], 0); + let p = ConstantScenarioParameter { + meta: ParameterMeta::new("my-parameter".into()), + values: vec![5.0, 4.0, 3.0, 2.0, 1.0], + scenario_group_index: 0, + }; let const_scenario_values = ConstParameterValues::default(); @@ -112,7 +160,11 @@ mod tests { .build() .unwrap(); - let p = ConstantScenarioParameter::new("my-parameter".into(), vec![5.0, 4.0, 3.0, 2.0, 1.0], 0); + let p = ConstantScenarioParameter { + meta: ParameterMeta::new("my-parameter".into()), + values: vec![5.0, 4.0, 3.0, 2.0, 1.0], + scenario_group_index: 0, + }; let const_scenario_values = ConstParameterValues::default(); diff --git a/pywr-core/src/parameters/control_curves/apportion.rs b/pywr-core/src/parameters/control_curves/apportion.rs index fbeb4921..c43a0a6a 100644 --- a/pywr-core/src/parameters/control_curves/apportion.rs +++ b/pywr-core/src/parameters/control_curves/apportion.rs @@ -1,13 +1,17 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, State}; use crate::timestep::Timestep; use std::collections::HashMap; -/// A parameter which divides a apportions a metric to an upper and lower amount based +/// A parameter which apportions a metric to an upper and lower amount based /// on the current value of a control curve. /// /// The control curve is expected to produce values between 0.0 and 1.0. If the control curve @@ -21,16 +25,6 @@ pub struct ApportionParameter { control_curve: MetricF64, } -impl ApportionParameter { - pub fn new(name: ParameterName, metric: MetricF64, control_curve: MetricF64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - control_curve, - } - } -} - impl Parameter for ApportionParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -67,3 +61,42 @@ impl GeneralParameter for ApportionParameter { self } } + +pub struct ApportionParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + control_curve: UnresolvedMetricF64, +} + +impl ApportionParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, control_curve: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + control_curve, + } + } +} + +impl ParameterBuilder for ApportionParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + let control_curve = resolve_metric_f64!(self, self.control_curve, resolution_maps, "control_curve"); + + let p = ApportionParameter { + meta: self.meta, + metric, + control_curve, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(bp.into()) + } +} diff --git a/pywr-core/src/parameters/control_curves/index.rs b/pywr-core/src/parameters/control_curves/index.rs index 71759fe2..972d233d 100644 --- a/pywr-core/src/parameters/control_curves/index.rs +++ b/pywr-core/src/parameters/control_curves/index.rs @@ -1,10 +1,14 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_f64_vec}; pub struct ControlCurveIndexParameter { meta: ParameterMeta, @@ -12,16 +16,6 @@ pub struct ControlCurveIndexParameter { control_curves: Vec, } -impl ControlCurveIndexParameter { - pub fn new(name: ParameterName, metric: MetricF64, control_curves: Vec) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - control_curves, - } - } -} - impl Parameter for ControlCurveIndexParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -55,3 +49,46 @@ impl GeneralParameter for ControlCurveIndexParameter { self } } + +pub struct ControlCurveIndexParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + control_curves: Vec, +} + +impl ControlCurveIndexParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + control_curves: Vec::new(), + } + } + + pub fn control_curve(&mut self, cc: UnresolvedMetricF64) -> &mut Self { + self.control_curves.push(cc); + self + } +} + +impl ParameterBuilder for ControlCurveIndexParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); + + let p = ControlCurveIndexParameter { + meta: self.meta, + metric, + control_curves, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/control_curves/interpolated.rs b/pywr-core/src/parameters/control_curves/interpolated.rs index 8889dcd3..5c7c4c1c 100644 --- a/pywr-core/src/parameters/control_curves/interpolated.rs +++ b/pywr-core/src/parameters/control_curves/interpolated.rs @@ -104,7 +104,7 @@ impl ParameterBuilder for ControlCurveInterpolatedParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metrics"); + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); let values = resolve_metric_f64_vec!(self, &self.values, resolution_maps, "values"); @@ -115,14 +115,7 @@ impl ParameterBuilder for ControlCurveInterpolatedParameterBuilder { values, }; - let bp = match p.try_into_simple() { - None => BuiltParameter::General(Box::new(p)), - Some(sp) => match sp.try_into_const() { - None => BuiltParameter::Simple(sp), - Some(cp) => BuiltParameter::Const(cp), - }, - }; - + let bp = BuiltParameter::General(Box::new(p)); Ok(bp.into()) } } diff --git a/pywr-core/src/parameters/control_curves/mod.rs b/pywr-core/src/parameters/control_curves/mod.rs index 34903968..4cb053a7 100644 --- a/pywr-core/src/parameters/control_curves/mod.rs +++ b/pywr-core/src/parameters/control_curves/mod.rs @@ -5,9 +5,9 @@ mod piecewise; mod simple; mod volume_between; -pub use apportion::ApportionParameter; -pub use index::ControlCurveIndexParameter; -pub use interpolated::ControlCurveInterpolatedParameterBuilder; -pub use piecewise::PiecewiseInterpolatedParameterBuilder; -pub use simple::ControlCurveParameter; -pub use volume_between::VolumeBetweenControlCurvesParameter; +pub use apportion::{ApportionParameter, ApportionParameterBuilder}; +pub use index::{ControlCurveIndexParameter, ControlCurveIndexParameterBuilder}; +pub use interpolated::{ControlCurveInterpolatedParameter, ControlCurveInterpolatedParameterBuilder}; +pub use piecewise::{PiecewiseInterpolatedParameter, PiecewiseInterpolatedParameterBuilder}; +pub use simple::{ControlCurveParameter, ControlCurveParameterBuilder}; +pub use volume_between::{VolumeBetweenControlCurvesParameter, VolumeBetweenControlCurvesParameterBuilder}; diff --git a/pywr-core/src/parameters/control_curves/piecewise.rs b/pywr-core/src/parameters/control_curves/piecewise.rs index 5dac662e..f960c007 100644 --- a/pywr-core/src/parameters/control_curves/piecewise.rs +++ b/pywr-core/src/parameters/control_curves/piecewise.rs @@ -116,7 +116,7 @@ impl ParameterBuilder for PiecewiseInterpolatedParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metrics"); + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); let p = PiecewiseInterpolatedParameter { @@ -128,14 +128,7 @@ impl ParameterBuilder for PiecewiseInterpolatedParameterBuilder { minimum: self.minimum, }; - let bp = match p.try_into_simple() { - None => BuiltParameter::General(Box::new(p)), - Some(sp) => match sp.try_into_const() { - None => BuiltParameter::Simple(sp), - Some(cp) => BuiltParameter::Const(cp), - }, - }; - + let bp = BuiltParameter::General(Box::new(p)); Ok(bp.into()) } } diff --git a/pywr-core/src/parameters/control_curves/simple.rs b/pywr-core/src/parameters/control_curves/simple.rs index 667916d5..72050e3d 100644 --- a/pywr-core/src/parameters/control_curves/simple.rs +++ b/pywr-core/src/parameters/control_curves/simple.rs @@ -1,10 +1,14 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +use crate::{resolve_metric_f64, resolve_metric_f64_vec}; pub struct ControlCurveParameter { meta: ParameterMeta, @@ -13,17 +17,6 @@ pub struct ControlCurveParameter { values: Vec, } -impl ControlCurveParameter { - pub fn new(name: ParameterName, metric: MetricF64, control_curves: Vec, values: Vec) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - control_curves, - values, - } - } -} - impl Parameter for ControlCurveParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -76,3 +69,57 @@ impl GeneralParameter for ControlCurveParameter { self } } + +pub struct ControlCurveParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + control_curves: Vec, + values: Vec, +} + +impl ControlCurveParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + control_curves: Vec::new(), + values: Vec::new(), + } + } + + pub fn control_curve(&mut self, cc: UnresolvedMetricF64) -> &mut Self { + self.control_curves.push(cc); + self + } + + pub fn value(&mut self, value: UnresolvedMetricF64) -> &mut Self { + self.values.push(value); + self + } +} + +impl ParameterBuilder for ControlCurveParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); + + let values = resolve_metric_f64_vec!(self, &self.values, resolution_maps, "values"); + + let p = ControlCurveParameter { + meta: self.meta, + metric, + control_curves, + values, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/control_curves/volume_between.rs b/pywr-core/src/parameters/control_curves/volume_between.rs index 88c61c7e..427b640a 100644 --- a/pywr-core/src/parameters/control_curves/volume_between.rs +++ b/pywr-core/src/parameters/control_curves/volume_between.rs @@ -1,6 +1,11 @@ -use crate::metric::SimpleMetricF64; +use crate::metric::{SimpleMetricF64, UnresolvedMetricF64}; +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; @@ -13,17 +18,6 @@ pub struct VolumeBetweenControlCurvesParameter { lower: Option, } -impl VolumeBetweenControlCurvesParameter { - pub fn new(name: ParameterName, total: M, upper: Option, lower: Option) -> Self { - Self { - meta: ParameterMeta::new(name), - total, - upper, - lower, - } - } -} - impl Parameter for VolumeBetweenControlCurvesParameter where M: Send + Sync, @@ -56,3 +50,90 @@ impl SimpleParameter for VolumeBetweenControlCurvesParameter { + meta: ParameterMeta, + total: M, + upper: Option, + lower: Option, +} + +impl VolumeBetweenControlCurvesParameterBuilder { + pub fn new(name: ParameterName, total: M) -> Self { + Self { + meta: ParameterMeta::new(name), + total, + upper: None, + lower: None, + } + } + + pub fn lower(&mut self, lower: M) -> &mut Self { + self.lower = Some(lower); + self + } + + pub fn upper(&mut self, upper: M) -> &mut Self { + self.upper = Some(upper); + self + } +} + +impl ParameterBuilder for VolumeBetweenControlCurvesParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let total = resolve_metric_f64!(self, self.total, resolution_maps, "total"); + let total: SimpleMetricF64 = + total + .try_into() + .map_err(|source| ParameterBuildError::CouldNotSimplifyMetricF64 { + attr: "total".to_string(), + source, + })?; + + let upper: Option = match &self.upper { + Some(upper) => { + let upper = resolve_metric_f64!(self, upper, resolution_maps, "upper"); + let upper: SimpleMetricF64 = + upper + .try_into() + .map_err(|source| ParameterBuildError::CouldNotSimplifyMetricF64 { + attr: "upper".to_string(), + source, + })?; + + Some(upper) + } + None => None, + }; + + let lower: Option = match &self.lower { + Some(lower) => { + let lower = resolve_metric_f64!(self, lower, resolution_maps, "lower"); + let lower = lower + .try_into() + .map_err(|source| ParameterBuildError::CouldNotSimplifyMetricF64 { + attr: "lower".to_string(), + source, + })?; + Some(lower) + } + None => None, + }; + + let p = VolumeBetweenControlCurvesParameter { + meta: self.meta, + total, + upper, + lower, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/deficit.rs b/pywr-core/src/parameters/deficit.rs index 8e86dcec..98dcd5b8 100644 --- a/pywr-core/src/parameters/deficit.rs +++ b/pywr-core/src/parameters/deficit.rs @@ -1,8 +1,10 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::{ - GeneralCalculationError, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, + BuiltParameter, GeneralCalculationError, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, + ParameterBuilder, ParameterMeta, ParameterName, ParameterState, }; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -17,16 +19,6 @@ pub struct DeficitParameter { max_flow: MetricF64, } -impl DeficitParameter { - pub fn new(name: ParameterName, flow: MetricF64, max_flow: MetricF64) -> Self { - Self { - meta: ParameterMeta::new(name), - flow, - max_flow, - } - } -} - impl Parameter for DeficitParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -56,3 +48,41 @@ impl GeneralParameter for DeficitParameter { self } } + +pub struct DeficitParameterBuilder { + meta: ParameterMeta, + flow: UnresolvedMetricF64, + max_flow: UnresolvedMetricF64, +} + +impl DeficitParameterBuilder { + pub fn new(name: ParameterName, flow: UnresolvedMetricF64, max_flow: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + flow, + max_flow, + } + } +} + +impl ParameterBuilder for DeficitParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let flow = resolve_metric_f64!(self, self.flow, resolution_maps, "flow"); + let max_flow = resolve_metric_f64!(self, self.max_flow, resolution_maps, "max_flow"); + + let p = DeficitParameter { + meta: self.meta, + flow, + max_flow, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/delay.rs b/pywr-core/src/parameters/delay.rs index ce8a5ba0..425f5e75 100644 --- a/pywr-core/src/parameters/delay.rs +++ b/pywr-core/src/parameters/delay.rs @@ -315,14 +315,7 @@ impl ParameterBuilder for DelayParameterBuilder { initial_value: self.initial_value, }; - let bp = match p.try_into_simple() { - None => BuiltParameter::General(Box::new(p)), - Some(sp) => match sp.try_into_const() { - None => BuiltParameter::Simple(sp), - Some(cp) => BuiltParameter::Const(cp), - }, - }; - + let bp = BuiltParameter::General(Box::new(p)); Ok(bp.into()) } } @@ -345,14 +338,7 @@ impl ParameterBuilder for DelayParameterBuilder { initial_value: self.initial_value, }; - let bp = match p.try_into_simple() { - None => BuiltParameter::General(Box::new(p)), - Some(sp) => match sp.try_into_const() { - None => BuiltParameter::Simple(sp), - Some(cp) => BuiltParameter::Const(cp), - }, - }; - + let bp = BuiltParameter::General(Box::new(p)); Ok(bp.into()) } } @@ -417,7 +403,7 @@ mod test { const DELAY: u64 = 3; // 3 time-step delay let parameter = DelayParameterBuilder::new( "test-parameter".into(), - UnresolvedMetricU64::new_index_parameter_before("test-x"), + UnresolvedMetricU64::new_parameter_before("test-x"), DELAY, 0, ); diff --git a/pywr-core/src/parameters/difference.rs b/pywr-core/src/parameters/difference.rs index 5453892f..1a9ff0f1 100644 --- a/pywr-core/src/parameters/difference.rs +++ b/pywr-core/src/parameters/difference.rs @@ -1,8 +1,12 @@ -use super::{Parameter, ParameterName, SimpleParameter}; -use crate::metric::{MetricF64, SimpleMetricF64}; -use crate::network::Network; +use super::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterName, + SimpleParameter, +}; +use crate::metric::{MetricF64, SimpleMetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::{GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta, ParameterState}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::{SimpleParameterValues, State}; use crate::timestep::Timestep; @@ -22,20 +26,6 @@ pub struct DifferenceParameter { max: Option, } -impl DifferenceParameter -where - M: Send + Sync + Clone, -{ - pub fn new(name: ParameterName, a: M, b: M, min: Option, max: Option) -> Self { - Self { - meta: ParameterMeta::new(name), - a, - b, - min, - max, - } - } -} impl Parameter for DifferenceParameter where M: Send + Sync, @@ -72,8 +62,8 @@ impl GeneralParameter for DifferenceParameter { // We can make a simple version if all metrics can be simplified let a: SimpleMetricF64 = self.a.clone().try_into().ok()?; let b: SimpleMetricF64 = self.b.clone().try_into().ok()?; - let min: Option = self.min.as_ref().map(|m| m.clone().try_into().ok())?; - let max: Option = self.max.as_ref().map(|m| m.clone().try_into().ok())?; + let min: Option = self.min.as_ref().map(|m| m.clone().try_into()).transpose().ok()?; + let max: Option = self.max.as_ref().map(|m| m.clone().try_into()).transpose().ok()?; Some(Box::new(DifferenceParameter:: { meta: self.meta.clone(), @@ -126,6 +116,67 @@ fn difference(a: f64, b: f64, min: Option, max: Option) -> f64 { result } +pub struct DifferenceParameterBuilder { + meta: ParameterMeta, + a: UnresolvedMetricF64, + b: UnresolvedMetricF64, + min: Option, + max: Option, +} + +impl DifferenceParameterBuilder { + pub fn new(name: ParameterName, a: UnresolvedMetricF64, b: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + a, + b, + min: None, + max: None, + } + } + + pub fn min(&mut self, min: UnresolvedMetricF64) -> &mut Self { + self.min = Some(min); + self + } + pub fn max(&mut self, max: UnresolvedMetricF64) -> &mut Self { + self.max = Some(max); + self + } +} + +impl ParameterBuilder for DifferenceParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let a = resolve_metric_f64!(self, self.a, resolution_maps, "a"); + let b = resolve_metric_f64!(self, self.b, resolution_maps, "b"); + let min = match &self.min { + Some(min) => Some(resolve_metric_f64!(self, min, resolution_maps, "min")), + None => None, + }; + let max = match &self.max { + Some(max) => Some(resolve_metric_f64!(self, max, resolution_maps, "max")), + None => None, + }; + + let p = DifferenceParameter { + meta: self.meta, + a, + b, + min, + max, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + #[cfg(test)] mod tests { use super::difference; diff --git a/pywr-core/src/parameters/division.rs b/pywr-core/src/parameters/division.rs index 32e16ee6..001c85f1 100644 --- a/pywr-core/src/parameters/division.rs +++ b/pywr-core/src/parameters/division.rs @@ -1,8 +1,9 @@ -use super::{Parameter, ParameterName}; -use crate::metric::MetricF64; -use crate::network::Network; +use super::{BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterName}; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, ParameterMeta, ParameterState}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -13,15 +14,6 @@ pub struct DivisionParameter { denominator: MetricF64, } -impl DivisionParameter { - pub fn new(name: ParameterName, numerator: MetricF64, denominator: MetricF64) -> Self { - Self { - meta: ParameterMeta::new(name), - numerator, - denominator, - } - } -} impl Parameter for DivisionParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -53,3 +45,41 @@ impl GeneralParameter for DivisionParameter { self } } + +pub struct DivisionParameterBuilder { + meta: ParameterMeta, + numerator: UnresolvedMetricF64, + denominator: UnresolvedMetricF64, +} + +impl DivisionParameterBuilder { + pub fn new(name: ParameterName, numerator: UnresolvedMetricF64, denominator: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + numerator, + denominator, + } + } +} + +impl ParameterBuilder for DivisionParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let numerator = resolve_metric_f64!(self, self.numerator, resolution_maps, "numerator"); + let denominator = resolve_metric_f64!(self, self.denominator, resolution_maps, "denominator"); + + let p = DivisionParameter { + meta: self.meta, + numerator, + denominator, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/hydropower.rs b/pywr-core/src/parameters/hydropower.rs index 33afadf4..b75ed7ba 100644 --- a/pywr-core/src/parameters/hydropower.rs +++ b/pywr-core/src/parameters/hydropower.rs @@ -1,21 +1,25 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_optional_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; use crate::utils::{hydropower_calculation, inverse_hydropower_calculation}; pub struct HydropowerTargetData { - pub actual_flow: Option, - pub target: Option, + pub actual_flow: Option, + pub target: Option, pub elevation: Option, pub min_head: Option, - pub max_flow: Option, - pub min_flow: Option, + pub max_flow: Option, + pub min_flow: Option, pub efficiency: Option, - pub water_elevation: Option, + pub water_elevation: Option, pub water_density: Option, pub flow_unit_conversion: Option, pub energy_unit_conversion: Option, @@ -37,23 +41,6 @@ pub struct HydropowerTargetParameter { } impl HydropowerTargetParameter { - pub fn new(name: ParameterName, turbine_data: HydropowerTargetData) -> Self { - Self { - meta: ParameterMeta::new(name), - actual_flow: turbine_data.actual_flow, - target: turbine_data.target, - water_elevation: turbine_data.water_elevation, - turbine_elevation: turbine_data.elevation.unwrap_or(0.0), - turbine_min_head: turbine_data.min_head.unwrap_or(0.0), - turbine_efficiency: turbine_data.efficiency.unwrap_or(1.0), - max_flow: turbine_data.max_flow, - min_flow: turbine_data.min_flow, - water_density: turbine_data.water_density.unwrap_or(1000.0), - flow_unit_conversion: turbine_data.flow_unit_conversion.unwrap_or(1.0), - energy_unit_conversion: turbine_data.energy_unit_conversion.unwrap_or(1e-6), - } - } - fn head(&self, model: &Network, state: &State) -> Result { let head = if let Some(water_elevation) = &self.water_elevation { water_elevation.get_value(model, state)? - self.turbine_elevation @@ -157,3 +144,72 @@ impl GeneralParameter for HydropowerTargetParameter { self } } + +pub struct HydropowerTargetParameterBuilder { + meta: ParameterMeta, + actual_flow: Option, + target: Option, + max_flow: Option, + min_flow: Option, + turbine_min_head: f64, + turbine_elevation: f64, + turbine_efficiency: f64, + water_elevation: Option, + water_density: f64, + flow_unit_conversion: f64, + energy_unit_conversion: f64, +} + +impl HydropowerTargetParameterBuilder { + pub fn new(name: ParameterName, turbine_data: HydropowerTargetData) -> Self { + Self { + meta: ParameterMeta::new(name), + actual_flow: turbine_data.actual_flow, + target: turbine_data.target, + water_elevation: turbine_data.water_elevation, + turbine_elevation: turbine_data.elevation.unwrap_or(0.0), + turbine_min_head: turbine_data.min_head.unwrap_or(0.0), + turbine_efficiency: turbine_data.efficiency.unwrap_or(1.0), + max_flow: turbine_data.max_flow, + min_flow: turbine_data.min_flow, + water_density: turbine_data.water_density.unwrap_or(1000.0), + flow_unit_conversion: turbine_data.flow_unit_conversion.unwrap_or(1.0), + energy_unit_conversion: turbine_data.energy_unit_conversion.unwrap_or(1e-6), + } + } +} + +impl ParameterBuilder for HydropowerTargetParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let actual_flow = resolve_optional_metric_f64!(self, &self.actual_flow, resolution_maps, "actual_flow"); + let target = resolve_optional_metric_f64!(self, &self.target, resolution_maps, "target"); + let max_flow = resolve_optional_metric_f64!(self, &self.max_flow, resolution_maps, "max_flow"); + let min_flow = resolve_optional_metric_f64!(self, &self.min_flow, resolution_maps, "min_flow"); + let water_elevation = + resolve_optional_metric_f64!(self, &self.water_elevation, resolution_maps, "water_elevation"); + + let p = HydropowerTargetParameter { + meta: self.meta, + actual_flow, + target, + max_flow, + min_flow, + turbine_min_head: self.turbine_min_head, + turbine_elevation: self.turbine_elevation, + turbine_efficiency: self.turbine_efficiency, + water_elevation, + water_density: self.water_density, + flow_unit_conversion: self.flow_unit_conversion, + energy_unit_conversion: self.energy_unit_conversion, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/indexed_array.rs b/pywr-core/src/parameters/indexed_array.rs index 1f24bbdb..5e45bc06 100644 --- a/pywr-core/src/parameters/indexed_array.rs +++ b/pywr-core/src/parameters/indexed_array.rs @@ -1,10 +1,14 @@ -use crate::metric::{MetricF64, MetricU64}; -use crate::network::Network; +use crate::metric::{MetricF64, MetricU64, UnresolvedMetricF64, UnresolvedMetricU64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +use crate::{resolve_metric_f64_vec, resolve_metric_u64}; pub struct IndexedArrayParameter { meta: ParameterMeta, @@ -12,16 +16,6 @@ pub struct IndexedArrayParameter { metrics: Vec, } -impl IndexedArrayParameter { - pub fn new(name: ParameterName, index_parameter: MetricU64, metrics: &[MetricF64]) -> Self { - Self { - meta: ParameterMeta::new(name), - index_parameter, - metrics: metrics.to_vec(), - } - } -} - impl Parameter for IndexedArrayParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -58,3 +52,46 @@ impl GeneralParameter for IndexedArrayParameter { self } } + +pub struct IndexedArrayParameterBuilder { + meta: ParameterMeta, + index_parameter: UnresolvedMetricU64, + metrics: Vec, +} + +impl IndexedArrayParameterBuilder { + pub fn new(name: ParameterName, index_parameter: UnresolvedMetricU64) -> Self { + Self { + meta: ParameterMeta::new(name), + index_parameter, + metrics: Vec::new(), + } + } + + pub fn metric(&mut self, metric: UnresolvedMetricF64) -> &mut Self { + self.metrics.push(metric); + self + } +} + +impl ParameterBuilder for IndexedArrayParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let index_parameter = resolve_metric_u64!(self, self.index_parameter, resolution_maps, "index_parameter"); + let metrics = resolve_metric_f64_vec!(self, &self.metrics, resolution_maps, "metrics"); + + let p = IndexedArrayParameter { + meta: self.meta, + index_parameter, + metrics, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/interpolated.rs b/pywr-core/src/parameters/interpolated.rs index 515b16de..6027641f 100644 --- a/pywr-core/src/parameters/interpolated.rs +++ b/pywr-core/src/parameters/interpolated.rs @@ -1,8 +1,12 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::linear_interpolation; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -15,16 +19,6 @@ pub struct InterpolatedParameter { error_on_bounds: bool, } -impl InterpolatedParameter { - pub fn new(name: ParameterName, x: MetricF64, points: Vec<(MetricF64, MetricF64)>, error_on_bounds: bool) -> Self { - Self { - meta: ParameterMeta::new(name), - x, - points, - error_on_bounds, - } - } -} impl Parameter for InterpolatedParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -65,3 +59,59 @@ impl GeneralParameter for InterpolatedParameter { self } } + +pub struct InterpolatedParameterBuilder { + meta: ParameterMeta, + x: UnresolvedMetricF64, + points: Vec<(UnresolvedMetricF64, UnresolvedMetricF64)>, + error_on_bounds: bool, +} + +impl InterpolatedParameterBuilder { + pub fn new( + name: ParameterName, + x: UnresolvedMetricF64, + points: Vec<(UnresolvedMetricF64, UnresolvedMetricF64)>, + ) -> Self { + Self { + meta: ParameterMeta::new(name), + x, + points, + error_on_bounds: true, + } + } + + pub fn error_on_bounds(&mut self, value: bool) -> &mut Self { + self.error_on_bounds = value; + self + } +} + +impl ParameterBuilder for InterpolatedParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let x = resolve_metric_f64!(self, self.x, resolution_maps, "x"); + + let mut points = Vec::with_capacity(self.points.len()); + for (i, (uxp, ufp)) in self.points.iter().enumerate() { + let xp = resolve_metric_f64!(self, uxp, resolution_maps, &format!("points[{i}].x")); + let fp = resolve_metric_f64!(self, ufp, resolution_maps, &format!("points[{i}].f")); + points.push((xp, fp)); + } + + let p = InterpolatedParameter { + meta: self.meta, + x, + points, + error_on_bounds: self.error_on_bounds, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/max.rs b/pywr-core/src/parameters/max.rs index 15806a18..c4e8c9a0 100644 --- a/pywr-core/src/parameters/max.rs +++ b/pywr-core/src/parameters/max.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -12,16 +16,6 @@ pub struct MaxParameter { threshold: f64, } -impl MaxParameter { - pub fn new(name: ParameterName, metric: MetricF64, threshold: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - threshold, - } - } -} - impl Parameter for MaxParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -49,3 +43,40 @@ impl GeneralParameter for MaxParameter { self } } + +pub struct MaxParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + threshold: f64, +} + +impl MaxParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl ParameterBuilder for MaxParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = MaxParameter { + meta: self.meta, + metric, + threshold: self.threshold, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/min.rs b/pywr-core/src/parameters/min.rs index 70d30b65..b20d0695 100644 --- a/pywr-core/src/parameters/min.rs +++ b/pywr-core/src/parameters/min.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -12,16 +16,6 @@ pub struct MinParameter { threshold: f64, } -impl MinParameter { - pub fn new(name: ParameterName, metric: MetricF64, threshold: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - threshold, - } - } -} - impl Parameter for MinParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -47,3 +41,40 @@ impl GeneralParameter for MinParameter { self } } + +pub struct MinParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + threshold: f64, +} + +impl MinParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl ParameterBuilder for MinParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = MinParameter { + meta: self.meta, + metric, + threshold: self.threshold, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 2db9332a..649e23bb 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -35,53 +35,58 @@ mod vector; use std::any::Any; // Re-imports use crate::metric::{ - ConstantMetricF64, ConstantMetricU64, MetricF64, MetricF64ResolutionError, MetricU64, MetricU64ResolutionError, - SimpleMetricF64, SimpleMetricU64, + ConstantMetricF64, ConstantMetricU64, MetricF64, MetricF64Error, MetricF64ResolutionError, MetricU64, + MetricU64ResolutionError, SimpleMetricF64, SimpleMetricU64, }; use crate::network::{Network, ResolutionMaps}; -use crate::scenario::ScenarioIndex; +use crate::scenario::{ScenarioGroupNotFound, ScenarioIndex}; use crate::state::{ ConstParameterValues, MultiValue, ParameterReturnValue, SetStateError, SimpleParameterValues, State, }; use crate::timestep::Timestep; pub use activation_function::ActivationFunction; -pub use aggregated::AggregatedParameterBuilder; -pub use aggregated_index::AggregatedIndexParameter; -pub use array::{Array1ParameterBuilder, Array2ParameterBuilder}; -pub use asymmetric::AsymmetricSwitchIndexParameter; -pub use constant::ConstantParameterBuilder; -pub use constant_scenario::ConstantScenarioParameter; +pub use aggregated::{AggregatedParameter, AggregatedParameterBuilder}; +pub use aggregated_index::{AggregatedIndexParameter, AggregatedIndexParameterBuilder}; +pub use array::{Array1Parameter, Array1ParameterBuilder, Array2Parameter, Array2ParameterBuilder}; +pub use asymmetric::{AsymmetricSwitchIndexParameter, AsymmetricSwitchIndexParameterBuilder}; +pub use constant::{ConstantParameter, ConstantParameterBuilder}; +pub use constant_scenario::{ConstantScenarioParameter, ConstantScenarioParameterBuilder}; pub use control_curves::{ - ApportionParameter, ControlCurveIndexParameter, ControlCurveInterpolatedParameterBuilder, ControlCurveParameter, - PiecewiseInterpolatedParameterBuilder, VolumeBetweenControlCurvesParameter, + ApportionParameter, ApportionParameterBuilder, ControlCurveIndexParameter, ControlCurveIndexParameterBuilder, + ControlCurveInterpolatedParameter, ControlCurveInterpolatedParameterBuilder, ControlCurveParameter, + ControlCurveParameterBuilder, PiecewiseInterpolatedParameter, PiecewiseInterpolatedParameterBuilder, + VolumeBetweenControlCurvesParameter, VolumeBetweenControlCurvesParameterBuilder, }; -pub use deficit::DeficitParameter; +pub use deficit::{DeficitParameter, DeficitParameterBuilder}; pub use delay::{DelayParameter, DelayParameterBuilder}; -pub use difference::DifferenceParameter; +pub use difference::{DifferenceParameter, DifferenceParameterBuilder}; pub use discount_factor::{DiscountFactorParameter, DiscountFactorParameterBuilder}; -pub use division::DivisionParameter; +pub use division::{DivisionParameter, DivisionParameterBuilder}; use errors::{ConstCalculationError, SimpleCalculationError}; pub use errors::{GeneralCalculationError, ParameterSetupError}; -pub use hydropower::{HydropowerTargetData, HydropowerTargetParameter}; -pub use indexed_array::IndexedArrayParameter; +pub use hydropower::{HydropowerTargetData, HydropowerTargetParameter, HydropowerTargetParameterBuilder}; +pub use indexed_array::{IndexedArrayParameter, IndexedArrayParameterBuilder}; pub use interpolate::{InterpolationError, interpolate, linear_interpolation}; -pub use interpolated::InterpolatedParameter; -pub use max::MaxParameter; -pub use min::MinParameter; +pub use interpolated::{InterpolatedParameter, InterpolatedParameterBuilder}; +pub use max::{MaxParameter, MaxParameterBuilder}; +pub use min::{MinParameter, MinParameterBuilder}; pub use multi_threshold::{MultiThresholdParameter, MultiThresholdParameterBuilder}; -pub use muskingum::{MuskingumInitialCondition, MuskingumParameter}; -pub use negative::NegativeParameter; -pub use negativemax::NegativeMaxParameter; -pub use negativemin::NegativeMinParameter; -pub use offset::OffsetParameter; -pub use polynomial::Polynomial1DParameter; +pub use muskingum::{MuskingumInitialCondition, MuskingumParameter, MuskingumParameterBuilder}; +use ndarray::ShapeError; +pub use negative::{NegativeParameter, NegativeParameterBuilder}; +pub use negativemax::{NegativeMaxParameter, NegativeMaxParameterBuilder}; +pub use negativemin::{NegativeMinParameter, NegativeMinParameterBuilder}; +pub use offset::{OffsetParameter, OffsetParameterBuilder}; +pub use polynomial::{Polynomial1DParameter, Polynomial1DParameterBuilder}; pub use profiles::{ - DailyProfileParameter, DiurnalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, - MonthlyProfileParameterBuilder, RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig, - UniformDrawdownProfileParameter, WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues, + DailyProfileParameter, DailyProfileParameterBuilder, DiurnalProfileParameter, DiurnalProfileParameterBuilder, + MonthlyInterpDay, MonthlyProfileParameter, MonthlyProfileParameterBuilder, RadialBasisFunction, + RbfProfileParameter, RbfProfileParameterBuilder, RbfProfileVariableConfig, UniformDrawdownProfileParameter, + UniformDrawdownProfileParameterBuilder, WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, + WeeklyProfileParameterBuilder, WeeklyProfileValues, }; #[cfg(feature = "pyo3")] -pub use py::{ParameterInfo, PyClassParameter, PyFuncParameter}; +pub use py::{ParameterInfo, PyClassParameter, PyClassParameterBuilder, PyFuncParameter, PyFuncParameterBuilder}; pub use rolling::{RollingParameter, RollingParameterBuilder}; use std::fmt; use std::fmt::{Display, Formatter}; @@ -91,8 +96,8 @@ use std::ops::Deref; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; use thiserror::Error; -pub use threshold::{Predicate, ThresholdParameter}; -pub use vector::VectorParameter; +pub use threshold::{Predicate, ThresholdParameter, ThresholdParameterBuilder}; +pub use vector::{VectorParameter, VectorParameterBuilder}; /// Simple parameter index. /// @@ -122,7 +127,7 @@ impl PartialEq for ConstParameterIndex { impl Eq for ConstParameterIndex {} impl ConstParameterIndex { - pub fn new(idx: usize) -> Self { + fn new(idx: usize) -> Self { Self { idx, phantom: PhantomData, @@ -172,7 +177,7 @@ impl PartialEq for SimpleParameterIndex { impl Eq for SimpleParameterIndex {} impl SimpleParameterIndex { - pub fn new(idx: usize) -> Self { + fn new(idx: usize) -> Self { Self { idx, phantom: PhantomData, @@ -460,7 +465,7 @@ impl From<&str> for ParameterName { /// Meta data common to all parameters. #[derive(Debug, Clone, PartialEq)] pub struct ParameterMeta { - name: ParameterName, + pub name: ParameterName, } impl ParameterMeta { @@ -738,8 +743,23 @@ pub trait GeneralParameter: Parameter { #[derive(Debug, Error)] pub enum ParameterBuildError { - #[error("Scenario group not found: {name}")] - ScenarioGroupNotFound { name: String }, + #[error("Scenario group not found: {}", .0.name)] + ScenarioGroupNotFound(#[from] ScenarioGroupNotFound), + #[error( + "Number of values ({values}) does not match the size ({scenarios}) of the specified scenario group '{group}'." + )] + ScenarioValuesLengthMismatch { + values: usize, + scenarios: usize, + group: String, + }, + #[error("Error subsetting array with dimensions {array_shape:?} with subset {subset:?}: {source}")] + ArraySubSetError { + array_shape: Vec, + subset: Vec, + #[source] + source: ShapeError, + }, #[error("Could not resolve f64 metric for `{attr}` attribute: {source}")] ResolveMetricF64Error { attr: String, @@ -752,6 +772,12 @@ pub enum ParameterBuildError { #[source] source: MetricU64ResolutionError, }, + #[error("Could not simplify f64 metric for `{attr}`: {source}")] + CouldNotSimplifyMetricF64 { + attr: String, + #[source] + source: MetricF64Error, + }, } pub enum BuiltParameter { @@ -845,6 +871,36 @@ macro_rules! resolve_metric_u64 { }; } +/// Resolve a single `Option` into a `Option` inside a +/// `ParameterBuilder::build` implementation. +/// +/// On `ParameterNotFound`, the macro early-returns `Ok(MaybeBuiltParameter::Retry($self))` +/// so the builder can be retried after more parameters are added. Any other +/// `MetricF64ResolutionError` is wrapped in `ParameterBuildError::ResolveMetricF64Error` +/// (tagged with `$attr`) and early-returned. +/// +#[macro_export] +macro_rules! resolve_optional_metric_f64 { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => { + match $unresolved { + Some(u) => match u.resolve($maps) { + Ok(m) => Some(m), + Err(err) => { + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + }, + None => None, + } + }; +} + /// Resolve a slice/`Vec` of `UnresolvedMetricF64` into a `Vec` inside a /// `ParameterBuilder::build` implementation. Same retry / error semantics as /// [`resolve_metric_f64!`]. @@ -881,6 +937,103 @@ macro_rules! resolve_metric_f64_vec { }}; } +/// Resolve a slice/`Vec` of `UnresolvedMetricU64` into a `Vec` inside a +/// `ParameterBuilder::build` implementation. Same retry / error semantics as +/// [`resolve_metric_u64!`]. +/// +/// `$unresolved` must be something that can be iterated as `&UnresolvedMetricU64` +/// and on which `.len()` is callable (e.g. `&self.values`, `self.indices.as_slice()`). +/// +#[macro_export] +macro_rules! resolve_metric_u64_vec { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + let unresolved = $unresolved; + let mut resolved = Vec::with_capacity(unresolved.len()); + for m in unresolved.iter() { + match m.resolve($maps) { + Ok(m) => resolved.push(m), + Err(err) => { + return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricU64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + } + } + resolved + }}; +} + +/// Resolve a `HashMap into a `HashMap` inside a +/// `ParameterBuilder::build` implementation. Same retry / error semantics as +/// [`resolve_metric_f64!`]. +/// +/// `$unresolved` must be something that can be iterated as `(&String, &UnresolvedMetricF64)` +/// and on which `.len()` is callable. +/// +#[macro_export] +macro_rules! resolve_metric_f64_hashmap { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + let unresolved = $unresolved; + let mut resolved = HashMap::with_capacity(unresolved.len()); + for (k, m) in unresolved.iter() { + match m.resolve($maps) { + Ok(m) => { + resolved.insert(k.clone(), m); + } + Err(err) => { + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + } + } + resolved + }}; +} + +/// Resolve a `HashMap into a `HashMap` inside a +/// `ParameterBuilder::build` implementation. Same retry / error semantics as +/// [`resolve_metric_u64!`]. +/// +/// `$unresolved` must be something that can be iterated as `(&String, &UnresolvedMetricU64)` +/// and on which `.len()` is callable. +/// +#[macro_export] +macro_rules! resolve_metric_u64_hashmap { + ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + let unresolved = $unresolved; + let mut resolved = HashMap::with_capacity(unresolved.len()); + for (k, m) in unresolved.iter() { + match m.resolve($maps) { + Ok(m) => { + resolved.insert(k.clone(), m); + } + Err(err) => { + return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { .. } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + } else { + Err($crate::parameters::ParameterBuildError::ResolveMetricU64Error { + attr: ($attr).to_string(), + source: err, + }) + }; + } + } + } + resolved + }}; +} + /// A trait that defines a component that produces a value each time-step. /// /// The trait is generic over the type of the value produced. @@ -1543,6 +1696,56 @@ impl ParameterCollection { } } + /// Push a new general parameter to the collection. + /// + /// The new parameter will be simplified as much as possible. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_general_f64(&mut self, p: Box>) -> ParameterIndex { + match p.try_into_simple() { + Some(p) => self.push_simple_f64(p), + None => { + let index = GeneralParameterIndex::new(self.general_f64.len()); + + self.general_resolve_order.push(GeneralParameterType::Parameter(index)); + self.general_f64.push(p); + + ParameterIndex::General(index) + } + } + } + + /// Push a new simple parameter to the collection. + /// + /// The new parameter will be simplified as much as possible. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_simple_f64(&mut self, p: Box>) -> ParameterIndex { + match p.try_into_const() { + Some(p) => self.push_const_f64(p), + None => { + let index = SimpleParameterIndex::new(self.simple_f64.len()); + + self.simple_resolve_order.push(SimpleParameterType::Parameter(index)); + self.simple_f64.push(p); + + ParameterIndex::Simple(index) + } + } + } + + /// Push a new const parameter to the collection. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_const_f64(&mut self, p: Box>) -> ParameterIndex { + let index = ConstParameterIndex::new(self.constant_f64.len()); + + self.constant_resolve_order.push(ConstParameterType::from(index)); + self.constant_f64.push(p); + + ParameterIndex::Const(index) + } + pub fn get_f64(&self, index: ParameterIndex) -> Option<&dyn Parameter> { match index { ParameterIndex::Const(idx) => self.constant_f64.get(*idx.deref()).map(|p| p.as_parameter()), @@ -1586,6 +1789,56 @@ impl ParameterCollection { } } + /// Push a new general parameter to the collection. + /// + /// The new parameter will be simplified as much as possible. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_general_u64(&mut self, p: Box>) -> ParameterIndex { + match p.try_into_simple() { + Some(p) => self.push_simple_u64(p), + None => { + let index = GeneralParameterIndex::new(self.general_u64.len()); + + self.general_resolve_order.push(GeneralParameterType::Index(index)); + self.general_u64.push(p); + + ParameterIndex::General(index) + } + } + } + + /// Push a new simple parameter to the collection. + /// + /// The new parameter will be simplified as much as possible. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_simple_u64(&mut self, p: Box>) -> ParameterIndex { + match p.try_into_const() { + Some(p) => self.push_const_u64(p), + None => { + let index = SimpleParameterIndex::new(self.simple_u64.len()); + + self.simple_resolve_order.push(SimpleParameterType::Index(index)); + self.simple_u64.push(p); + + ParameterIndex::Simple(index) + } + } + } + + /// Push a new const parameter to the collection. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_const_u64(&mut self, p: Box>) -> ParameterIndex { + let index = ConstParameterIndex::new(self.constant_u64.len()); + + self.constant_resolve_order.push(ConstParameterType::from(index)); + self.constant_u64.push(p); + + ParameterIndex::Const(index) + } + pub fn get_u64(&self, index: ParameterIndex) -> Option<&dyn Parameter> { match index { ParameterIndex::Const(idx) => self.constant_u64.get(*idx.deref()).map(|p| p.as_parameter()), @@ -1629,6 +1882,56 @@ impl ParameterCollection { } } + /// Push a new general parameter to the collection. + /// + /// The new parameter will be simplified as much as possible. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_general_multi(&mut self, p: Box>) -> ParameterIndex { + match p.try_into_simple() { + Some(p) => self.push_simple_multi(p), + None => { + let index = GeneralParameterIndex::new(self.general_multi.len()); + + self.general_resolve_order.push(GeneralParameterType::Multi(index)); + self.general_multi.push(p); + + ParameterIndex::General(index) + } + } + } + + /// Push a new simple parameter to the collection. + /// + /// The new parameter will be simplified as much as possible. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_simple_multi(&mut self, p: Box>) -> ParameterIndex { + match p.try_into_const() { + Some(p) => self.push_const_multi(p), + None => { + let index = SimpleParameterIndex::new(self.simple_multi.len()); + + self.simple_resolve_order.push(SimpleParameterType::Multi(index)); + self.simple_multi.push(p); + + ParameterIndex::Simple(index) + } + } + } + + /// Push a new const parameter to the collection. + /// + /// SAFETY: This must remain a private function to maintain the indexing guarantees. + fn push_const_multi(&mut self, p: Box>) -> ParameterIndex { + let index = ConstParameterIndex::new(self.constant_multi.len()); + + self.constant_resolve_order.push(ConstParameterType::from(index)); + self.constant_multi.push(p); + + ParameterIndex::Const(index) + } + pub fn get_multi(&self, index: &ParameterIndex) -> Option<&dyn Parameter> { match index { ParameterIndex::Const(idx) => self.constant_multi.get(*idx.deref()).map(|p| p.as_parameter()), @@ -2204,7 +2507,7 @@ pub enum ParameterCollectionBuilderError { ParameterBuildError { name: ParameterName, #[source] - source: ParameterBuildError, + source: Box, }, #[error("Circulate parameters found: {names:?}")] CircularParameterReference { names: Vec }, @@ -2245,6 +2548,13 @@ impl ParameterCollectionBuilder { self.f64.len() + self.u64.len() + self.multi.len() } + /// Returns true if the builder contains a parameter with the `name`. + pub fn contains_name(&self, name: &ParameterName) -> bool { + self.f64.iter().any(|p| p.name() == name) + || self.u64.iter().any(|p| p.name() == name) + || self.multi.iter().any(|p| p.name() == name) + } + pub fn build( mut self, resolution_maps: &mut ResolutionMaps, @@ -2271,31 +2581,9 @@ impl ParameterCollectionBuilder { MaybeBuiltParameter::Built(built) => { // Parameter successfully built. Let's add it to the collection, resolve order and resolution map. let idx = match built { - BuiltParameter::General(p) => { - let idx = GeneralParameterIndex::new(collection.general_f64.len()); - collection.general_f64.push(p); - collection - .general_resolve_order - .push(GeneralParameterType::Parameter(idx)); - ParameterIndex::General(idx) - } - BuiltParameter::Simple(p) => { - let idx = SimpleParameterIndex::new(collection.simple_f64.len()); - collection.simple_f64.push(p); - collection - .simple_resolve_order - .push(SimpleParameterType::Parameter(idx)); - ParameterIndex::Simple(idx) - } - BuiltParameter::Const(p) => { - let idx = ConstParameterIndex::new(collection.constant_f64.len()); - collection.constant_f64.push(p); - collection - .constant_resolve_order - .push(ConstParameterType::Parameter(idx)); - - ParameterIndex::Const(idx) - } + BuiltParameter::General(p) => collection.push_general_f64(p), + BuiltParameter::Simple(p) => collection.push_simple_f64(p), + BuiltParameter::Const(p) => collection.push_const_f64(p), }; resolution_maps.parameters_f64.insert(name, idx); @@ -2305,7 +2593,12 @@ impl ParameterCollectionBuilder { } } } - Err(err) => return Err(ParameterCollectionBuilderError::ParameterBuildError { name, source: err }), + Err(source) => { + return Err(ParameterCollectionBuilderError::ParameterBuildError { + name, + source: Box::new(source), + }); + } } } @@ -2322,25 +2615,9 @@ impl ParameterCollectionBuilder { MaybeBuiltParameter::Built(built) => { // Parameter successfully built. Let's add it to the collection, resolve order and resolution map. let idx = match built { - BuiltParameter::General(p) => { - let idx = GeneralParameterIndex::new(collection.general_u64.len()); - collection.general_u64.push(p); - collection.general_resolve_order.push(GeneralParameterType::Index(idx)); - ParameterIndex::General(idx) - } - BuiltParameter::Simple(p) => { - let idx = SimpleParameterIndex::new(collection.simple_u64.len()); - collection.simple_u64.push(p); - collection.simple_resolve_order.push(SimpleParameterType::Index(idx)); - ParameterIndex::Simple(idx) - } - BuiltParameter::Const(p) => { - let idx = ConstParameterIndex::new(collection.constant_u64.len()); - collection.constant_u64.push(p); - collection.constant_resolve_order.push(ConstParameterType::Index(idx)); - - ParameterIndex::Const(idx) - } + BuiltParameter::General(p) => collection.push_general_u64(p), + BuiltParameter::Simple(p) => collection.push_simple_u64(p), + BuiltParameter::Const(p) => collection.push_const_u64(p), }; resolution_maps.parameters_u64.insert(name, idx); @@ -2350,7 +2627,12 @@ impl ParameterCollectionBuilder { } } } - Err(err) => return Err(ParameterCollectionBuilderError::ParameterBuildError { name, source: err }), + Err(source) => { + return Err(ParameterCollectionBuilderError::ParameterBuildError { + name, + source: Box::new(source), + }); + } } } @@ -2367,25 +2649,9 @@ impl ParameterCollectionBuilder { MaybeBuiltParameter::Built(built) => { // Parameter successfully built. Let's add it to the collection, resolve order and resolution map. let idx = match built { - BuiltParameter::General(p) => { - let idx = GeneralParameterIndex::new(collection.general_multi.len()); - collection.general_multi.push(p); - collection.general_resolve_order.push(GeneralParameterType::Multi(idx)); - ParameterIndex::General(idx) - } - BuiltParameter::Simple(p) => { - let idx = SimpleParameterIndex::new(collection.simple_multi.len()); - collection.simple_multi.push(p); - collection.simple_resolve_order.push(SimpleParameterType::Multi(idx)); - ParameterIndex::Simple(idx) - } - BuiltParameter::Const(p) => { - let idx = ConstParameterIndex::new(collection.constant_multi.len()); - collection.constant_multi.push(p); - collection.constant_resolve_order.push(ConstParameterType::Multi(idx)); - - ParameterIndex::Const(idx) - } + BuiltParameter::General(p) => collection.push_general_multi(p), + BuiltParameter::Simple(p) => collection.push_simple_multi(p), + BuiltParameter::Const(p) => collection.push_const_multi(p), }; resolution_maps.parameters_multi.insert(name, idx); @@ -2395,7 +2661,12 @@ impl ParameterCollectionBuilder { } } } - Err(err) => return Err(ParameterCollectionBuilderError::ParameterBuildError { name, source: err }), + Err(source) => { + return Err(ParameterCollectionBuilderError::ParameterBuildError { + name, + source: Box::new(source), + }); + } } } @@ -2439,6 +2710,7 @@ mod tests { use crate::parameters::errors::{ConstCalculationError, SimpleCalculationError}; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, MultiValue}; + use crate::test_utils::default_domain; struct TestParameterBuilder { meta: ParameterMeta, @@ -2609,20 +2881,20 @@ mod tests { collection.f64(Box::new(TestParameterBuilder::default())); collection.f64(Box::new(TestParameterBuilder::default())); - assert!(collection.build(&mut ResolutionMaps::default()).is_err()); + assert!(collection.build(&mut ResolutionMaps::new(default_domain())).is_err()); let mut collection = ParameterCollectionBuilder::default(); collection.u64(Box::new(TestParameterBuilder::default())); collection.u64(Box::new(TestParameterBuilder::default())); - assert!(collection.build(&mut ResolutionMaps::default()).is_err()); + assert!(collection.build(&mut ResolutionMaps::new(default_domain())).is_err()); let mut collection = ParameterCollectionBuilder::default(); collection.f64(Box::new(TestParameterBuilder::default())); collection.u64(Box::new(TestParameterBuilder::default())); - assert!(collection.build(&mut ResolutionMaps::default()).is_err()); + assert!(collection.build(&mut ResolutionMaps::new(default_domain())).is_err()); } } diff --git a/pywr-core/src/parameters/multi_threshold.rs b/pywr-core/src/parameters/multi_threshold.rs index 35148821..e6245a10 100644 --- a/pywr-core/src/parameters/multi_threshold.rs +++ b/pywr-core/src/parameters/multi_threshold.rs @@ -121,7 +121,7 @@ impl ParameterBuilder for MultiThresholdParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metrics"); + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); let thresholds = resolve_metric_f64_vec!(self, &self.thresholds, resolution_maps, "thresholds"); let p = MultiThresholdParameter { diff --git a/pywr-core/src/parameters/muskingum.rs b/pywr-core/src/parameters/muskingum.rs index 26c2783b..e2c3007e 100644 --- a/pywr-core/src/parameters/muskingum.rs +++ b/pywr-core/src/parameters/muskingum.rs @@ -1,8 +1,10 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::{ - GeneralCalculationError, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, + BuiltParameter, GeneralCalculationError, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, + ParameterBuilder, ParameterMeta, ParameterName, ParameterState, }; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, State}; use crate::timestep::Timestep; @@ -42,26 +44,6 @@ pub struct MuskingumParameter { initial_condition: MuskingumInitialCondition, } -impl MuskingumParameter { - pub fn new( - name: ParameterName, - inflow: MetricF64, - outflow: MetricF64, - travel_time: MetricF64, - weight: MetricF64, - initial_condition: MuskingumInitialCondition, - ) -> Self { - Self { - meta: ParameterMeta::new(name), - inflow, - outflow, - travel_time, - weight, - initial_condition, - } - } -} - impl Parameter for MuskingumParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -139,3 +121,62 @@ fn steady_state_inflow_factor(weight: f64, travel_time: f64) -> f64 { fn steady_state_outflow_factor(weight: f64, travel_time: f64) -> f64 { 1.0 - (2.0 * travel_time * (1.0 - weight) - 1.0) / (2.0 * travel_time * (1.0 - weight) + 1.0) } + +pub struct MuskingumParameterBuilder { + meta: ParameterMeta, + inflow: UnresolvedMetricF64, + outflow: UnresolvedMetricF64, + /// Travel time of the flood wave through routing reach / node (K) + travel_time: UnresolvedMetricF64, + /// Dimensionless weight (0 ≤ X ≤ 0.5). + weight: UnresolvedMetricF64, + /// Initial condition for the Muskingum routing. + initial_condition: MuskingumInitialCondition, +} + +impl MuskingumParameterBuilder { + pub fn new( + name: ParameterName, + inflow: UnresolvedMetricF64, + outflow: UnresolvedMetricF64, + travel_time: UnresolvedMetricF64, + weight: UnresolvedMetricF64, + initial_condition: MuskingumInitialCondition, + ) -> Self { + Self { + meta: ParameterMeta::new(name), + inflow, + outflow, + travel_time, + weight, + initial_condition, + } + } +} + +impl ParameterBuilder for MuskingumParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let inflow = resolve_metric_f64!(self, self.inflow, resolution_maps, "inflow"); + let outflow = resolve_metric_f64!(self, self.outflow, resolution_maps, "outflow"); + let travel_time = resolve_metric_f64!(self, self.travel_time, resolution_maps, "travel_time"); + let weight = resolve_metric_f64!(self, self.weight, resolution_maps, "weight"); + + let p = MuskingumParameter { + meta: self.meta, + inflow, + outflow, + travel_time, + weight, + initial_condition: self.initial_condition, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/negative.rs b/pywr-core/src/parameters/negative.rs index 7e6050ee..afe793f4 100644 --- a/pywr-core/src/parameters/negative.rs +++ b/pywr-core/src/parameters/negative.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -11,15 +15,6 @@ pub struct NegativeParameter { metric: MetricF64, } -impl NegativeParameter { - pub fn new(name: ParameterName, metric: MetricF64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - } - } -} - impl Parameter for NegativeParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -47,3 +42,37 @@ impl GeneralParameter for NegativeParameter { self } } + +pub struct NegativeParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, +} + +impl NegativeParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + } + } +} + +impl ParameterBuilder for NegativeParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = NegativeParameter { + meta: self.meta, + metric, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/negativemax.rs b/pywr-core/src/parameters/negativemax.rs index d2ef9082..9b200486 100644 --- a/pywr-core/src/parameters/negativemax.rs +++ b/pywr-core/src/parameters/negativemax.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -12,16 +16,6 @@ pub struct NegativeMaxParameter { threshold: f64, } -impl NegativeMaxParameter { - pub fn new(name: ParameterName, metric: MetricF64, threshold: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - threshold, - } - } -} - impl Parameter for NegativeMaxParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -47,3 +41,40 @@ impl GeneralParameter for NegativeMaxParameter { self } } + +pub struct NegativeMaxParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + threshold: f64, +} + +impl NegativeMaxParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl ParameterBuilder for NegativeMaxParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = NegativeMaxParameter { + meta: self.meta, + metric, + threshold: self.threshold, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/negativemin.rs b/pywr-core/src/parameters/negativemin.rs index 1b9233dd..68453fec 100644 --- a/pywr-core/src/parameters/negativemin.rs +++ b/pywr-core/src/parameters/negativemin.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -12,16 +16,6 @@ pub struct NegativeMinParameter { threshold: f64, } -impl NegativeMinParameter { - pub fn new(name: ParameterName, metric: MetricF64, threshold: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - threshold, - } - } -} - impl Parameter for NegativeMinParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -47,3 +41,40 @@ impl GeneralParameter for NegativeMinParameter { self } } + +pub struct NegativeMinParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + threshold: f64, +} + +impl NegativeMinParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl ParameterBuilder for NegativeMinParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = NegativeMinParameter { + meta: self.meta, + metric, + threshold: self.threshold, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/offset.rs b/pywr-core/src/parameters/offset.rs index f1aac633..c68cc743 100644 --- a/pywr-core/src/parameters/offset.rs +++ b/pywr-core/src/parameters/offset.rs @@ -1,11 +1,12 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ - ActivationFunction, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, VariableConfig, - VariableParameter, VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, - downcast_variable_config_ref, + ActivationFunction, BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, + ParameterBuilder, ParameterMeta, ParameterName, ParameterState, VariableConfig, VariableParameter, + VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, }; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -20,14 +21,6 @@ pub struct OffsetParameter { type InternalValue = Option; impl OffsetParameter { - pub fn new(name: ParameterName, metric: MetricF64, offset: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - offset, - } - } - /// Return the current value. /// /// If the internal state is None, the value is returned directly. Otherwise, the internal value must @@ -120,3 +113,40 @@ impl VariableParameter for OffsetParameter { Some(vec![activation_function.upper_bound()]) } } + +pub struct OffsetParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + offset: f64, +} + +impl OffsetParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, offset: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + offset, + } + } +} + +impl ParameterBuilder for OffsetParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = OffsetParameter { + meta: self.meta, + metric, + offset: self.offset, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/polynomial.rs b/pywr-core/src/parameters/polynomial.rs index e6d4a457..e58d06af 100644 --- a/pywr-core/src/parameters/polynomial.rs +++ b/pywr-core/src/parameters/polynomial.rs @@ -1,7 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -14,18 +18,6 @@ pub struct Polynomial1DParameter { offset: f64, } -impl Polynomial1DParameter { - pub fn new(name: ParameterName, metric: MetricF64, coefficients: Vec, scale: f64, offset: f64) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - coefficients, - scale, - offset, - } - } -} - impl Parameter for Polynomial1DParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -60,3 +52,56 @@ impl GeneralParameter for Polynomial1DParameter { self } } + +pub struct Polynomial1DParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + coefficients: Vec, + scale: f64, + offset: f64, +} + +impl Polynomial1DParameterBuilder { + pub fn new(name: ParameterName, metric: UnresolvedMetricF64, coefficients: Vec) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + coefficients, + scale: 1.0, + offset: 0.0, + } + } + + pub fn scale(&mut self, scale: f64) -> &mut Self { + self.scale = scale; + self + } + + pub fn offset(&mut self, offset: f64) -> &mut Self { + self.offset = offset; + self + } +} + +impl ParameterBuilder for Polynomial1DParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + + let p = Polynomial1DParameter { + meta: self.meta, + metric, + coefficients: self.coefficients, + scale: self.scale, + offset: self.offset, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/profiles/daily.rs b/pywr-core/src/parameters/profiles/daily.rs index 1f0c563e..618df3e4 100644 --- a/pywr-core/src/parameters/profiles/daily.rs +++ b/pywr-core/src/parameters/profiles/daily.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; @@ -9,15 +13,6 @@ pub struct DailyProfileParameter { values: [f64; 366], } -impl DailyProfileParameter { - pub fn new(name: ParameterName, values: [f64; 366]) -> Self { - Self { - meta: ParameterMeta::new(name), - values, - } - } -} - impl Parameter for DailyProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -42,3 +37,35 @@ impl SimpleParameter for DailyProfileParameter { self } } + +pub struct DailyProfileParameterBuilder { + meta: ParameterMeta, + values: [f64; 366], +} + +impl DailyProfileParameterBuilder { + pub fn new(name: ParameterName, values: [f64; 366]) -> Self { + Self { + meta: ParameterMeta::new(name), + values, + } + } +} + +impl ParameterBuilder for DailyProfileParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = DailyProfileParameter { + meta: self.meta, + values: self.values, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/profiles/diurnal.rs b/pywr-core/src/parameters/profiles/diurnal.rs index 61f248f0..90f1769b 100644 --- a/pywr-core/src/parameters/profiles/diurnal.rs +++ b/pywr-core/src/parameters/profiles/diurnal.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; @@ -13,15 +17,6 @@ pub struct DiurnalProfileParameter { values: [f64; 24], } -impl DiurnalProfileParameter { - pub fn new(name: ParameterName, values: [f64; 24]) -> Self { - Self { - meta: ParameterMeta::new(name), - values, - } - } -} - impl Parameter for DiurnalProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -46,3 +41,35 @@ impl SimpleParameter for DiurnalProfileParameter { self } } + +pub struct DiurnalProfileParameterBuilder { + meta: ParameterMeta, + values: [f64; 24], +} + +impl DiurnalProfileParameterBuilder { + pub fn new(name: ParameterName, values: [f64; 24]) -> Self { + Self { + meta: ParameterMeta::new(name), + values, + } + } +} + +impl ParameterBuilder for DiurnalProfileParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = DiurnalProfileParameter { + meta: self.meta, + values: self.values, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/profiles/mod.rs b/pywr-core/src/parameters/profiles/mod.rs index dc42a7c1..2e5b0977 100644 --- a/pywr-core/src/parameters/profiles/mod.rs +++ b/pywr-core/src/parameters/profiles/mod.rs @@ -5,9 +5,11 @@ mod rbf; mod uniform_drawdown; mod weekly; -pub use daily::DailyProfileParameter; -pub use diurnal::DiurnalProfileParameter; +pub use daily::{DailyProfileParameter, DailyProfileParameterBuilder}; +pub use diurnal::{DiurnalProfileParameter, DiurnalProfileParameterBuilder}; pub use monthly::{MonthlyInterpDay, MonthlyProfileParameter, MonthlyProfileParameterBuilder}; -pub use rbf::{RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig}; -pub use uniform_drawdown::UniformDrawdownProfileParameter; -pub use weekly::{WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues}; +pub use rbf::{RadialBasisFunction, RbfProfileParameter, RbfProfileParameterBuilder, RbfProfileVariableConfig}; +pub use uniform_drawdown::{UniformDrawdownProfileParameter, UniformDrawdownProfileParameterBuilder}; +pub use weekly::{ + WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileParameterBuilder, WeeklyProfileValues, +}; diff --git a/pywr-core/src/parameters/profiles/monthly.rs b/pywr-core/src/parameters/profiles/monthly.rs index c2467f3f..8594fcd8 100644 --- a/pywr-core/src/parameters/profiles/monthly.rs +++ b/pywr-core/src/parameters/profiles/monthly.rs @@ -22,7 +22,7 @@ pub struct MonthlyProfileParameter { } pub struct MonthlyProfileParameterBuilder { - name: ParameterName, + meta: ParameterMeta, values: [f64; 12], interp_day: Option, } @@ -30,7 +30,7 @@ pub struct MonthlyProfileParameterBuilder { impl MonthlyProfileParameterBuilder { pub fn new(name: ParameterName, values: [f64; 12]) -> Self { Self { - name, + meta: ParameterMeta::new(name), values, interp_day: None, } @@ -44,7 +44,7 @@ impl MonthlyProfileParameterBuilder { impl ParameterBuilder for MonthlyProfileParameterBuilder { fn name(&self) -> &ParameterName { - &self.name + &self.meta.name } fn build( @@ -52,7 +52,7 @@ impl ParameterBuilder for MonthlyProfileParameterBuilder { _resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { Ok(BuiltParameter::Simple(Box::new(MonthlyProfileParameter { - meta: ParameterMeta::new(self.name.clone()), + meta: self.meta, values: self.values, interp_day: self.interp_day, })) diff --git a/pywr-core/src/parameters/profiles/rbf.rs b/pywr-core/src/parameters/profiles/rbf.rs index c00dac18..5588b531 100644 --- a/pywr-core/src/parameters/profiles/rbf.rs +++ b/pywr-core/src/parameters/profiles/rbf.rs @@ -1,7 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::{ParameterSetupError, SimpleCalculationError}; use crate::parameters::{ - Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter, VariableConfig, VariableParameter, - VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, VariableConfig, VariableParameter, VariableParameterError, + downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, }; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; @@ -100,16 +102,6 @@ impl RbfProfileInternalState { } } -impl RbfProfileParameter { - pub fn new(name: ParameterName, points: Vec<(u32, f64)>, function: RadialBasisFunction) -> Self { - Self { - meta: ParameterMeta::new(name), - points, - function, - } - } -} - impl Parameter for RbfProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -385,6 +377,41 @@ fn interpolate_rbf_profile(points: &[(u32, f64)], function: &RadialBasisFunction profile.try_into().unwrap() } +pub struct RbfProfileParameterBuilder { + meta: ParameterMeta, + points: Vec<(u32, f64)>, + function: RadialBasisFunction, +} + +impl RbfProfileParameterBuilder { + pub fn new(name: ParameterName, points: Vec<(u32, f64)>, function: RadialBasisFunction) -> Self { + Self { + meta: ParameterMeta::new(name), + points, + function, + } + } +} + +impl ParameterBuilder for RbfProfileParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = RbfProfileParameter { + meta: self.meta, + points: self.points, + function: self.function, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) + } +} + #[cfg(test)] mod tests { use crate::parameters::profiles::rbf::{RadialBasisFunction, interpolate_rbf, interpolate_rbf_profile}; diff --git a/pywr-core/src/parameters/profiles/uniform_drawdown.rs b/pywr-core/src/parameters/profiles/uniform_drawdown.rs index e876e22b..12438c51 100644 --- a/pywr-core/src/parameters/profiles/uniform_drawdown.rs +++ b/pywr-core/src/parameters/profiles/uniform_drawdown.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; @@ -15,21 +19,6 @@ pub struct UniformDrawdownProfileParameter { reset_doy: u16, } -impl UniformDrawdownProfileParameter { - pub fn new(name: ParameterName, reset_day: u32, reset_month: u32, residual_days: u8) -> Self { - // Calculate the reset day of year in a known leap year. - let reset_doy = NaiveDate::from_ymd_opt(2016, reset_month, reset_day) - .expect("Invalid reset day") - .ordinal() as u16; - - Self { - meta: ParameterMeta::new(name), - residual_days, - reset_doy, - } - } -} - impl Parameter for UniformDrawdownProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -85,3 +74,43 @@ impl SimpleParameter for UniformDrawdownProfileParameter { self } } + +pub struct UniformDrawdownProfileParameterBuilder { + meta: ParameterMeta, + residual_days: u8, + reset_doy: u16, +} + +impl UniformDrawdownProfileParameterBuilder { + pub fn new(name: ParameterName, reset_day: u32, reset_month: u32, residual_days: u8) -> Self { + // Calculate the reset day of year in a known leap year. + let reset_doy = NaiveDate::from_ymd_opt(2016, reset_month, reset_day) + .expect("Invalid reset day") + .ordinal() as u16; + + Self { + meta: ParameterMeta::new(name), + residual_days, + reset_doy, + } + } +} + +impl ParameterBuilder for UniformDrawdownProfileParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = UniformDrawdownProfileParameter { + meta: self.meta, + residual_days: self.residual_days, + reset_doy: self.reset_doy, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/profiles/weekly.rs b/pywr-core/src/parameters/profiles/weekly.rs index 45d3d8e5..be1f4972 100644 --- a/pywr-core/src/parameters/profiles/weekly.rs +++ b/pywr-core/src/parameters/profiles/weekly.rs @@ -1,5 +1,9 @@ +use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + BuiltParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, + ParameterName, ParameterState, SimpleParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; @@ -159,16 +163,6 @@ pub struct WeeklyProfileParameter { interp_day: Option, } -impl WeeklyProfileParameter { - pub fn new(name: ParameterName, values: WeeklyProfileValues, interp_day: Option) -> Self { - Self { - meta: ParameterMeta::new(name), - values, - interp_day, - } - } -} - impl Parameter for WeeklyProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -194,6 +188,46 @@ impl SimpleParameter for WeeklyProfileParameter { } } +pub struct WeeklyProfileParameterBuilder { + meta: ParameterMeta, + values: WeeklyProfileValues, + interp_day: Option, +} + +impl WeeklyProfileParameterBuilder { + pub fn new(name: ParameterName, values: WeeklyProfileValues) -> Self { + Self { + meta: ParameterMeta::new(name), + values, + interp_day: None, + } + } + + pub fn interp_day(&mut self, interp_day: WeeklyInterpDay) -> &mut Self { + self.interp_day = Some(interp_day); + self + } +} + +impl ParameterBuilder for WeeklyProfileParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = WeeklyProfileParameter { + meta: self.meta, + values: self.values, + interp_day: self.interp_day, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) + } +} + #[cfg(test)] mod tests { use crate::parameters::profiles::weekly::{WeeklyInterpDay, WeeklyProfileValues}; diff --git a/pywr-core/src/parameters/py.rs b/pywr-core/src/parameters/py.rs index 8043b5b9..7139621d 100644 --- a/pywr-core/src/parameters/py.rs +++ b/pywr-core/src/parameters/py.rs @@ -1,10 +1,14 @@ -use super::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, Timestep}; -use crate::metric::{MetricF64, MetricU64}; -use crate::network::Network; +use super::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, Timestep, +}; +use crate::metric::{MetricF64, MetricU64, UnresolvedMetricF64, UnresolvedMetricU64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::downcast_internal_state_mut; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, State}; +use crate::{resolve_metric_f64_hashmap, resolve_metric_u64_hashmap}; use ahash::RandomState; use pyo3::exceptions::PyKeyError; use pyo3::prelude::*; @@ -70,22 +74,6 @@ impl PartialEq for PyCommon { } impl PyCommon { - fn new( - meta: ParameterMeta, - args: Py, - kwargs: Py, - metrics: &HashMap, - indices: &HashMap, - ) -> Self { - Self { - meta, - args, - kwargs, - metrics: metrics.clone(), - indices: indices.clone(), - } - } - fn update_metrics( &self, network: &Network, @@ -115,6 +103,36 @@ impl PyCommon { } } +struct PyCommonBuilder { + meta: ParameterMeta, + args: Py, + kwargs: Py, + metrics: HashMap, + indices: HashMap, +} + +impl PyCommonBuilder { + fn new(meta: ParameterMeta, args: Py, kwargs: Py) -> Self { + Self { + meta, + args, + kwargs, + metrics: HashMap::new(), + indices: HashMap::new(), + } + } + + fn metric(&mut self, key: &str, value: UnresolvedMetricF64) -> &mut Self { + self.metrics.insert(key.to_string(), value); + self + } + + fn index(&mut self, key: &str, value: UnresolvedMetricU64) -> &mut Self { + self.indices.insert(key.to_string(), value); + self + } +} + /// A Python parameter that returns the value produced by a Python object. /// /// This parameter allows you to define a Python class that implements a `before` and/or `after` methods, @@ -163,20 +181,6 @@ fn ensure_parameter_info( } impl PyClassParameter { - pub fn new( - name: ParameterName, - object: Py, - args: Py, - kwargs: Py, - metrics: &HashMap, - indices: &HashMap, - ) -> Self { - Self { - class: object, - common: PyCommon::new(ParameterMeta::new(name), args, kwargs, metrics, indices), - } - } - fn setup(&self) -> Result>, ParameterSetupError> { Python::initialize(); @@ -374,6 +378,117 @@ impl GeneralParameter for PyClassParameter { } } +pub struct PyClassParameterBuilder { + class: Py, + common: PyCommonBuilder, +} + +impl PyClassParameterBuilder { + pub fn new(name: ParameterName, object: Py, args: Py, kwargs: Py) -> Self { + Self { + class: object, + common: PyCommonBuilder::new(ParameterMeta::new(name), args, kwargs), + } + } + + pub fn metric(&mut self, key: &str, value: UnresolvedMetricF64) -> &mut Self { + self.common.metric(key, value); + self + } + + pub fn index(&mut self, key: &str, value: UnresolvedMetricU64) -> &mut Self { + self.common.index(key, value); + self + } +} + +impl ParameterBuilder for PyClassParameterBuilder { + fn name(&self) -> &ParameterName { + &self.common.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, "indices"); + + let common = PyCommon { + meta: self.common.meta, + args: self.common.args, + kwargs: self.common.kwargs, + metrics, + indices, + }; + + let p = PyClassParameter { + class: self.class, + common, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + +impl ParameterBuilder for PyClassParameterBuilder { + fn name(&self) -> &ParameterName { + &self.common.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, "indices"); + + let common = PyCommon { + meta: self.common.meta, + args: self.common.args, + kwargs: self.common.kwargs, + metrics, + indices, + }; + + let p = PyClassParameter { + class: self.class, + common, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + +impl ParameterBuilder for PyClassParameterBuilder { + fn name(&self) -> &ParameterName { + &self.common.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, "indices"); + + let common = PyCommon { + meta: self.common.meta, + args: self.common.args, + kwargs: self.common.kwargs, + metrics, + indices, + }; + + let p = PyClassParameter { + class: self.class, + common, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + /// A Python parameter that returns the value produced by a Python function. /// /// This parameter allows you to define a Python function which takes a `ParameterInfo` object as its first argument, @@ -401,20 +516,6 @@ impl InternalInfo { } impl PyFuncParameter { - pub fn new( - name: ParameterName, - function: Py, - args: Py, - kwargs: Py, - metrics: &HashMap, - indices: &HashMap, - ) -> Self { - Self { - function, - common: PyCommon::new(ParameterMeta::new(name), args, kwargs, metrics, indices), - } - } - fn setup(&self) -> Result>, ParameterSetupError> { Python::initialize(); @@ -577,6 +678,117 @@ impl GeneralParameter for PyFuncParameter { } } +pub struct PyFuncParameterBuilder { + function: Py, + common: PyCommonBuilder, +} + +impl PyFuncParameterBuilder { + pub fn new(name: ParameterName, object: Py, args: Py, kwargs: Py) -> Self { + Self { + function: object, + common: PyCommonBuilder::new(ParameterMeta::new(name), args, kwargs), + } + } + + pub fn metric(&mut self, key: &str, value: UnresolvedMetricF64) -> &mut Self { + self.common.metric(key, value); + self + } + + pub fn index(&mut self, key: &str, value: UnresolvedMetricU64) -> &mut Self { + self.common.index(key, value); + self + } +} + +impl ParameterBuilder for PyFuncParameterBuilder { + fn name(&self) -> &ParameterName { + &self.common.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, "indices"); + + let common = PyCommon { + meta: self.common.meta, + args: self.common.args, + kwargs: self.common.kwargs, + metrics, + indices, + }; + + let p = PyFuncParameter { + function: self.function, + common, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + +impl ParameterBuilder for PyFuncParameterBuilder { + fn name(&self) -> &ParameterName { + &self.common.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, "indices"); + + let common = PyCommon { + meta: self.common.meta, + args: self.common.args, + kwargs: self.common.kwargs, + metrics, + indices, + }; + + let p = PyFuncParameter { + function: self.function, + common, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + +impl ParameterBuilder for PyFuncParameterBuilder { + fn name(&self) -> &ParameterName { + &self.common.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, "indices"); + + let common = PyCommon { + meta: self.common.meta, + args: self.common.args, + kwargs: self.common.kwargs, + metrics, + indices, + }; + + let p = PyFuncParameter { + function: self.function, + common, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} + #[cfg(test)] mod tests { use super::*; @@ -666,14 +878,15 @@ class MyParameter: let args = Python::attach(|py| PyTuple::new(py, [0]).unwrap().unbind()); let kwargs = Python::attach(|py| PyDict::new(py).unbind()); - let param = PyClassParameter::new( - "my-parameter".into(), - class, + let common = PyCommon { + meta: ParameterMeta::new("my-parameter".into()), args, kwargs, - &HashMap::new(), - &HashMap::new(), - ); + metrics: Default::default(), + indices: Default::default(), + }; + + let param = PyClassParameter { class, common }; let time = default_time_domain_builder().build().unwrap(); let timesteps = time.timesteps(); @@ -771,14 +984,15 @@ class MyParameter: let args = Python::attach(|py| PyTuple::new(py, [0]).unwrap().unbind()); let kwargs = Python::attach(|py| PyDict::new(py).unbind()); - let param = PyClassParameter::new( - "my-parameter".into(), - class, + let common = PyCommon { + meta: ParameterMeta::new("my-parameter".into()), args, kwargs, - &HashMap::new(), - &HashMap::new(), - ); + metrics: Default::default(), + indices: Default::default(), + }; + + let param = PyClassParameter { class, common }; let time = default_time_domain_builder().build().unwrap(); let timesteps = time.timesteps(); @@ -839,14 +1053,15 @@ def my_function(info, count, **kwargs): let args = Python::attach(|py| PyTuple::new(py, [2]).unwrap().unbind()); let kwargs = Python::attach(|py| PyDict::new(py).unbind()); - let param = PyFuncParameter::new( - "MyParameter".into(), - function, + let common = PyCommon { + meta: ParameterMeta::new("my-parameter".into()), args, kwargs, - &HashMap::new(), - &HashMap::new(), - ); + metrics: Default::default(), + indices: Default::default(), + }; + + let param = PyFuncParameter { function, common }; let time = default_time_domain_builder().build().unwrap(); let timesteps = time.timesteps(); diff --git a/pywr-core/src/parameters/rolling.rs b/pywr-core/src/parameters/rolling.rs index fae0241c..1048f9c5 100644 --- a/pywr-core/src/parameters/rolling.rs +++ b/pywr-core/src/parameters/rolling.rs @@ -459,7 +459,7 @@ mod tests { let parameter = RollingParameterBuilder::new( "my-parameter".into(), - UnresolvedMetricU64::new_index_parameter_before("my-metric"), + UnresolvedMetricU64::new_parameter_before("my-metric"), 3, 0, 3, diff --git a/pywr-core/src/parameters/threshold.rs b/pywr-core/src/parameters/threshold.rs index 4feb344f..79fc9a67 100644 --- a/pywr-core/src/parameters/threshold.rs +++ b/pywr-core/src/parameters/threshold.rs @@ -1,9 +1,11 @@ -use crate::metric::MetricF64; -use crate::network::Network; +use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ - GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, downcast_internal_state_mut, + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, downcast_internal_state_mut, }; +use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -37,24 +39,6 @@ pub struct ThresholdParameter { ratchet: bool, } -impl ThresholdParameter { - pub fn new( - name: ParameterName, - metric: MetricF64, - threshold: MetricF64, - predicate: Predicate, - ratchet: bool, - ) -> Self { - Self { - meta: ParameterMeta::new(name), - metric, - threshold, - predicate, - ratchet, - } - } -} - impl Parameter for ThresholdParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -108,3 +92,58 @@ impl GeneralParameter for ThresholdParameter { self } } + +pub struct ThresholdParameterBuilder { + meta: ParameterMeta, + metric: UnresolvedMetricF64, + threshold: UnresolvedMetricF64, + predicate: Predicate, + ratchet: bool, +} + +impl ThresholdParameterBuilder { + pub fn new( + name: ParameterName, + metric: UnresolvedMetricF64, + threshold: UnresolvedMetricF64, + predicate: Predicate, + ) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + predicate, + ratchet: false, + } + } + + /// Enable ratchet + pub fn ratchet(&mut self) -> &mut Self { + self.ratchet = true; + self + } +} + +impl ParameterBuilder for ThresholdParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + let threshold = resolve_metric_f64!(self, self.threshold, resolution_maps, "threshold"); + + let p = ThresholdParameter { + meta: self.meta, + metric, + threshold, + predicate: self.predicate, + ratchet: self.ratchet, + }; + + Ok(MaybeBuiltParameter::Built(BuiltParameter::General(Box::new(p)))) + } +} diff --git a/pywr-core/src/parameters/vector.rs b/pywr-core/src/parameters/vector.rs index 985b95f7..00b2611b 100644 --- a/pywr-core/src/parameters/vector.rs +++ b/pywr-core/src/parameters/vector.rs @@ -1,6 +1,9 @@ -use crate::network::Network; +use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; -use crate::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; +use crate::parameters::{ + BuiltParameter, GeneralParameter, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, + ParameterMeta, ParameterName, ParameterState, +}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; @@ -10,15 +13,6 @@ pub struct VectorParameter { values: Vec, } -impl VectorParameter { - pub fn new(name: ParameterName, values: Vec) -> Self { - Self { - meta: ParameterMeta::new(name), - values, - } - } -} - impl Parameter for VectorParameter { fn meta(&self) -> &ParameterMeta { &self.meta @@ -51,3 +45,36 @@ impl GeneralParameter for VectorParameter { self } } + +pub struct VectorParameterBuilder { + meta: ParameterMeta, + values: Vec, +} + +impl VectorParameterBuilder { + pub fn new(name: ParameterName, values: &[f64]) -> Self { + Self { + meta: ParameterMeta::new(name), + values: values.to_vec(), + } + } +} + +impl ParameterBuilder for VectorParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build( + self: Box, + _resolution_maps: &ResolutionMaps, + ) -> Result, ParameterBuildError> { + let p = VectorParameter { + meta: self.meta, + values: self.values, + }; + + let bp = BuiltParameter::General(Box::new(p)); + Ok(bp.into()) + } +} diff --git a/pywr-core/src/recorders/csv.rs b/pywr-core/src/recorders/csv.rs index 336d0d9e..b61df4a2 100644 --- a/pywr-core/src/recorders/csv.rs +++ b/pywr-core/src/recorders/csv.rs @@ -1,10 +1,10 @@ use super::{ - MetricSetState, Recorder, RecorderFinalResult, RecorderFinaliseError, RecorderInternalState, RecorderMeta, - RecorderSaveError, RecorderSetupError, Timestep, downcast_internal_state, downcast_internal_state_mut, + MetricSetState, Recorder, RecorderBuilder, RecorderBuilderError, RecorderFinalResult, RecorderFinaliseError, + RecorderInternalState, RecorderMeta, RecorderSaveError, RecorderSetupError, Timestep, downcast_internal_state, + downcast_internal_state_mut, }; use crate::models::ModelDomain; -use crate::network::Network; -use crate::recorders::metric_set::MetricSetIndex; +use crate::network::{MetricSetIndex, Network, ResolutionMaps}; use crate::scenario::ScenarioIndex; use crate::state::State; use chrono::NaiveDateTime; @@ -41,14 +41,6 @@ struct Internal { } impl CsvWideFmtOutput { - pub fn new>(name: &str, filename: P, metric_set_idx: MetricSetIndex) -> Self { - Self { - meta: RecorderMeta::new(name), - filename: filename.into(), - metric_set_idx, - } - } - fn write_values(&self, metric_set_states: &[Vec], internal: &mut Internal) -> Result<(), CsvError> { let mut row = Vec::new(); @@ -191,6 +183,46 @@ impl Recorder for CsvWideFmtOutput { } } +/// Output the values from a [`crate::recorders::MetricSet`] to a CSV file. +#[derive(Clone, Debug)] +pub struct CsvWideFmtOutputBuilder { + meta: RecorderMeta, + filename: PathBuf, + metric_set: String, +} + +impl CsvWideFmtOutputBuilder { + pub fn new>(name: &str, filename: P, metric_set: &str) -> Self { + Self { + meta: RecorderMeta::new(name), + filename: filename.into(), + metric_set: metric_set.to_string(), + } + } +} + +impl RecorderBuilder for CsvWideFmtOutputBuilder { + fn name(&self) -> &str { + &self.meta.name + } + + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric_set_idx = resolution_maps.metric_sets.get(&self.metric_set).ok_or_else(|| { + RecorderBuilderError::MetricSetNotFound { + name: self.metric_set.clone(), + } + })?; + + let r = CsvWideFmtOutput { + meta: self.meta, + filename: self.filename, + metric_set_idx: *metric_set_idx, + }; + + Ok(Box::new(r)) + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct CsvLongFmtRecord { time_start: NaiveDateTime, @@ -217,20 +249,6 @@ pub struct CsvLongFmtOutput { } impl CsvLongFmtOutput { - pub fn new>( - name: &str, - filename: P, - metric_set_indices: &[MetricSetIndex], - decimal_places: Option, - ) -> Self { - Self { - meta: RecorderMeta::new(name), - filename: filename.into(), - metric_set_indices: metric_set_indices.to_vec(), - decimal_places, - } - } - fn write_values( &self, network: &Network, @@ -332,3 +350,56 @@ impl Recorder for CsvLongFmtOutput { Ok(None) } } + +#[derive(Clone, Debug)] +pub struct CsvLongFmtOutputBuilder { + meta: RecorderMeta, + filename: PathBuf, + metric_sets: Vec, + decimal_places: Option, +} + +impl CsvLongFmtOutputBuilder { + pub fn new>(name: &str, filename: P, decimal_places: Option) -> Self { + Self { + meta: RecorderMeta::new(name), + filename: filename.into(), + metric_sets: vec![], + decimal_places, + } + } + + pub fn metric_set(&mut self, metric_set: &str) -> &mut Self { + self.metric_sets.push(metric_set.to_string()); + self + } +} + +impl RecorderBuilder for CsvLongFmtOutputBuilder { + fn name(&self) -> &str { + &self.meta.name + } + + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric_set_indices = self + .metric_sets + .iter() + .map(|ms| { + resolution_maps + .metric_sets + .get(ms) + .copied() + .ok_or_else(|| RecorderBuilderError::MetricSetNotFound { name: ms.clone() }) + }) + .collect::, _>>()?; + + let r = CsvLongFmtOutput { + meta: self.meta, + filename: self.filename, + metric_set_indices, + decimal_places: self.decimal_places, + }; + + Ok(Box::new(r)) + } +} diff --git a/pywr-core/src/recorders/hdf.rs b/pywr-core/src/recorders/hdf.rs index 45c1258e..24347576 100644 --- a/pywr-core/src/recorders/hdf.rs +++ b/pywr-core/src/recorders/hdf.rs @@ -1,11 +1,10 @@ use super::{ - MetricSetState, OutputMetric, Recorder, RecorderFinalResult, RecorderFinaliseError, RecorderInternalState, - RecorderMeta, RecorderSaveError, RecorderSetupError, Timestep, downcast_internal_state, - downcast_internal_state_mut, + MetricSetState, OutputMetric, Recorder, RecorderBuilder, RecorderBuilderError, RecorderFinalResult, + RecorderFinaliseError, RecorderInternalState, RecorderMeta, RecorderSaveError, RecorderSetupError, Timestep, + downcast_internal_state, downcast_internal_state_mut, }; use crate::models::ModelDomain; -use crate::network::Network; -use crate::recorders::MetricSetIndex; +use crate::network::{MetricSetIndex, Network, ResolutionMaps}; use crate::scenario::{ScenarioDomain, ScenarioIndex}; use crate::state::State; use chrono::{Datelike, Timelike}; @@ -88,16 +87,6 @@ impl DateTime { } } -impl HDF5Recorder { - pub fn new>(name: &str, filename: P, metric_set_idx: MetricSetIndex) -> Self { - Self { - meta: RecorderMeta::new(name), - filename: filename.into(), - metric_set_idx, - } - } -} - impl Recorder for HDF5Recorder { fn meta(&self) -> &RecorderMeta { &self.meta @@ -387,3 +376,43 @@ fn write_scenarios_metadata(file: &hdf5_metno::File, domain: &ScenarioDomain) -> Ok(()) } + +#[derive(Clone, Debug)] +pub struct HDF5RecorderBuilder { + meta: RecorderMeta, + filename: PathBuf, + // TODO this could support saving multiple metric sets in different groups + metric_set: String, +} + +impl HDF5RecorderBuilder { + pub fn new>(name: &str, filename: P, metric_set: &str) -> Self { + Self { + meta: RecorderMeta::new(name), + filename: filename.into(), + metric_set: metric_set.into(), + } + } +} + +impl RecorderBuilder for HDF5RecorderBuilder { + fn name(&self) -> &str { + &self.meta.name + } + + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric_set_idx = resolution_maps.metric_sets.get(&self.metric_set).ok_or_else(|| { + RecorderBuilderError::MetricSetNotFound { + name: self.metric_set.clone(), + } + })?; + + let r = HDF5Recorder { + meta: self.meta, + filename: self.filename, + metric_set_idx: *metric_set_idx, + }; + + Ok(Box::new(r)) + } +} diff --git a/pywr-core/src/recorders/memory.rs b/pywr-core/src/recorders/memory.rs index c334b0e1..ca11776d 100644 --- a/pywr-core/src/recorders/memory.rs +++ b/pywr-core/src/recorders/memory.rs @@ -1,11 +1,11 @@ use crate::agg_funcs::{AggFuncError, AggFuncF64}; use crate::models::ModelDomain; -use crate::network::Network; +use crate::network::{MetricSetIndex, Network, ResolutionMaps}; use crate::recorders::aggregator::PeriodValue; use crate::recorders::{ - MetricSetIndex, MetricSetState, Recorder, RecorderAggregationError, RecorderDataFrameError, RecorderFinalResult, - RecorderFinaliseError, RecorderInternalState, RecorderMeta, RecorderSaveError, RecorderSetupError, - downcast_internal_state, downcast_internal_state_mut, + MetricSetState, Recorder, RecorderAggregationError, RecorderBuilder, RecorderBuilderError, RecorderDataFrameError, + RecorderFinalResult, RecorderFinaliseError, RecorderInternalState, RecorderMeta, RecorderSaveError, + RecorderSetupError, downcast_internal_state, downcast_internal_state_mut, }; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -303,17 +303,6 @@ pub struct MemoryRecorder { order: AggregationOrder, } -impl MemoryRecorder { - pub fn new(name: &str, metric_set_idx: MetricSetIndex, aggregation: Aggregation, order: AggregationOrder) -> Self { - Self { - meta: RecorderMeta::new(name), - metric_set_idx, - aggregation, - order, - } - } -} - impl Recorder for MemoryRecorder { fn meta(&self) -> &RecorderMeta { &self.meta @@ -399,6 +388,47 @@ impl Recorder for MemoryRecorder { } } +pub struct MemoryRecorderBuilder { + meta: RecorderMeta, + metric_set: String, + aggregation: Aggregation, + order: AggregationOrder, +} + +impl MemoryRecorderBuilder { + pub fn new(name: &str, metric_set: &str, aggregation: Aggregation, order: AggregationOrder) -> Self { + Self { + meta: RecorderMeta::new(name), + metric_set: metric_set.to_string(), + aggregation, + order, + } + } +} + +impl RecorderBuilder for MemoryRecorderBuilder { + fn name(&self) -> &str { + &self.meta.name + } + + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError> { + let metric_set_idx = resolution_maps.metric_sets.get(&self.metric_set).ok_or_else(|| { + RecorderBuilderError::MetricSetNotFound { + name: self.metric_set.clone(), + } + })?; + + let r = MemoryRecorder { + meta: self.meta, + metric_set_idx: *metric_set_idx, + aggregation: self.aggregation, + order: self.order, + }; + + Ok(Box::new(r)) + } +} + #[cfg(test)] mod tests { use super::{Aggregation, InternalState, MemoryRecorderResult}; diff --git a/pywr-core/src/recorders/metric_set.rs b/pywr-core/src/recorders/metric_set.rs index 54630108..8e70ea39 100644 --- a/pywr-core/src/recorders/metric_set.rs +++ b/pywr-core/src/recorders/metric_set.rs @@ -1,12 +1,9 @@ -use crate::metric::{MetricF64, MetricF64Error}; -use crate::network::Network; +use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; +use crate::network::{Network, ResolutionMaps}; use crate::recorders::aggregator::{Aggregator, AggregatorState, PeriodValue}; use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; -use std::fmt; -use std::fmt::{Display, Formatter}; -use std::ops::Deref; use thiserror::Error; /// A container for a [`MetricF64`] that retains additional information from the schema. @@ -25,16 +22,6 @@ pub struct OutputMetric { } impl OutputMetric { - pub fn new(name: &str, attribute: &str, ty: &str, sub_type: Option<&str>, metric: MetricF64) -> Self { - Self { - name: name.to_string(), - attribute: attribute.to_string(), - ty: ty.to_string(), - sub_type: sub_type.map(|s| s.to_string()), - metric, - } - } - pub fn get_value(&self, model: &Network, state: &State) -> Result { self.metric.get_value(model, state) } @@ -55,26 +42,37 @@ impl OutputMetric { } } -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] -pub struct MetricSetIndex(usize); - -impl MetricSetIndex { - pub fn new(idx: usize) -> Self { - Self(idx) - } +#[derive(Clone, PartialEq)] +pub struct UnresolvedOutputMetric { + name: String, + attribute: String, + // The originating type of the metric (e.g. node, parameter, etc.) + ty: String, + // The originating subtype of the metric (e.g. node type, parameter type, etc.) + sub_type: Option, + metric: UnresolvedMetricF64, } -impl Deref for MetricSetIndex { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 +impl UnresolvedOutputMetric { + pub fn new(name: &str, attribute: &str, ty: &str, sub_type: Option<&str>, metric: UnresolvedMetricF64) -> Self { + Self { + name: name.to_string(), + attribute: attribute.to_string(), + ty: ty.to_string(), + sub_type: sub_type.map(|s| s.to_string()), + metric, + } } -} -impl Display for MetricSetIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + pub fn resolve(self, resolution_maps: &ResolutionMaps) -> Result { + let metric = self.metric.resolve(resolution_maps)?; + Ok(OutputMetric { + name: self.name, + attribute: self.attribute, + ty: self.ty, + sub_type: self.sub_type, + metric, + }) } } @@ -107,14 +105,6 @@ pub struct MetricSet { } impl MetricSet { - pub fn new(name: &str, aggregator: Option, metrics: Vec) -> Self { - Self { - name: name.to_string(), - aggregator, - metrics, - } - } - /// The name of the [`MetricSet`]. pub fn name(&self) -> &str { &self.name @@ -209,3 +199,62 @@ impl MetricSet { } } } + +#[derive(Error, Debug)] +pub enum MetricSetBuilderError { + #[error("Could not resolve output f64 metric `{name}`: {source}")] + ResolveMetricF64Error { + name: String, + #[source] + source: MetricF64ResolutionError, + }, +} + +pub struct MetricSetBuilder { + name: String, + aggregator: Option, + metrics: Vec, +} + +impl MetricSetBuilder { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + aggregator: None, + metrics: Vec::new(), + } + } + pub fn name(&self) -> &str { + &self.name + } + + pub fn aggregator(&mut self, aggregator: Aggregator) -> &mut Self { + self.aggregator = Some(aggregator); + self + } + + pub fn metric(&mut self, metric: UnresolvedOutputMetric) -> &mut Self { + self.metrics.push(metric); + self + } + + pub fn build(self, resolution_maps: &ResolutionMaps) -> Result { + let metrics = self + .metrics + .into_iter() + .map(|m| { + let name = m.name.clone(); + m.resolve(resolution_maps) + .map_err(|source| MetricSetBuilderError::ResolveMetricF64Error { name, source }) + }) + .collect::, _>>()?; + + let ms = MetricSet { + name: self.name, + aggregator: self.aggregator, + metrics, + }; + + Ok(ms) + } +} diff --git a/pywr-core/src/recorders/mod.rs b/pywr-core/src/recorders/mod.rs index 4b692b24..45a2737a 100644 --- a/pywr-core/src/recorders/mod.rs +++ b/pywr-core/src/recorders/mod.rs @@ -12,7 +12,7 @@ use crate::metric::{ UnresolvedMetricF64, UnresolvedMetricU64, }; use crate::models::ModelDomain; -use crate::network::{Network, ResolutionMaps}; +use crate::network::{MetricSetIndex, Network, ResolutionMaps}; use crate::recorders::csv::CsvError; #[cfg(feature = "hdf5")] use crate::recorders::hdf::Hdf5Error; @@ -20,12 +20,15 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; pub use aggregator::{AggregationFrequency, Aggregator, PeriodValue}; -pub use csv::{CsvLongFmtOutput, CsvLongFmtRecord, CsvWideFmtOutput}; +pub use csv::{CsvLongFmtOutput, CsvLongFmtOutputBuilder, CsvLongFmtRecord, CsvWideFmtOutput, CsvWideFmtOutputBuilder}; use float_cmp::{ApproxEq, F64Margin, approx_eq}; #[cfg(feature = "hdf5")] -pub use hdf::HDF5Recorder; -pub use memory::{Aggregation, AggregationError, AggregationOrder, MemoryRecorder}; -pub use metric_set::{MetricSet, MetricSetIndex, MetricSetSaveError, MetricSetState, OutputMetric}; +pub use hdf::{HDF5Recorder, HDF5RecorderBuilder}; +pub use memory::{Aggregation, AggregationError, AggregationOrder, MemoryRecorder, MemoryRecorderBuilder}; +pub use metric_set::{ + MetricSet, MetricSetBuilder, MetricSetBuilderError, MetricSetSaveError, MetricSetState, OutputMetric, + UnresolvedOutputMetric, +}; use ndarray::Array2; use ndarray::prelude::*; use polars::prelude::PolarsError; @@ -227,6 +230,8 @@ pub enum RecorderBuilderError { #[source] source: MetricU64ResolutionError, }, + #[error("Metric set `{name}` not found.")] + MetricSetNotFound { name: String }, } pub trait RecorderBuilder { diff --git a/pywr-core/src/scenario.rs b/pywr-core/src/scenario.rs index f30a7e7f..a8fc9e1f 100644 --- a/pywr-core/src/scenario.rs +++ b/pywr-core/src/scenario.rs @@ -9,8 +9,6 @@ use thiserror::Error; pub enum ScenarioDomainBuilderError { #[error("Scenario group name `{0}` already exists")] DuplicateGroupName(String), - #[error("Scenario group name `{0}` not found")] - GroupNameNotFound(String), #[error("Cannot use both combinations and slices")] CombinationsAndSlices, #[error("No labels provided for for scenario group `{group}`.")] @@ -678,6 +676,13 @@ impl ScenarioIndex { } } +/// Errors that can occur from [`ScenarioDomain`]. +#[derive(Error, Debug)] +#[error("Scenario group name `{name}` not found")] +pub struct ScenarioGroupNotFound { + pub name: String, +} + /// The scenario domain for a model. #[derive(Debug, Clone)] pub struct ScenarioDomain { @@ -709,12 +714,12 @@ impl ScenarioDomain { /// /// # Errors /// - /// - [`ScenarioDomainBuilderError::GroupNameNotFound`] if the group name is not found in the domain. - pub fn group_index(&self, name: &str) -> Result { + /// - [`ScenarioGroupNotFound`] if the group name is not found in the domain. + pub fn group_index(&self, name: &str) -> Result { self.groups .iter() .position(|g| g.name == name) - .ok_or_else(|| ScenarioDomainBuilderError::GroupNameNotFound(name.to_string())) + .ok_or_else(|| ScenarioGroupNotFound { name: name.to_string() }) } /// Return the scenario groups for the domain. @@ -727,8 +732,8 @@ impl ScenarioDomain { /// /// # Errors /// - /// - [`ScenarioDomainBuilderError::GroupNameNotFound`] if the group name is not found - pub fn group_scenario_subset(&self, name: &str) -> Result, ScenarioDomainBuilderError> { + /// - [`ScenarioGroupNotFound`] if the group name is not found + pub fn group_scenario_subset(&self, name: &str) -> Result, ScenarioGroupNotFound> { let group_index = self.group_index(name)?; Ok(self.scenario_map[group_index].as_deref()) } @@ -737,8 +742,8 @@ impl ScenarioDomain { /// /// # Errors /// - /// - [`ScenarioDomainBuilderError::GroupNameNotFound`] if the group name is not found - pub fn group_size(&self, name: &str) -> Result { + /// - [`ScenarioGroupNotFound`] if the group name is not found + pub fn group_size(&self, name: &str) -> Result { let group_index = self.group_index(name)?; Ok(self.groups[group_index].size()) } diff --git a/pywr-core/src/test_utils.rs b/pywr-core/src/test_utils.rs index 923f1525..df2baeed 100644 --- a/pywr-core/src/test_utils.rs +++ b/pywr-core/src/test_utils.rs @@ -79,8 +79,8 @@ pub fn simple_network(builder: &mut NetworkBuilder, inflow_scenario: &str, num_i .cost(UnresolvedMetricF64::new_parameter_before("demand-cost")); builder.node(output_node); - builder.connect("input", None, "link", None); - builder.connect("link", None, "output", None); + builder.connect("input", "link"); + builder.connect("link", "output"); let inflow = Array::from_shape_fn((366, num_inflow_scenarios), |(i, j)| 1.0 + i as f64 + j as f64); let inflow = Array2ParameterBuilder::new("inflow".into(), inflow, inflow_scenario); @@ -133,7 +133,7 @@ pub fn simple_storage_network() -> NetworkBuilder { .cost(UnresolvedMetricF64::new_parameter_before("demand-cost")); builder.node(output_node); - builder.connect("reservoir", None, "output", None); + builder.connect("reservoir", "output"); // Apply demand to the model // TODO convenience function for adding a constant constraint. @@ -220,7 +220,7 @@ pub fn run_and_assert_parameter_u64( let rec = AssertionU64RecorderBuilder::new( "assert", - UnresolvedMetricU64::new_index_parameter_before(p_name), + UnresolvedMetricU64::new_parameter_before(p_name), expected_values, ); model_builder.network_builder().recorder(Box::new(rec)); @@ -579,8 +579,14 @@ fn make_simple_system( .cost((-500.0).into()); builder.node(output_node); - builder.connect("input", Some(suffix), "link", Some(suffix)); - builder.connect("link", Some(suffix), "output", Some(suffix)); + builder.connect( + UnresolvedNode::new("input", Some(suffix)), + UnresolvedNode::new("link", Some(suffix)), + ); + builder.connect( + UnresolvedNode::new("link", Some(suffix)), + UnresolvedNode::new("output", Some(suffix)), + ); let inflow_distr: Normal = Normal::new(9.0, 1.0).unwrap(); @@ -630,8 +636,14 @@ fn make_simple_connections( network_builder.node(node_builder); - network_builder.connect("link", Some(&format!("sys-{i:04}")), "transfer", Some(&sub_name)); - network_builder.connect("transfer", Some(&sub_name), "link", Some(&format!("sys-{j:04}"))); + network_builder.connect( + UnresolvedNode::new("link", Some(&format!("sys-{i:04}"))), + UnresolvedNode::new("transfer", Some(&sub_name)), + ); + network_builder.connect( + UnresolvedNode::new("transfer", Some(&sub_name)), + UnresolvedNode::new("link", Some(&format!("sys-{j:04}"))), + ); connections_added += 1; } diff --git a/pywr-core/src/virtual_storage.rs b/pywr-core/src/virtual_storage.rs index 3cbf0370..27606ca6 100644 --- a/pywr-core/src/virtual_storage.rs +++ b/pywr-core/src/virtual_storage.rs @@ -579,8 +579,11 @@ mod tests { output_node0.sub_name("0").max_flow(10.0.into()).cost((-10.0).into()); network_builder.node(output_node0); - network_builder.connect("input", None, "link", Some("0")); - network_builder.connect("link", Some("0"), "output", Some("0")); + network_builder.connect("input", UnresolvedNode::new("link", Some("0"))); + network_builder.connect( + UnresolvedNode::new("link", Some("0")), + UnresolvedNode::new("output", Some("0")), + ); let mut link_node1 = NodeBuilder::new("link", NodeType::Link); link_node1.sub_name("1"); @@ -590,8 +593,11 @@ mod tests { output_node1.sub_name("1").max_flow(10.0.into()).cost((-10.0).into()); network_builder.node(output_node1); - network_builder.connect("input", None, "link", Some("1")); - network_builder.connect("link", Some("1"), "output", Some("1")); + network_builder.connect("input", UnresolvedNode::new("link", Some("1"))); + network_builder.connect( + UnresolvedNode::new("link", Some("1")), + UnresolvedNode::new("output", Some("1")), + ); // Virtual storage with contributions from link-node0 than link-node1 let mut vs_builder = VirtualStorageNodeBuilder::new( diff --git a/pywr-schema/src/edge.rs b/pywr-schema/src/edge.rs index 39f6cdb3..2ed14313 100644 --- a/pywr-schema/src/edge.rs +++ b/pywr-schema/src/edge.rs @@ -5,7 +5,7 @@ use crate::SchemaError; use crate::network::LoadArgs; use crate::nodes::NodeSlot; #[cfg(feature = "core")] -use pywr_core::{edge::EdgeIndex, metric::MetricF64, node::NodeIndex}; +use pywr_core::{metric::UnresolvedMetricF64, network::UnresolvedEdge, node::UnresolvedNode}; use pywr_schema_macros::skip_serializing_none; use schemars::JsonSchema; use std::fmt::{Display, Formatter}; @@ -67,11 +67,10 @@ impl Edge { /// Returns an iterator of the pairs (from, to) of `NodeIndex` that represent this /// edge when added to a model. In general this can be several nodes because some nodes /// have multiple internal nodes when connected from or to. - fn iter_node_index_pairs( + fn iter_node_connection_pairs( &self, - network: &pywr_core::network::Network, args: &LoadArgs, - ) -> Result + use<>, SchemaError> { + ) -> Result + use<>, SchemaError> { let from_node = args.schema .get_node_by_name(self.from_node.as_str()) @@ -87,70 +86,41 @@ impl Edge { })?; // Collect the node indices at each end of the edge - let from_node_indices: Vec = from_node - .output_connectors(self.from_slot.as_ref())? - .into_iter() - .map(|(name, sub_name)| { - network - .get_node_index_by_name(name, sub_name.as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: name.to_string(), - sub_name, - }) - }) - .collect::>()?; - - let to_node_indices: Vec = to_node - .input_connectors(self.to_slot.as_ref())? - .into_iter() - .map(|(name, sub_name)| { - network - .get_node_index_by_name(name, sub_name.as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: name.to_string(), - sub_name, - }) - }) - .collect::>()?; - - let pairs: Vec<_> = from_node_indices + let from_node_nodes = from_node.output_connectors(self.from_slot.as_ref())?; + let to_node_nodes = to_node.input_connectors(self.to_slot.as_ref())?; + + let pairs: Vec<_> = from_node_nodes + .clone() .into_iter() - .flat_map(|from_node_index| std::iter::repeat(from_node_index).zip(to_node_indices.iter().copied())) + .flat_map(|from| std::iter::repeat(from).zip(to_node_nodes.iter().cloned())) .collect(); Ok(pairs.into_iter()) } /// Add the edge to the network - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { // Connect each "from" connector to each "to" connector - for (from_node_index, to_node_index) in self.iter_node_index_pairs(network, args)? { - network.connect_nodes(from_node_index, to_node_index)?; + for (from_node, to_node) in self.iter_node_connection_pairs(args)? { + network.connect(from_node, to_node); } Ok(()) } /// Create a metric that will return this edge's total flow in the model. - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - args: &LoadArgs, - ) -> Result { - let indices: Vec = self - .iter_node_index_pairs(network, args)? - .map(|(from_node_index, to_node_index)| { - network - .get_edge_index(from_node_index, to_node_index) - .ok_or_else(|| SchemaError::EdgeNotFound { - from_node: self.from_node.clone(), - to_node: self.to_node.clone(), - }) - }) - .collect::>()?; - - let metric = MetricF64::MultiEdgeFlow { - indices, + pub fn create_metric(&self, args: &LoadArgs) -> Result { + let edges = self + .iter_node_connection_pairs(args)? + .map(|(from_node, to_node)| UnresolvedEdge::new(from_node, to_node)) + .collect::>(); + + let metric = UnresolvedMetricF64::MultiEdgeFlow { + edges, name: self.to_string(), }; diff --git a/pywr-schema/src/error.rs b/pywr-schema/src/error.rs index ea37ad47..9361a126 100644 --- a/pywr-schema/src/error.rs +++ b/pywr-schema/src/error.rs @@ -54,9 +54,6 @@ pub enum SchemaError { #[error("Pywr model domain error: {0}")] #[cfg(feature = "core")] CoreModelDomainError(#[from] pywr_core::models::ModelDomainError), - #[error("Multi-network model error: {0}")] - #[cfg(feature = "core")] - CoreMultiNetworkModelError(#[from] pywr_core::models::MultiNetworkModelError), #[error("Metric F64 error: {0}")] #[cfg(feature = "core")] CoreMetricF64Error(#[from] pywr_core::metric::MetricF64Error), @@ -121,15 +118,6 @@ pub enum SchemaError { NodeConnectionSlotRequired { msg: String }, #[error("Checksum error: {0}")] ChecksumError(#[from] ChecksumError), - #[error( - "Number of values ({values}) for parameter '{name}' does not match the size ({scenarios}) of the specified scenario group '{group}'." - )] - ScenarioValuesLengthMismatch { - values: usize, - name: String, - scenarios: usize, - group: String, - }, } #[cfg(all(feature = "core", feature = "pyo3"))] diff --git a/pywr-schema/src/metric.rs b/pywr-schema/src/metric.rs index ccda07d4..4793e1c1 100644 --- a/pywr-schema/src/metric.rs +++ b/pywr-schema/src/metric.rs @@ -22,10 +22,9 @@ use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; use pyo3::{PyResult, exceptions::PyRuntimeError, pyclass, pymethods}; #[cfg(feature = "core")] use pywr_core::{ - metric::{MetricF64, MetricU64}, - models::MultiNetworkTransferIndex, + metric::{UnresolvedMetricF64, UnresolvedMetricU64}, parameters::ParameterName, - recorders::OutputMetric, + recorders::UnresolvedOutputMetric, }; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::ParameterValue as ParameterValueV1; @@ -96,15 +95,15 @@ impl From for Metric { impl Metric { pub fn load( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { + ) -> Result { match self { Self::Node(node_ref) => node_ref.load_f64(network, args), - Self::VirtualNode(node_ref) => node_ref.load_f64(network, args), + Self::VirtualNode(node_ref) => node_ref.load_f64(args), // Global parameter with no parent - Self::Parameter(parameter_ref) => parameter_ref.load_f64(network, None), + Self::Parameter(parameter_ref) => Ok(parameter_ref.load_f64(None)), // Local parameter loaded from parent's namespace Self::LocalParameter(parameter_ref) => { if parent.is_none() { @@ -113,7 +112,7 @@ impl Metric { )); } - parameter_ref.load_f64(network, parent) + Ok(parameter_ref.load_f64(parent)) } Self::Literal { value } => Ok((*value).into()), Self::Table(table_ref) => { @@ -127,10 +126,10 @@ impl Metric { Ok(value.into()) } Self::Timeseries(ts_ref) => { - let param_idx = match &ts_ref.columns { + let param_name = match &ts_ref.columns { Some(TimeseriesColumns::Scenario { name }) => { args.timeseries - .load_df_f64(network, ts_ref.name.as_ref(), args.domain, name.as_str())? + .load_df_f64(network, ts_ref.name.as_ref(), name.as_str())? } Some(TimeseriesColumns::Column { name }) => { args.timeseries @@ -138,16 +137,13 @@ impl Metric { } None => args.timeseries.load_single_column_f64(network, ts_ref.name.as_ref())?, }; - Ok(param_idx.into_metric_f64_before()) + Ok(UnresolvedMetricF64::new_parameter_before(param_name)) } Self::InterNetworkTransfer { name } => { // Find the matching inter model transfer - match args.inter_network_transfers.iter().position(|t| &t.name == name) { - Some(idx) => Ok(MetricF64::InterNetworkTransfer(MultiNetworkTransferIndex(idx))), - None => Err(SchemaError::InterNetworkTransferNotFound(name.to_string())), - } + Ok(UnresolvedMetricF64::InterNetworkTransfer(name.clone())) } - Self::Edge(edge_ref) => edge_ref.load(network, args), + Self::Edge(edge_ref) => edge_ref.load(args), } } @@ -205,16 +201,16 @@ impl Metric { pub fn load_as_output( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { + ) -> Result { let metric = self.load(network, args, parent)?; let ty = self.to_string(); let sub_type = self.sub_type(args)?; - Ok(OutputMetric::new( + Ok(UnresolvedOutputMetric::new( self.name()?.as_str(), &self.attribute(args)?, &ty, @@ -303,9 +299,9 @@ impl NodeAttrReference { #[cfg(feature = "core")] pub fn load_f64( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, - ) -> Result { + ) -> Result { // This is the associated node in the schema let node = args .schema @@ -319,11 +315,7 @@ impl NodeAttrReference { /// Load a node reference into a [`MetricUsize`]. #[cfg(feature = "core")] - pub fn load_u64( - &self, - _network: &mut pywr_core::network::Network, - args: &LoadArgs, - ) -> Result { + pub fn load_u64(&self, args: &LoadArgs) -> Result { // This is the associated node in the schema let _node = args .schema @@ -393,11 +385,7 @@ impl VirtualNodeAttrReference { /// Load a node reference into a [`MetricF64`]. #[cfg(feature = "core")] - pub fn load_f64( - &self, - network: &mut pywr_core::network::Network, - args: &LoadArgs, - ) -> Result { + pub fn load_f64(&self, args: &LoadArgs) -> Result { // This is the associated node in the schema let node = args.schema @@ -406,7 +394,7 @@ impl VirtualNodeAttrReference { name: self.name.clone(), })?; - node.create_metric(network, self.attribute) + node.create_metric(self.attribute) } /// Load a node reference into a [`MetricUsize`]. @@ -415,7 +403,7 @@ impl VirtualNodeAttrReference { &self, _network: &mut pywr_core::network::Network, args: &LoadArgs, - ) -> Result { + ) -> Result { // This is the associated node in the schema let _node = args.schema @@ -533,11 +521,7 @@ impl ParameterReference { /// from the `network`. If `parent` is the optional parameter name space from which to load /// the parameter. #[cfg(feature = "core")] - pub fn load_f64( - &self, - network: &mut pywr_core::network::Network, - parent: Option<&str>, - ) -> Result { + pub fn load_f64(&self, parent: Option<&str>) -> UnresolvedMetricF64 { let name = ParameterName::new(&self.name, parent); // Determine the return value to use let return_value = self.return_value.unwrap_or_default().into(); @@ -545,27 +529,13 @@ impl ParameterReference { match &self.key { Some(key) => { // Key given; this should be a multi-valued parameter - let idx = network.get_multi_valued_parameter_index_by_name(&name).ok_or_else(|| { - SchemaError::CoreParameterNotFound { - name: self.name.to_string(), - key: Some(key.clone()), - } - })?; - - Ok(idx.into_metric_f64(key, return_value)) - } - None => { - if let Some(idx) = network.get_parameter_index_by_name(&name) { - Ok(idx.into_metric_f64(return_value)) - } else if let Some(idx) = network.get_index_parameter_index_by_name(&name) { - Ok(idx.into_metric_f64(return_value)) - } else { - Err(SchemaError::CoreParameterNotFound { - name: self.name.to_string(), - key: None, - }) + UnresolvedMetricF64::MultiParameterValue { + name, + key: key.to_string(), + return_value, } } + None => UnresolvedMetricF64::ParameterValue { name, return_value }, } } @@ -573,11 +543,7 @@ impl ParameterReference { /// from the `network`. If `parent` is the optional parameter name space from which to load /// the parameter. #[cfg(feature = "core")] - pub fn load_u64( - &self, - network: &mut pywr_core::network::Network, - parent: Option<&str>, - ) -> Result { + pub fn load_u64(&self, parent: Option<&str>) -> UnresolvedMetricU64 { let name = ParameterName::new(&self.name, parent); // Determine the return value to use let return_value = self.return_value.unwrap_or_default().into(); @@ -585,27 +551,13 @@ impl ParameterReference { match &self.key { Some(key) => { // Key given; this should be a multi-valued parameter - let idx = network.get_multi_valued_parameter_index_by_name(&name).ok_or_else(|| { - SchemaError::CoreParameterNotFound { - name: self.name.to_string(), - key: Some(key.clone()), - } - })?; - Ok(idx.into_metric_u64(key, return_value)) - } - None => { - if let Some(idx) = network.get_index_parameter_index_by_name(&name) { - Ok(idx.into_metric_u64(return_value)) - } else if network.get_parameter_index_by_name(&name).is_some() { - // Inform the user we found the parameter, but it was the wrong type - Err(SchemaError::IndexParameterExpected(self.name.to_string())) - } else { - Err(SchemaError::CoreParameterNotFound { - name: self.name.to_string(), - key: None, - }) + UnresolvedMetricU64::MultiParameterValue { + name, + key: key.to_string(), + return_value, } } + None => UnresolvedMetricU64::ParameterValue { name, return_value }, } } #[cfg(feature = "core")] @@ -640,9 +592,9 @@ pub struct EdgeReference { #[cfg(feature = "core")] impl EdgeReference { - pub fn load(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result { + pub fn load(&self, args: &LoadArgs) -> Result { // This is the associated node in the schema - self.edge.create_metric(network, args) + self.edge.create_metric(args) } } @@ -685,14 +637,14 @@ impl From for IndexMetric { impl IndexMetric { pub fn load( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { + ) -> Result { match self { - Self::Node(node_ref) => node_ref.load_u64(network, args), + Self::Node(node_ref) => node_ref.load_u64(args), // Global parameter with no parent - Self::Parameter(parameter_ref) => parameter_ref.load_u64(network, None), + Self::Parameter(parameter_ref) => Ok(parameter_ref.load_u64(None)), // Local parameter loaded from parent's namespace Self::LocalParameter(parameter_ref) => { if parent.is_none() { @@ -701,7 +653,7 @@ impl IndexMetric { )); } - parameter_ref.load_u64(network, parent) + Ok(parameter_ref.load_u64(parent)) } Self::Constant { value } => Ok((*value).into()), Self::Table(table_ref) => { @@ -715,10 +667,10 @@ impl IndexMetric { Ok(value.into()) } Self::Timeseries(ts_ref) => { - let param_idx = match &ts_ref.columns { + let param_name = match &ts_ref.columns { Some(TimeseriesColumns::Scenario { name }) => { args.timeseries - .load_df_usize(network, ts_ref.name.as_ref(), args.domain, name.as_str())? + .load_df_usize(network, ts_ref.name.as_ref(), name.as_str())? } Some(TimeseriesColumns::Column { name }) => { args.timeseries @@ -728,14 +680,11 @@ impl IndexMetric { .timeseries .load_single_column_usize(network, ts_ref.name.as_ref())?, }; - Ok(param_idx.into_metric_u64_before()) + Ok(UnresolvedMetricU64::new_parameter_before(param_name)) } Self::InterNetworkTransfer { name } => { // Find the matching inter model transfer - match args.inter_network_transfers.iter().position(|t| &t.name == name) { - Some(idx) => Ok(MetricU64::InterNetworkTransfer(MultiNetworkTransferIndex(idx))), - None => Err(SchemaError::InterNetworkTransferNotFound(name.to_string())), - } + Ok(UnresolvedMetricU64::InterNetworkTransfer(name.clone())) } } } diff --git a/pywr-schema/src/metric_sets/mod.rs b/pywr-schema/src/metric_sets/mod.rs index f0561229..aed10227 100644 --- a/pywr-schema/src/metric_sets/mod.rs +++ b/pywr-schema/src/metric_sets/mod.rs @@ -8,6 +8,8 @@ use crate::metric::{EdgeReference, VirtualNodeAttrReference}; use crate::network::LoadArgs; #[cfg(feature = "core")] use crate::parameters::{Parameter, PythonReturnType}; +#[cfg(feature = "core")] +use pywr_core::recorders::UnresolvedOutputMetric; use pywr_schema_macros::skip_serializing_none; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -158,9 +160,11 @@ pub struct MetricSet { impl MetricSet { #[cfg(feature = "core")] - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { - use pywr_core::recorders::OutputMetric; - + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { // Create metrics from filters and load them as output metrics let metrics_from_filters = self .filters @@ -171,7 +175,7 @@ impl MetricSet { let output_metrics = match &self.metrics { Some(metrics) => { - let mut output_metrics: Vec = metrics + let mut output_metrics: Vec = metrics .iter() .map(|m| m.load_as_output(network, args, None)) .collect::>()?; @@ -191,10 +195,17 @@ impl MetricSet { return Err(SchemaError::EmptyMetricSet(self.name.clone())); } - let aggregator = self.aggregator.clone().map(|a| a.load(args.data_path)).transpose()?; + let mut metric_set = pywr_core::recorders::MetricSetBuilder::new(&self.name); + + if let Some(aggregator) = &self.aggregator { + metric_set.aggregator(aggregator.load(args.data_path)?); + } + + for m in output_metrics { + metric_set.metric(m); + } - let metric_set = pywr_core::recorders::MetricSet::new(&self.name, aggregator, output_metrics); - let _ = network.add_metric_set(metric_set)?; + network.metric_set(metric_set); Ok(()) } diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index ce8a47f9..47de50f9 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -19,11 +19,16 @@ use pyo3::Python; use pyo3::{Bound, PyErr, PyResult, exceptions::PyRuntimeError, pyclass, pymethods, types::PyType}; #[cfg(feature = "core")] use pywr_core::{ - models::{Model, ModelBuilder, ModelDomain, MultiModelBuilder, MultiModelBuilderError, MultiNetworkModel}, + models::{ + Model, ModelBuilder, ModelDomainBuilder, MultiNetworkEntryBuilder, MultiNetworkModelBuilder, + MultiNetworkModelBuilderError, MultiNetworkTransferBuilder, + }, timestep::TimestepDuration, }; use pywr_schema_macros::skip_serializing_none; use schemars::JsonSchema; +#[cfg(feature = "core")] +use std::collections::HashMap; use std::num::NonZeroU64; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -117,13 +122,13 @@ impl From for Date { } #[derive(serde::Deserialize, serde::Serialize, Clone, Debug, JsonSchema)] -pub struct Timestepper { +pub struct TimeDomain { pub start: Date, pub end: Date, pub timestep: Timestep, } -impl Default for Timestepper { +impl Default for TimeDomain { fn default() -> Self { Self { start: Date::Date(NaiveDate::from_ymd_opt(2000, 1, 1).expect("Invalid date")), @@ -133,7 +138,7 @@ impl Default for Timestepper { } } -impl From for Timestepper { +impl From for TimeDomain { fn from(v1: pywr_v1_schema::model::Timestepper) -> Self { Self { start: v1.start.into(), @@ -144,8 +149,8 @@ impl From for Timestepper { } #[cfg(feature = "core")] -impl From for pywr_core::timestep::TimeDomainBuilder { - fn from(ts: Timestepper) -> Self { +impl From for pywr_core::timestep::TimeDomainBuilder { + fn from(ts: TimeDomain) -> Self { let timestep = match ts.timestep { Timestep::Hours { hours } => TimestepDuration::Hours(hours), Timestep::Days { days } => TimestepDuration::Days(days), @@ -461,7 +466,7 @@ impl From for PyErr { #[cfg_attr(feature = "pyo3", pyclass)] pub struct ModelSchema { pub metadata: Metadata, - pub timestepper: Timestepper, + pub time: TimeDomain, pub scenarios: Option, pub network: NetworkSchema, } @@ -501,7 +506,7 @@ impl ModelSchema { description: None, minimum_version: None, }, - timestepper: Timestepper { + time: TimeDomain { start: *start, end: *end, timestep: Timestep::default(), @@ -519,31 +524,37 @@ impl ModelSchema { Ok(serde_json::from_str(data.as_str())?) } + /// Create a [`pywr_core::models::ModelBuilder`] from the schema. #[cfg(feature = "core")] - pub fn build_model( + pub fn create_model_builder( &self, data_path: Option<&Path>, output_path: Option<&Path>, - ) -> Result { - let timestepper = self.timestepper.clone().into(); + ) -> Result { + let time_domain_builder = self.time.clone().into(); let scenario_builder = match &self.scenarios { Some(scenarios) => scenarios.clone().try_into()?, None => pywr_core::scenario::ScenarioDomainBuilder::default(), }; - let domain = ModelDomain::try_from(timestepper, scenario_builder)?; + let mut domain_builder = ModelDomainBuilder::new(time_domain_builder); + domain_builder.scenario(scenario_builder); + + // TODO handle this error + let domain = domain_builder.clone().build().unwrap(); + + let mut network_builder = pywr_core::network::NetworkBuilder::default(); - let (network, _tables, _ts) = self - .network - .build_network(&domain, data_path, output_path, &[]) + self.network + .add_to_network(&mut network_builder, &domain, data_path, output_path, &[]) .map_err(|source| ModelSchemaBuildError::NetworkBuildError { source: Box::new(source), })?; - let model = Model::new(domain, network); + let model_builder = ModelBuilder::new(domain_builder, network_builder); - Ok(model) + Ok(model_builder) } /// Convert a v1 model to a v2 model. @@ -557,7 +568,7 @@ impl ModelSchema { let mut errors = Vec::new(); let metadata = v1.metadata.into(); - let timestepper = v1.timestepper.into(); + let time = v1.timestepper.into(); let scenarios = match v1.scenarios.map(|s| s.try_into()) { Some(Ok(scenarios)) => Some(scenarios), Some(Err(err)) => { @@ -573,7 +584,7 @@ impl ModelSchema { ( Self { metadata, - timestepper, + time, scenarios, network, }, @@ -627,7 +638,8 @@ impl ModelSchema { #[cfg(feature = "core")] #[pyo3(name="build", signature = (data_path=None, output_path=None))] fn build_py(&mut self, data_path: Option, output_path: Option) -> PyResult { - let model = self.build_model(data_path.as_deref(), output_path.as_deref())?; + let builder = self.create_model_builder(data_path.as_deref(), output_path.as_deref())?; + let model = builder.build()?; Ok(model) } } @@ -673,11 +685,10 @@ pub enum MultiNetworkModelSchemaBuildError { #[source] source: Box, }, - #[error("Failed to add network `{name}` to the model: {source}")] - AddNetworkError { - name: String, + #[error("Failed to build the model: {source}")] + ModelBuildError { #[source] - source: MultiNetworkModelError, + source: Box, }, } @@ -770,7 +781,7 @@ impl From for PyErr { #[cfg_attr(feature = "pyo3", pyclass)] pub struct MultiNetworkModelSchema { pub metadata: Metadata, - pub timestepper: Timestepper, + pub time: TimeDomain, pub scenarios: Option, pub networks: Vec, } @@ -791,7 +802,7 @@ impl MultiNetworkModelSchema { description: None, minimum_version: None, }, - timestepper: Timestepper { + time: TimeDomain { start: *start, end: *end, timestep: Timestep::default(), @@ -809,30 +820,35 @@ impl MultiNetworkModelSchema { } #[cfg(feature = "core")] - pub fn build_model( + pub fn create_model_builder( &self, data_path: Option<&Path>, output_path: Option<&Path>, - ) -> Result { - let timestepper = self.timestepper.clone().into(); + ) -> Result { + let time_builder = self.time.clone().into(); let scenario_builder = match &self.scenarios { Some(scenarios) => scenarios.clone().try_into()?, None => pywr_core::scenario::ScenarioDomainBuilder::default(), }; - let domain = ModelDomain::try_from(timestepper, scenario_builder)?; - let mut networks = Vec::with_capacity(self.networks.len()); - let mut inter_network_transfers = Vec::new(); + let mut domain_builder = ModelDomainBuilder::new(time_builder); + domain_builder.scenario(scenario_builder); + let domain = domain_builder.clone().build().unwrap(); + + let mut network_entry_builders = Vec::with_capacity(self.networks.len()); + let mut network_builder_map = HashMap::with_capacity(self.networks.len()); let mut schemas: Vec<(NetworkSchema, LoadedTableCollection, LoadedTimeseriesCollection)> = Vec::with_capacity(self.networks.len()); // First load all the networks // These will contain any parameters that are referenced by the inter-model transfers // Because of potential circular references, we need to load all the networks first. - for network_entry in &self.networks { + for (i, network_entry) in self.networks.iter().enumerate() { // Load the network itself - let (network, schema, tables, timeseries) = match &network_entry.network { + let mut network_builder = pywr_core::network::NetworkBuilder::default(); + + let (schema, tables, timeseries) = match &network_entry.network { NetworkSchemaRef::Path(path) => { let pth = if let Some(dp) = data_path { if path.is_relative() { @@ -847,50 +863,58 @@ impl MultiNetworkModelSchema { let network_schema = NetworkSchema::from_path(&pth) .map_err(|source| MultiNetworkModelSchemaBuildError::NetworkReadError { path: pth, source })?; - let (net, tables, timeseries) = network_schema - .build_network(&domain, data_path, output_path, &network_entry.transfers) + let (tables, timeseries) = network_schema + .add_to_network( + &mut network_builder, + &domain, + data_path, + output_path, + &network_entry.transfers, + ) .map_err(|source| MultiNetworkModelSchemaBuildError::NetworkBuildError { name: network_entry.name.clone(), source: Box::new(source), })?; - (net, network_schema, tables, timeseries) + (network_schema, tables, timeseries) } NetworkSchemaRef::Inline(network_schema) => { - let (net, tables, timeseries) = network_schema - .build_network(&domain, data_path, output_path, &network_entry.transfers) + let (tables, timeseries) = network_schema + .add_to_network( + &mut network_builder, + &domain, + data_path, + output_path, + &network_entry.transfers, + ) .map_err(|source| MultiNetworkModelSchemaBuildError::NetworkBuildError { name: network_entry.name.clone(), source: Box::new(source), })?; - (net, network_schema.clone(), tables, timeseries) + (network_schema.clone(), tables, timeseries) } }; schemas.push((schema, tables, timeseries)); - networks.push((network_entry.name.clone(), network)); + + network_entry_builders.push(network_builder); + network_builder_map.insert(network_entry.name.clone(), i); } - // Now load the inter-model transfers - for (to_network_idx, network_entry) in self.networks.iter().enumerate() { + // Now load the inter-model transfer builders. + let mut transfer_builders = Vec::with_capacity(self.networks.len()); + for network_entry in &self.networks { + let mut t_builders = Vec::with_capacity(network_entry.transfers.len()); for transfer in &network_entry.transfers { // Load the metric from the "from" network - - let (from_network_idx, from_network) = networks - .iter_mut() - .enumerate() - .find_map(|(idx, (name, net))| { - if name.as_str() == transfer.from_network.as_str() { - Some((idx, net)) - } else { - None - } - }) - .ok_or_else(|| MultiNetworkModelSchemaBuildError::AddTransferError { + let from_network_idx = *network_builder_map.get(&transfer.from_network).ok_or_else(|| { + MultiNetworkModelSchemaBuildError::AddTransferError { name: transfer.name.clone(), source: Box::new(SchemaError::NetworkNotFound(transfer.from_network.clone())), - })?; + } + })?; + let from_network = &mut network_entry_builders[from_network_idx]; // The transfer metric will fail to load if it is defined as an inter-model transfer itself. let (from_schema, from_tables, from_timeseries) = &schemas[from_network_idx]; @@ -911,24 +935,35 @@ impl MultiNetworkModelSchema { } })?; - inter_network_transfers.push((from_network_idx, from_metric, to_network_idx, transfer.initial_value)); + let mut transfer_builder = + MultiNetworkTransferBuilder::new(&transfer.name, &transfer.from_network, from_metric); + if let Some(iv) = transfer.initial_value { + transfer_builder.initial_value(iv); + } + + t_builders.push(transfer_builder); } + + transfer_builders.push(t_builders); } // Now construct the model from the loaded components - let mut model = MultiNetworkModel::new(domain); + let mut model_builder = MultiNetworkModelBuilder::new(domain_builder); - for (name, network) in networks { - model - .add_network(&name, network) - .map_err(|source| MultiNetworkModelSchemaBuildError::AddNetworkError { name, source })?; - } + for (network_entry, (network_builder, t_builders)) in self + .networks + .iter() + .zip(network_entry_builders.into_iter().zip(transfer_builders)) + { + let mut entry_builder = MultiNetworkEntryBuilder::new(&network_entry.name, network_builder); + for t in t_builders { + entry_builder.transfer(t); + } - for (from_network_idx, from_metric, to_network_idx, initial_value) in inter_network_transfers { - model.add_inter_network_transfer(from_network_idx, from_metric, to_network_idx, initial_value); + model_builder.network(entry_builder); } - Ok(model) + Ok(model_builder) } } @@ -972,7 +1007,8 @@ impl MultiNetworkModelSchema { data_path: Option, output_path: Option, ) -> PyResult { - let model = self.build_model(data_path.as_deref(), output_path.as_deref())?; + let builder = self.create_model_builder(data_path.as_deref(), output_path.as_deref())?; + let model = builder.build()?; Ok(model) } } @@ -980,7 +1016,7 @@ impl MultiNetworkModelSchema { #[cfg(test)] mod tests { use super::{ModelSchema, ScenarioDomain}; - use crate::model::Timestepper; + use crate::model::TimeDomain; use crate::visit::VisitPaths; use std::fs; use std::fs::read_to_string; @@ -1012,7 +1048,7 @@ mod tests { } "#; - let timestep: Timestepper = serde_json::from_str(timestepper_str).unwrap(); + let timestep: TimeDomain = serde_json::from_str(timestepper_str).unwrap(); match timestep.start { super::Date::Date(date) => { @@ -1042,7 +1078,7 @@ mod tests { } "#; - let timestep: Timestepper = serde_json::from_str(timestepper_str).unwrap(); + let timestep: TimeDomain = serde_json::from_str(timestepper_str).unwrap(); match timestep.start { super::Date::DateTime(date_time) => { @@ -1095,7 +1131,7 @@ mod tests { // Expect this to file as the path has been updated to a missing file. #[cfg(feature = "core")] - if schema.build_model(model_fn.parent(), None).is_ok() { + if schema.create_model_builder(model_fn.parent(), None).is_ok() { let str = serde_json::to_string_pretty(&schema).unwrap(); panic!("Expected an error due to missing file: {str}"); } @@ -1126,9 +1162,9 @@ mod core_tests { use crate::metric::{Metric, ParameterReference}; use crate::parameters::{AggregatedParameter, ConstantParameter, Parameter, ParameterMeta}; use ndarray::{Array1, Array2, Axis}; - use pywr_core::{ - metric::MetricF64, recorders::AssertionF64Recorder, solvers::ClpSolver, test_utils::run_all_solvers, - }; + use pywr_core::metric::UnresolvedMetricF64; + use pywr_core::recorders::AssertionF64RecorderBuilder; + use pywr_core::{solvers::ClpSolver, test_utils::run_all_solvers}; use std::fs::read_to_string; use std::path::PathBuf; @@ -1140,25 +1176,24 @@ mod core_tests { fn test_simple1_run() { let data = model_str(); let schema: ModelSchema = serde_json::from_str(&data).unwrap(); - let mut model = schema.build_model(None, None).unwrap(); + let mut model_builder = schema.create_model_builder(None, None).unwrap(); - let network = model.network_mut(); - assert_eq!(network.nodes().len(), 3); - assert_eq!(network.edges().len(), 2); - - let demand1_idx = network.get_node_index_by_name("demand1", None).unwrap(); + let network_builder = model_builder.network_builder(); let expected_values: Array1 = [10.0; 365].to_vec().into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - let rec = AssertionF64Recorder::new( + let rec = AssertionF64RecorderBuilder::new( "assert-demand1", - MetricF64::NodeInFlow(demand1_idx), + UnresolvedMetricF64::NodeInFlow("demand1".into()), expected_values, - None, - None, ); - network.add_recorder(Box::new(rec)).unwrap(); + network_builder.recorder(Box::new(rec)); + + let model = model_builder.build().unwrap(); + let network = model.network(); + assert_eq!(network.nodes().len(), 3); + assert_eq!(network.edges().len(), 2); // Test all solvers run_all_solvers(&model, &[], &[], &[]); @@ -1226,7 +1261,8 @@ mod core_tests { } // TODO this could assert a specific type of error - assert!(schema.build_model(None, None).is_err()); + let builder = schema.create_model_builder(None, None).unwrap(); + assert!(builder.build().is_err()); } /// Test that a model loads if the aggregated parameter is defined before its dependencies. @@ -1278,7 +1314,7 @@ mod core_tests { ]); } // TODO this could assert a specific type of error - let _ = schema.build_model(None, None).unwrap(); + let _ = schema.create_model_builder(None, None).unwrap(); } /// Test the multi1 model @@ -1288,45 +1324,41 @@ mod core_tests { model_fn.push("tests/multi1/model.json"); let schema = MultiNetworkModelSchema::from_path(model_fn.as_path()).unwrap(); - let mut model = schema.build_model(model_fn.parent(), None).unwrap(); + let mut builder = schema.create_model_builder(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 network_1 = builder + .entry_builder("network1") + .expect("network 1 not found") + .network_builder(); let expected_values: Array1 = [10.0; 365].to_vec().into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - let rec = AssertionF64Recorder::new( + let rec = AssertionF64RecorderBuilder::new( "assert-demand1", - MetricF64::NodeInFlow(demand1_idx), + UnresolvedMetricF64::NodeInFlow("demand1".into()), expected_values, - None, - None, ); - network_1.add_recorder(Box::new(rec)).unwrap(); + network_1.recorder(Box::new(rec)); // 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 network_2 = builder + .entry_builder("network2") + .expect("network 2 not found") + .network_builder(); let expected_values: Array1 = [10.0; 365].to_vec().into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - let rec = AssertionF64Recorder::new( + let rec = AssertionF64RecorderBuilder::new( "assert-demand2", - MetricF64::NodeInFlow(demand1_idx), + UnresolvedMetricF64::NodeInFlow("demand2".into()), expected_values, - None, - None, ); - network_2.add_recorder(Box::new(rec)).unwrap(); + network_2.recorder(Box::new(rec)); + + let model = builder.build().unwrap(); model.run::(&Default::default()).unwrap(); } @@ -1338,46 +1370,42 @@ mod core_tests { model_fn.push("tests/multi2/model.json"); let schema = MultiNetworkModelSchema::from_path(model_fn.as_path()).unwrap(); - let mut model = schema.build_model(model_fn.parent(), None).unwrap(); + let mut builder = schema.create_model_builder(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 network_1 = builder + .entry_builder("network1") + .expect("network 1 not found") + .network_builder(); let expected_values: Array1 = [10.0; 365].to_vec().into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - let rec = AssertionF64Recorder::new( + let rec = AssertionF64RecorderBuilder::new( "assert-demand1", - MetricF64::NodeInFlow(demand1_idx), + UnresolvedMetricF64::NodeInFlow("demand1".into()), expected_values, - None, - None, ); - network_1.add_recorder(Box::new(rec)).unwrap(); + network_1.recorder(Box::new(rec)); // 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 network_2 = builder + .entry_builder("network2") + .expect("network 2 not found") + .network_builder(); let expected_values: Array1 = [10.0; 365].to_vec().into(); let expected_values: Array2 = expected_values.insert_axis(Axis(1)); - let rec = AssertionF64Recorder::new( + let rec = AssertionF64RecorderBuilder::new( "assert-demand2", - MetricF64::NodeInFlow(demand1_idx), + UnresolvedMetricF64::NodeInFlow("demand2".into()), expected_values, - None, - None, ); - network_2.add_recorder(Box::new(rec)).unwrap(); + network_2.recorder(Box::new(rec)); + + let model = builder.build().unwrap(); model.run::(&Default::default()).unwrap(); } diff --git a/pywr-schema/src/network.rs b/pywr-schema/src/network.rs index 7fdddc8f..d8e8f437 100644 --- a/pywr-schema/src/network.rs +++ b/pywr-schema/src/network.rs @@ -446,22 +446,14 @@ impl NetworkSchema { } #[cfg(feature = "core")] - pub fn build_network( + pub fn add_to_network( &self, + network_builder: &mut pywr_core::network::NetworkBuilder, domain: &ModelDomain, data_path: Option<&Path>, output_path: Option<&Path>, inter_network_transfers: &[MultiNetworkTransfer], - ) -> Result< - ( - pywr_core::network::Network, - LoadedTableCollection, - LoadedTimeseriesCollection, - ), - NetworkSchemaBuildError, - > { - let mut network = pywr_core::network::Network::default(); - + ) -> Result<(LoadedTableCollection, LoadedTimeseriesCollection), NetworkSchemaBuildError> { let tables = LoadedTableCollection::from_schema(self.tables.as_deref(), data_path)?; let timeseries = LoadedTimeseriesCollection::from_schema(self.timeseries.as_deref(), domain, data_path)?; @@ -474,57 +466,28 @@ impl NetworkSchema { inter_network_transfers, }; - // Create a combined list of nodes and virtual nodes - let mut remaining_nodes: Vec = self - .nodes - .iter() - .map(|n| n.clone().into()) - .chain( - self.virtual_nodes - .clone() - .unwrap_or_default() - .into_iter() - .map(|vn| vn.into()), - ) - .collect(); - - while !remaining_nodes.is_empty() { - let mut failed_nodes: Vec = Vec::new(); - let n = remaining_nodes.len(); - for node in remaining_nodes.into_iter() { - if let Err(e) = node.add_to_model(&mut network, &args) { - // Adding the node failed! - match e { - // And it failed because another node was not found. - // Let's try to load more nodes and see if this one can be added later - SchemaError::CoreNodeNotFound { .. } => failed_nodes.push(node), - _ => { - return match node { - NodeOrVirtualNode::Node(n) => Err(NetworkSchemaBuildError::AddNodeError { - name: n.name().to_string(), - source: Box::new(e), - }), - NodeOrVirtualNode::Virtual(vn) => Err(NetworkSchemaBuildError::AddVirtualNodeError { - name: vn.name().to_string(), - source: Box::new(e), - }), - }; - } - } - }; - } + for node in &self.nodes { + node.add_to_network(network_builder, &args) + .map_err(|source| NetworkSchemaBuildError::AddNodeError { + name: node.name().to_string(), + source: Box::new(source), + })?; + } - if failed_nodes.len() == n { - // Could not load any nodes; must be a circular reference - return Err(NetworkSchemaBuildError::CircularNodeReference); + if let Some(virtual_nodes) = &self.virtual_nodes { + for v_node in virtual_nodes { + v_node.add_to_network(network_builder, &args).map_err(|source| { + NetworkSchemaBuildError::AddVirtualNodeError { + name: v_node.name().to_string(), + source: Box::new(source), + } + })?; } - - remaining_nodes = failed_nodes; } // Create the edges for edge in &self.edges { - edge.add_to_model(&mut network, &args) + edge.add_to_network(network_builder, &args) .map_err(|source| NetworkSchemaBuildError::AddEdgeError { from_node: edge.from_node.clone(), to_node: edge.to_node.clone(), @@ -532,84 +495,36 @@ impl NetworkSchema { })?; } - // Gather all the parameters from the nodes - let mut remaining_parameters: Vec<(Option<&str>, Parameter)> = Vec::new(); + // Add all the parameters from the nodes for node in &self.nodes { if let Some(local_parameters) = node.local_parameters() { - remaining_parameters.extend(local_parameters.iter().map(|p| (Some(node.name()), p.clone()))); + for parameter in local_parameters { + parameter + .add_to_network(network_builder, &args, Some(node.name())) + .map_err(|source| NetworkSchemaBuildError::AddLocalParameterError { + parent: node.name().to_string(), + name: parameter.name().to_string(), + source: Box::new(source), + })?; + } } } // Add any global parameters if let Some(parameters) = self.parameters.as_deref() { - remaining_parameters.extend(parameters.iter().map(|p| (None, p.clone()))); - } - - // Create all the parameters - while !remaining_parameters.is_empty() { - let mut failed_parameters: Vec<(Option<&str>, Parameter)> = Vec::new(); - let n = remaining_parameters.len(); - for (parent, parameter) in remaining_parameters.into_iter() { - if let Err(e) = parameter.add_to_model(&mut network, &args, parent) { - // Adding the parameter failed! - match e { - // And it failed because another parameter was not found. - // Let's try to load more parameters and see if this one can be added later - SchemaError::CoreParameterNotFound { .. } => failed_parameters.push((parent, parameter)), - _ => { - return match parent { - Some(p) => Err(NetworkSchemaBuildError::AddLocalParameterError { - parent: p.to_string(), - name: parameter.name().to_string(), - source: Box::new(e), - }), - None => { - // Global parameter - Err(NetworkSchemaBuildError::AddParameterError { - name: parameter.name().to_string(), - source: Box::new(e), - }) - } - }; - } - }; - } - } - - if failed_parameters.len() == n { - // Could not load any parameters; must be a circular reference - let failed_names = failed_parameters.iter().map(|(_n, p)| p.name().to_string()).collect(); - return Err(NetworkSchemaBuildError::CircularParameterReference(failed_names)); - } - - remaining_parameters = failed_parameters; - } - - // Apply the constraints to the nodes - for node in &self.nodes { - node.set_constraints(&mut network, &args).map_err(|source| { - NetworkSchemaBuildError::SetNodeConstraintsError { - name: node.name().to_string(), - source: Box::new(source), - } - })?; - } - - // Apply the constraints to the virtual nodes - if let Some(virtual_nodes) = &self.virtual_nodes { - for node in virtual_nodes { - node.set_constraints(&mut network, &args).map_err(|source| { - NetworkSchemaBuildError::SetVirtualNodeConstraintsError { - name: node.name().to_string(), + for parameter in parameters { + parameter + .add_to_network(network_builder, &args, None) + .map_err(|source| NetworkSchemaBuildError::AddParameterError { + name: parameter.name().to_string(), source: Box::new(source), - } - })?; + })?; } } // Create all of the metric sets if let Some(metric_sets) = &self.metric_sets { for metric_set in metric_sets { - metric_set.add_to_model(&mut network, &args).map_err(|source| { + metric_set.add_to_network(network_builder, &args).map_err(|source| { NetworkSchemaBuildError::AddMetricSetError { name: metric_set.name.clone(), source: Box::new(source), @@ -622,7 +537,7 @@ impl NetworkSchema { if let Some(outputs) = &self.outputs { for output in outputs { output - .add_to_model(&mut network, data_path, output_path) + .add_to_model(network_builder, data_path, output_path) .map_err(|source| NetworkSchemaBuildError::AddOutputError { name: output.name().to_string(), source: Box::new(source), @@ -630,7 +545,7 @@ impl NetworkSchema { } } - Ok((network, tables, timeseries)) + Ok((tables, timeseries)) } } diff --git a/pywr-schema/src/nodes/abstraction.rs b/pywr-schema/src/nodes/abstraction.rs index 74cf1cfe..cb92dfe5 100644 --- a/pywr-schema/src/nodes/abstraction.rs +++ b/pywr-schema/src/nodes/abstraction.rs @@ -8,7 +8,7 @@ use crate::nodes::{NodeMeta, NodeSlot}; use crate::parameters::Parameter; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use schemars::JsonSchema; @@ -94,39 +94,52 @@ pub struct AbstractionNode { impl AbstractionNode { const DEFAULT_ATTRIBUTE: AbstractionNodeAttribute = AbstractionNodeAttribute::Abstraction; const DEFAULT_COMPONENT: AbstractionNodeComponent = AbstractionNodeComponent::Abstraction; - const DEFAULT_OUTPUT_SLOT: AbstractionOutputNodeSlot = AbstractionOutputNodeSlot::River; - fn mrf_sub_name() -> Option<&'static str> { - Some("mrf") + pub fn iter_output_slots(&self) -> impl Iterator + '_ { + [ + AbstractionOutputNodeSlot::River.into(), + AbstractionOutputNodeSlot::Abstraction.into(), + ] + .into_iter() + } + + pub fn default_attribute(&self) -> AbstractionNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } + + pub fn default_component(&self) -> AbstractionNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl AbstractionNode { + const DEFAULT_OUTPUT_SLOT: AbstractionOutputNodeSlot = AbstractionOutputNodeSlot::River; + fn mrf_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("mrf")) } - fn bypass_sub_name() -> Option<&'static str> { - Some("bypass") + fn bypass_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("bypass")) } - fn abstraction_sub_name() -> Option<&'static str> { - Some("abstraction") + fn abstraction_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("abstraction")) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { - let mut connectors = vec![ - (self.meta.name.as_str(), Self::bypass_sub_name().map(|s| s.to_string())), - ( - self.meta.name.as_str(), - Self::abstraction_sub_name().map(|s| s.to_string()), - ), - ]; + let mut connectors = vec![self.bypass_sub_name(), self.abstraction_sub_name()]; if self.mrf.is_some() { - connectors.push((self.meta.name.as_str(), Self::mrf_sub_name().map(|s| s.to_string()))); + connectors.push(self.mrf_sub_name()); } Ok(connectors) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { let slot = match slot { Some(s) => s.clone().try_into()?, None => Self::DEFAULT_OUTPUT_SLOT, @@ -135,47 +148,20 @@ impl AbstractionNode { let indices = match slot { AbstractionOutputNodeSlot::River => { if self.mrf.is_some() { - vec![ - (self.meta.name.as_str(), Self::mrf_sub_name().map(|s| s.to_string())), - (self.meta.name.as_str(), Self::bypass_sub_name().map(|s| s.to_string())), - ] + vec![self.mrf_sub_name(), self.bypass_sub_name()] } else { - vec![(self.meta.name.as_str(), Self::bypass_sub_name().map(|s| s.to_string()))] + vec![self.bypass_sub_name()] } } - AbstractionOutputNodeSlot::Abstraction => vec![( - self.meta.name.as_str(), - Self::abstraction_sub_name().map(|s| s.to_string()), - )], + AbstractionOutputNodeSlot::Abstraction => vec![self.abstraction_sub_name()], }; Ok(indices) } - - pub fn iter_output_slots(&self) -> impl Iterator + '_ { - [ - AbstractionOutputNodeSlot::River.into(), - AbstractionOutputNodeSlot::Abstraction.into(), - ] - .into_iter() - } - - pub fn default_attribute(&self) -> AbstractionNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> AbstractionNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl AbstractionNode { - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, @@ -187,129 +173,80 @@ impl AbstractionNode { let mut indices = Vec::new(); if self.mrf.is_some() { - indices.push( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::mrf_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::mrf_sub_name().map(String::from), - })?, - ); + indices.push(self.mrf_sub_name()); } - indices.push( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::bypass_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::bypass_sub_name().map(String::from), - })?, - ); - indices.push( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::abstraction_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::abstraction_sub_name().map(String::from), - })?, - ); + indices.push(self.bypass_sub_name()); + indices.push(self.abstraction_sub_name()); indices } AbstractionNodeComponent::Outflow => { let mut indices = Vec::new(); if self.mrf.is_some() { - indices.push( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::mrf_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::mrf_sub_name().map(String::from), - })?, - ); + indices.push(self.mrf_sub_name()); } - indices.push( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::bypass_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::bypass_sub_name().map(String::from), - })?, - ); + indices.push(self.bypass_sub_name()); indices } AbstractionNodeComponent::Abstraction => { - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::abstraction_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::abstraction_sub_name().map(String::from), - })?, - ] + vec![self.abstraction_sub_name()] } }; Ok(indices) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - if self.mrf.is_some() { - network.add_link_node(self.meta.name.as_str(), Self::mrf_sub_name())?; - } - - network.add_link_node(self.meta.name.as_str(), Self::bypass_sub_name())?; - network.add_link_node(self.meta.name.as_str(), Self::abstraction_sub_name())?; - - Ok(()) - } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { if let Some(mrf) = &self.mrf { + let mut mrf_node = pywr_core::node::NodeBuilder::link(self.mrf_sub_name()); + let value = mrf.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), Self::mrf_sub_name(), value.into())?; - } + mrf_node.max_flow(value); - if let Some(cost) = &self.mrf_cost { - if self.mrf.is_some() { + if let Some(cost) = &self.mrf_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::mrf_sub_name(), value.into())?; - } else { - return Err(SchemaError::InvalidNodeAttributes { - msg: format!( - "MRF cost defined but no MRF constraint provided for node '{}'", - self.meta.name - ), - }); + mrf_node.cost(value); } + + network.node(mrf_node); + } else if self.mrf_cost.is_some() { + return Err(SchemaError::InvalidNodeAttributes { + msg: format!( + "MRF cost defined but no MRF constraint provided for node '{}'", + self.meta.name + ), + }); } + let bypass_node = pywr_core::node::NodeBuilder::link(self.bypass_sub_name()); + let mut abstraction_node = pywr_core::node::NodeBuilder::link(self.abstraction_sub_name()); + if let Some(abs_max_flow) = &self.abs_max_flow { let value = abs_max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), Self::abstraction_sub_name(), value.into())?; + abstraction_node.max_flow(value); } if let Some(abs_min_flow) = &self.abs_min_flow { let value = abs_min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), Self::abstraction_sub_name(), value.into())?; + abstraction_node.min_flow(value); } if let Some(cost) = &self.abs_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::abstraction_sub_name(), value.into())?; + abstraction_node.cost(value); } + network.node(bypass_node); + network.node(abstraction_node); + Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -318,27 +255,22 @@ impl AbstractionNode { match attr { AbstractionNodeAttribute::Inflow => { - let indices = self.node_indices_for_flow_constraints(network, Some(NodeComponent::Inflow))?; - Ok(MetricF64::MultiNodeInFlow { - indices, + let nodes = self.nodes_for_flow_constraints(Some(NodeComponent::Inflow))?; + Ok(UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), }) } AbstractionNodeAttribute::Outflow => { - let indices = self.node_indices_for_flow_constraints(network, Some(NodeComponent::Outflow))?; - Ok(MetricF64::MultiNodeInFlow { - indices, + let nodes = self.nodes_for_flow_constraints(Some(NodeComponent::Outflow))?; + Ok(UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), }) } AbstractionNodeAttribute::Abstraction => { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::abstraction_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::abstraction_sub_name().map(String::from), - })?; - Ok(MetricF64::NodeOutFlow(idx)) + let node = self.abstraction_sub_name(); + Ok(UnresolvedMetricF64::NodeOutFlow(node)) } } } diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index f78308c5..44cc5586 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -1,19 +1,20 @@ use crate::error::ComponentConversionError; -use crate::error::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; +use crate::nodes::NodeMeta; use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_initial_storage, try_convert_node_attr, try_convert_node_meta}; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] use pywr_core::{ - metric::MetricF64, - node::StorageInitialVolume as CoreStorageInitialVolume, - parameters::{DeficitParameter, ParameterName}, + metric::UnresolvedMetricF64, + node::{UnresolvedNode, UnresolvedStorageInitialVolume}, + parameters::{DeficitParameterBuilder, ParameterName}, }; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; @@ -71,21 +72,6 @@ impl InputNode { pub const DEFAULT_ATTRIBUTE: InputNodeAttribute = InputNodeAttribute::Outflow; pub const DEFAULT_COMPONENT: InputNodeComponent = InputNodeComponent::Outflow; - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - pub fn default_attribute(&self) -> InputNodeAttribute { Self::DEFAULT_ATTRIBUTE } @@ -97,76 +83,74 @@ impl InputNode { #[cfg(feature = "core")] impl InputNode { - pub fn node_indices_for_flow_constraints( + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) + } + } + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) + } + } + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let attr = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let idx = match attr { - InputNodeComponent::Outflow => network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?, + let name = match attr { + InputNodeComponent::Outflow => UnresolvedNode::new(self.meta.name.as_str(), None), }; - Ok(vec![idx]) - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_input_node(self.meta.name.as_str(), None)?; - Ok(()) + Ok(vec![name]) } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut builder = pywr_core::node::NodeBuilder::input(self.meta.name.as_str()); + if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), None, value.into())?; + builder.cost(value); } if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), None, value.into())?; + builder.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), None, value.into())?; + builder.min_flow(value); } + network.node(builder); + Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = UnresolvedNode::new(self.meta.name.as_str(), None); let metric = match attr { - InputNodeAttribute::Outflow => MetricF64::NodeOutFlow(idx), + InputNodeAttribute::Outflow => UnresolvedMetricF64::NodeOutFlow(name), }; Ok(metric) @@ -316,303 +300,279 @@ impl LinkNode { const DEFAULT_ATTRIBUTE: LinkNodeAttribute = LinkNodeAttribute::Outflow; const DEFAULT_COMPONENT: LinkNodeComponent = LinkNodeComponent::Outflow; - fn soft_min_node_sub_name() -> Option<&'static str> { - Some("soft_min") + pub fn default_attribute(&self) -> LinkNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } + + pub fn default_component(&self) -> LinkNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl LinkNode { + fn soft_min_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("soft_min")) } - fn soft_max_node_sub_name() -> Option<&'static str> { - Some("soft_max") + fn soft_max_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("soft_max")) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { return Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }); } - let mut connectors = vec![(self.meta.name.as_str(), None)]; + let mut connectors = vec![UnresolvedNode::new(self.meta.name.as_str(), None)]; if self.soft_min.is_some() { - connectors.push(( - self.meta.name.as_str(), - Self::soft_min_node_sub_name().map(|s| s.to_string()), - )); + connectors.push(self.soft_min_node_sub_name()); } if self.soft_max.is_some() { - connectors.push(( - self.meta.name.as_str(), - Self::soft_max_node_sub_name().map(|s| s.to_string()), - )); + connectors.push(self.soft_max_node_sub_name()); } Ok(connectors) } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { return Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }); } - let mut connectors = vec![(self.meta.name.as_str(), None)]; + let mut connectors = vec![UnresolvedNode::new(self.meta.name.as_str(), None)]; if self.soft_min.is_some() { - connectors.push(( - self.meta.name.as_str(), - Self::soft_min_node_sub_name().map(|s| s.to_string()), - )); + connectors.push(self.soft_min_node_sub_name()); } if self.soft_max.is_some() { - connectors.push(( - self.meta.name.as_str(), - Self::soft_max_node_sub_name().map(|s| s.to_string()), - )); + connectors.push(self.soft_max_node_sub_name()); } Ok(connectors) } - pub fn default_attribute(&self) -> LinkNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> LinkNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl LinkNode { - fn aggregated_node_sub_name() -> Option<&'static str> { - Some("aggregate_node") + fn aggregated_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("aggregate_node")) } /// The aggregated node name of `[name]` and `[name].soft_min` when both soft constraints are provided. - fn aggregated_node_l_l_min_sub_name() -> Option<&'static str> { - Some("aggregate_node_l_l_min") + fn aggregated_node_l_l_min_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("aggregate_node_l_l_min")) } - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let indices = match component { + let nodes = match component { // The same nodes are returned for inflow and outflow LinkNodeComponent::Inflow | LinkNodeComponent::Outflow => { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = UnresolvedNode::new(self.meta.name.as_str(), None); - let mut indices = vec![idx]; + let mut nodes = vec![name]; if self.soft_min.is_some() { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::soft_min_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::soft_min_node_sub_name().map(|s| s.to_string()), - })?; - - indices.push(idx); + nodes.push(self.soft_min_node_sub_name()); } if self.soft_max.is_some() { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::soft_max_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::soft_max_node_sub_name().map(|s| s.to_string()), - })?; - - indices.push(idx); + nodes.push(self.soft_max_node_sub_name()); } - indices + nodes } }; - Ok(indices) + Ok(nodes) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - let node_name = self.meta.name.as_str(); - let link = network.add_link_node(node_name, None)?; - // add soft constrained nodes and aggregated node - match (&self.soft_min, &self.soft_max) { - (Some(_), None) => { - // add L_min and aggregated node for L and L_min - let soft_min_node = network.add_link_node(node_name, Self::soft_min_node_sub_name())?; - network.add_aggregated_node( - node_name, - Self::aggregated_node_sub_name(), - &[vec![link], vec![soft_min_node]], - None, - )?; - } - (None, Some(_)) => { - // add L_max and aggregated node for L and L_max - let soft_max_node = network.add_link_node(node_name, Self::soft_max_node_sub_name())?; - network.add_aggregated_node( - node_name, - Self::aggregated_node_sub_name(), - &[vec![link], vec![soft_max_node]], - None, - )?; - } - (Some(_), Some(_)) => { - // add L_min and L_max, and aggregated node for L, L_min and L_max - let soft_min_node = network.add_link_node(node_name, Self::soft_min_node_sub_name())?; - let soft_max_node = network.add_link_node(node_name, Self::soft_max_node_sub_name())?; - network.add_aggregated_node( - node_name, - Self::aggregated_node_sub_name(), - &[vec![link], vec![soft_min_node], vec![soft_max_node]], - None, - )?; - network.add_aggregated_node( - node_name, - Self::aggregated_node_l_l_min_sub_name(), - &[vec![link], vec![soft_min_node]], - None, - )?; - } - (None, None) => {} - }; - Ok(()) - } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { let node_name = self.meta.name.as_str(); - match (&self.soft_min, &self.soft_max) { - (None, None) => { - // soft constraints not added. Set constraints for L only - if let Some(cost) = &self.cost { - let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, None, value.into())?; - } - if let Some(max_flow) = &self.max_flow { - let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(node_name, None, value.into())?; - } + let mut link = pywr_core::node::NodeBuilder::link(self.meta.name.as_str()); - if let Some(min_flow) = &self.min_flow { - let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(node_name, None, value.into())?; - } - } + // add soft constrained nodes and aggregated node + match (&self.soft_min, &self.soft_max) { (Some(soft_min), None) => { + // add L_min and aggregated node for L and L_min + let mut soft_min_node = pywr_core::node::NodeBuilder::link(self.soft_min_node_sub_name()); + + let mut agg_node = + pywr_core::aggregated_node::AggregatedNodeBuilder::new(self.aggregated_node_sub_name()); + + agg_node.nodes(vec![ + UnresolvedNode::new(node_name, None), + self.soft_min_node_sub_name(), + ]); + // add L_min constraints if let Some(soft_min_flow) = &soft_min.flow { let value = soft_min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(node_name, Self::soft_min_node_sub_name(), value.into())?; + soft_min_node.max_flow(value); } if let Some(soft_min_cost) = &soft_min.cost { let value = soft_min_cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, Self::soft_min_node_sub_name(), value.into())?; + soft_min_node.cost(value); } // add cost on L if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, None, value.into())?; + link.cost(value); } // add constraints on aggregated node if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_max_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + agg_node.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_min_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + agg_node.min_flow(value); } + + network.node(link); + network.node(soft_min_node); + network.agg_node(agg_node); } (None, Some(soft_max)) => { + // add L_max and aggregated node for L and L_max + let mut soft_max_node = pywr_core::node::NodeBuilder::link(self.soft_max_node_sub_name()); + + let mut agg_node = + pywr_core::aggregated_node::AggregatedNodeBuilder::new(self.aggregated_node_sub_name()); + + agg_node.nodes(vec![ + UnresolvedNode::new(node_name, None), + self.soft_max_node_sub_name(), + ]); + // add L_max constraints if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, Self::soft_max_node_sub_name(), value.into())?; + soft_max_node.cost(value); } if let Some(soft_max_flow) = &soft_max.flow { let value = soft_max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(node_name, Self::soft_max_node_sub_name(), value.into())?; + soft_max_node.max_flow(value); } // add constraints on L if let Some(soft_max_cost) = &soft_max.cost { let value = soft_max_cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, None, value.into())?; + link.cost(value); } // add constraints on aggregated node if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_max_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + agg_node.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_min_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + agg_node.min_flow(value); } + + network.node(link); + network.node(soft_max_node); + network.agg_node(agg_node); } (Some(soft_min), Some(soft_max)) => { + // add L_min and L_max, and aggregated node for L, L_min and L_max + let mut soft_min_node = pywr_core::node::NodeBuilder::link(self.soft_min_node_sub_name()); + + let mut soft_max_node = pywr_core::node::NodeBuilder::link(self.soft_max_node_sub_name()); + + let mut agg_node = + pywr_core::aggregated_node::AggregatedNodeBuilder::new(self.aggregated_node_sub_name()); + + agg_node.nodes(vec![ + UnresolvedNode::new(node_name, None), + self.soft_min_node_sub_name(), + self.soft_max_node_sub_name(), + ]); + + let mut agg_node_l_l = + pywr_core::aggregated_node::AggregatedNodeBuilder::new(self.aggregated_node_l_l_min_sub_name()); + + agg_node_l_l.nodes(vec![ + UnresolvedNode::new(node_name, None), + self.soft_min_node_sub_name(), + ]); + // set L_max constraint if let Some(soft_max_cost) = &soft_max.cost { let value = soft_max_cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, Self::soft_max_node_sub_name(), value.into())?; + soft_max_node.cost(value); } // set L constraint if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, None, value.into())?; + link.cost(value); } // set L_min constraints if let Some(soft_min_flow) = &soft_min.flow { let value = soft_min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(node_name, Self::soft_min_node_sub_name(), value.into())?; + soft_min_node.max_flow(value); } if let Some(soft_min_cost) = &soft_min.cost { let value = soft_min_cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(node_name, Self::soft_min_node_sub_name(), value.into())?; + soft_min_node.cost(value); } // add constraints on node aggregating all three nodes if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_max_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + agg_node.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_min_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + agg_node.min_flow(value); } // add constraints on node aggregating `[name]` and `[name].soft_min` if let Some(soft_max_flow) = &soft_max.flow { let value = soft_max_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_max_flow( - node_name, - Self::aggregated_node_l_l_min_sub_name(), - value.into(), - )?; + agg_node.max_flow(value); } + + network.node(link); + network.node(soft_min_node); + network.node(soft_max_node); + network.agg_node(agg_node); + network.agg_node(agg_node_l_l); + } + (None, None) => { + // soft constraints not added. Set constraints for L only + if let Some(cost) = &self.cost { + let value = cost.load(network, args, Some(&self.meta.name))?; + link.cost(value); + } + + if let Some(max_flow) = &self.max_flow { + let value = max_flow.load(network, args, Some(&self.meta.name))?; + link.max_flow(value); + } + + if let Some(min_flow) = &self.min_flow { + let value = min_flow.load(network, args, Some(&self.meta.name))?; + link.min_flow(value); + } + network.node(link); } }; Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -620,59 +580,29 @@ impl LinkNode { }; let node_name = self.meta.name.as_str(); - let link_node = - network - .get_node_index_by_name(node_name, None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: node_name.to_string(), - sub_name: None, - })?; + let link_node = UnresolvedNode::new(node_name, None); // combine the flow through the nodes - let indices = match (&self.soft_min, &self.soft_max) { + let nodes = match (&self.soft_min, &self.soft_max) { (Some(_), None) => { - let soft_min_node = network - .get_node_index_by_name(node_name, Self::soft_min_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: node_name.to_string(), - sub_name: Self::soft_min_node_sub_name().map(String::from), - })?; - vec![link_node, soft_min_node] + vec![link_node, self.soft_min_node_sub_name()] } (None, Some(_)) => { - let soft_max_node = network - .get_node_index_by_name(node_name, Self::soft_max_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: node_name.to_string(), - sub_name: Self::soft_max_node_sub_name().map(String::from), - })?; - vec![link_node, soft_max_node] + vec![link_node, self.soft_max_node_sub_name()] } (Some(_), Some(_)) => { - let soft_min_node = network - .get_node_index_by_name(node_name, Self::soft_min_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: node_name.to_string(), - sub_name: Self::soft_min_node_sub_name().map(String::from), - })?; - let soft_max_node = network - .get_node_index_by_name(node_name, Self::soft_max_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: node_name.to_string(), - sub_name: Self::soft_max_node_sub_name().map(String::from), - })?; - vec![link_node, soft_min_node, soft_max_node] + vec![link_node, self.soft_min_node_sub_name(), self.soft_max_node_sub_name()] } (None, None) => vec![link_node], }; let metric = match attr { - LinkNodeAttribute::Outflow => MetricF64::MultiNodeInFlow { - indices, + LinkNodeAttribute::Outflow => UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), }, - LinkNodeAttribute::Inflow => MetricF64::MultiNodeOutFlow { - indices, + LinkNodeAttribute::Inflow => UnresolvedMetricF64::MultiNodeOutFlow { + nodes, name: self.meta.name.to_string(), }, }; @@ -782,22 +712,6 @@ impl OutputNode { const DEFAULT_ATTRIBUTE: OutputNodeAttribute = OutputNodeAttribute::Inflow; const DEFAULT_COMPONENT: OutputNodeComponent = OutputNodeComponent::Inflow; - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - pub fn default_attribute(&self) -> OutputNodeAttribute { Self::DEFAULT_ATTRIBUTE } @@ -809,97 +723,95 @@ impl OutputNode { #[cfg(feature = "core")] impl OutputNode { - pub fn node_indices_for_flow_constraints( + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) + } + } + + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) + } + } + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let idx = match component { - OutputNodeComponent::Inflow => network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?, + let node = match component { + OutputNodeComponent::Inflow => UnresolvedNode::new(self.meta.name.as_str(), None), }; - Ok(vec![idx]) + Ok(vec![node]) } pub fn create_metric( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, attribute: Option, - ) -> Result { + ) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = UnresolvedNode::new(self.meta.name.as_str(), None); let metric = match attr { - OutputNodeAttribute::Inflow => MetricF64::NodeInFlow(idx), + OutputNodeAttribute::Inflow => UnresolvedMetricF64::NodeInFlow(name), OutputNodeAttribute::Deficit => { let deficit_parameter_name = ParameterName::new("deficit", Some(self.meta.name.as_str())); // Create a parameter for the deficit metric if it does not already exist - let deficit_parameter_idx = match network.get_parameter_index_by_name(&deficit_parameter_name) { - Some(p_idx) => p_idx, - None => { - let deficit_parameter = DeficitParameter::new( - deficit_parameter_name, - MetricF64::NodeInFlow(idx), - MetricF64::NodeMaxFlow(idx), - ); - - network.add_parameter(Box::new(deficit_parameter))? - } - }; - - deficit_parameter_idx.into_metric_f64_after() + if !network.parameters().contains_name(&deficit_parameter_name) { + let flow = UnresolvedMetricF64::NodeInFlow(self.meta.name.as_str().into()); + let max_flow = UnresolvedMetricF64::NodeMaxFlow(self.meta.name.as_str().into()); + let deficit_builder = DeficitParameterBuilder::new(deficit_parameter_name.clone(), flow, max_flow); + + network.parameters().f64(Box::new(deficit_builder)); + } + + UnresolvedMetricF64::new_parameter_after(deficit_parameter_name) } }; Ok(metric) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_output_node(self.meta.name.as_str(), None)?; - Ok(()) - } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut output = pywr_core::node::NodeBuilder::output(self.meta.name.as_str()); + if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), None, value.into())?; + output.cost(value); } if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), None, value.into())?; + output.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), None, value.into())?; + output.min_flow(value); } + network.node(output); + Ok(()) } } @@ -956,11 +868,13 @@ impl Default for StorageInitialVolume { } #[cfg(feature = "core")] -impl From for CoreStorageInitialVolume { +impl From for UnresolvedStorageInitialVolume { fn from(v: StorageInitialVolume) -> Self { match v { - StorageInitialVolume::Absolute { volume } => CoreStorageInitialVolume::Absolute(volume), - StorageInitialVolume::Proportional { proportion } => CoreStorageInitialVolume::Proportional(proportion), + StorageInitialVolume::Absolute { volume } => UnresolvedStorageInitialVolume::Absolute(volume), + StorageInitialVolume::Proportional { proportion } => { + UnresolvedStorageInitialVolume::Proportional(proportion) + } } } } @@ -1006,92 +920,84 @@ pub struct StorageNode { impl StorageNode { const DEFAULT_ATTRIBUTE: StorageNodeAttribute = StorageNodeAttribute::Volume; - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn default_attribute(&self) -> StorageNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } +} + +#[cfg(feature = "core")] +impl StorageNode { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![(self.meta.name.as_str(), None)]) + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![(self.meta.name.as_str(), None)]) + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) } } + pub fn nodes_for_storage_constraints(&self) -> Result, SchemaError> { + let node = UnresolvedNode::new(self.meta.name.as_str(), None); - pub fn default_attribute(&self) -> StorageNodeAttribute { - Self::DEFAULT_ATTRIBUTE + Ok(vec![node]) } -} -#[cfg(feature = "core")] -impl StorageNode { - pub fn node_indices_for_storage_constraints( + /// Creates a populated node builder. + pub(crate) fn make_builder( &self, - network: &pywr_core::network::Network, - ) -> Result, SchemaError> { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; - Ok(vec![idx]) - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - // Add the node with no constraints - network.add_storage_node(self.meta.name.as_str(), None, self.initial_volume.into(), None, None)?; - Ok(()) - } - - pub fn set_constraints( - &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, - ) -> Result<(), SchemaError> { + ) -> Result { + let mut storage = pywr_core::node::NodeBuilder::storage(self.meta.name.as_str()); + + storage.initial_volume(self.initial_volume.into()); + if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), None, value.into())?; + storage.cost(value); } if let Some(min_volume) = &self.min_volume { let value = min_volume.load(network, args, Some(&self.meta.name))?; - network.set_node_min_volume(self.meta.name.as_str(), None, Some(value.try_into()?))?; + storage.min_volume(value); } if let Some(max_volume) = &self.max_volume { let value = max_volume.load(network, args, Some(&self.meta.name))?; - network.set_node_max_volume(self.meta.name.as_str(), None, Some(value.try_into()?))?; + storage.max_volume(value); } + Ok(storage) + } + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { + let storage = self.make_builder(network, args)?; + network.node(storage); Ok(()) } - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = UnresolvedNode::new(self.meta.name.as_str(), None); let metric = match attr { - StorageNodeAttribute::Volume => MetricF64::NodeVolume(idx), - StorageNodeAttribute::MaxVolume => MetricF64::NodeMaxVolume(idx), - StorageNodeAttribute::ProportionalVolume => MetricF64::NodeProportionalVolume(idx), + StorageNodeAttribute::Volume => UnresolvedMetricF64::NodeVolume(name), + StorageNodeAttribute::MaxVolume => UnresolvedMetricF64::NodeMaxVolume(name), + StorageNodeAttribute::ProportionalVolume => UnresolvedMetricF64::NodeProportionalVolume(name), }; Ok(metric) @@ -1197,22 +1103,6 @@ impl CatchmentNode { const DEFAULT_ATTRIBUTE: CatchmentNodeAttribute = CatchmentNodeAttribute::Outflow; const DEFAULT_COMPONENT: CatchmentNodeComponent = CatchmentNodeComponent::Outflow; - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - pub fn default_attribute(&self) -> CatchmentNodeAttribute { Self::DEFAULT_ATTRIBUTE } @@ -1224,71 +1114,70 @@ impl CatchmentNode { #[cfg(feature = "core")] impl CatchmentNode { - pub fn node_indices_for_flow_constraints( + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) + } + } + + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![UnresolvedNode::new(self.meta.name.as_str(), None)]) + } + } + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let idx = match component { - CatchmentNodeComponent::Outflow => network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?, + let node = match component { + CatchmentNodeComponent::Outflow => UnresolvedNode::new(self.meta.name.as_str(), None), }; - Ok(vec![idx]) - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_input_node(self.meta.name.as_str(), None)?; - Ok(()) + Ok(vec![node]) } - - pub fn set_constraints( + pub fn add_to_model( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut input = pywr_core::node::NodeBuilder::input(self.meta.name.as_str()); + if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), None, value.into())?; + input.cost(value); } if let Some(flow) = &self.flow { let value = flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), None, value.clone().into())?; - network.set_node_max_flow(self.meta.name.as_str(), None, value.into())?; + input.min_flow(value.clone()); + input.max_flow(value); } + network.node(input); + Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = UnresolvedNode::new(self.meta.name.as_str(), None); let metric = match attr { - CatchmentNodeAttribute::Outflow => MetricF64::NodeOutFlow(idx), + CatchmentNodeAttribute::Outflow => UnresolvedMetricF64::NodeOutFlow(name), }; Ok(metric) diff --git a/pywr-schema/src/nodes/delay.rs b/pywr-schema/src/nodes/delay.rs index b98016ce..b261b874 100644 --- a/pywr-schema/src/nodes/delay.rs +++ b/pywr-schema/src/nodes/delay.rs @@ -1,15 +1,16 @@ -use crate::error::SchemaError; use crate::error::{ComponentConversionError, ConversionError}; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; +use crate::nodes::NodeMeta; use crate::parameters::{ConstantValue, Parameter}; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::v1::try_convert_node_meta; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::{metric::MetricF64, parameters::ParameterName}; +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode, parameters::ParameterName}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::nodes::DelayNode as DelayNodeV1; use schemars::JsonSchema; @@ -62,119 +63,91 @@ impl DelayNode { const DEFAULT_ATTRIBUTE: DelayNodeAttribute = DelayNodeAttribute::Outflow; const DEFAULT_COMPONENT: DelayNodeComponent = DelayNodeComponent::Outflow; - fn output_sub_name() -> Option<&'static str> { - Some("inflow") + pub fn default_attribute(&self) -> DelayNodeAttribute { + Self::DEFAULT_ATTRIBUTE } - fn input_sub_name() -> Option<&'static str> { - Some("outflow") + pub fn default_component(&self) -> DelayNodeComponent { + Self::DEFAULT_COMPONENT } +} - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { +#[cfg(feature = "core")] +impl DelayNode { + fn output_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("inflow")) + } + + fn input_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("outflow")) + } + + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { // Inflow goes to the output node - Ok(vec![( - self.meta.name.as_str(), - Self::output_sub_name().map(|s| s.to_string()), - )]) + Ok(vec![self.output_sub_name()]) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { // Outflow goes from the input node - Ok(vec![( - self.meta.name.as_str(), - Self::input_sub_name().map(|s| s.to_string()), - )]) + Ok(vec![self.input_sub_name()]) } } - - pub fn default_attribute(&self) -> DelayNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> DelayNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl DelayNode { - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let idx = match component { - DelayNodeComponent::Inflow => network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?, - DelayNodeComponent::Outflow => network - .get_node_index_by_name(self.meta.name.as_str(), Self::input_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::input_sub_name().map(String::from), - })?, + let node = match component { + DelayNodeComponent::Inflow => self.output_sub_name(), + DelayNodeComponent::Outflow => self.input_sub_name(), }; - Ok(vec![idx]) + Ok(vec![node]) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_output_node(self.meta.name.as_str(), Self::output_sub_name())?; - network.add_input_node(self.meta.name.as_str(), Self::input_sub_name())?; - - Ok(()) - } - - pub fn set_constraints( + pub fn add_to_model( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let output_node = pywr_core::node::NodeBuilder::output(self.output_sub_name()); + let mut input_node = pywr_core::node::NodeBuilder::input(self.input_sub_name()); + // Create the delay parameter using the node's name as the parent identifier let name = ParameterName::new("delay", Some(self.meta.name.as_str())); - let output_idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?; - let metric = MetricF64::NodeInFlow(output_idx); - let p = pywr_core::parameters::DelayParameter::new( - name, + + let metric = UnresolvedMetricF64::NodeInFlow(output_node.name().clone()); + let p = pywr_core::parameters::DelayParameterBuilder::new( + name.clone(), metric, self.delay.load(args.tables)?, self.initial_value.load(args.tables)?, ); - let delay_idx = network.add_parameter(Box::new(p))?; + network.parameters().f64(Box::new(p)); // Apply it as a constraint on the input node. - let metric: MetricF64 = delay_idx.into_metric_f64_before(); - network.set_node_max_flow(self.meta.name.as_str(), Self::input_sub_name(), metric.clone().into())?; - network.set_node_min_flow(self.meta.name.as_str(), Self::input_sub_name(), metric.into())?; + let metric = UnresolvedMetricF64::new_parameter_before(name); + input_node.min_flow(metric.clone()); + input_node.max_flow(metric); + + network.node(output_node); + network.node(input_node); Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -182,24 +155,8 @@ impl DelayNode { }; let metric = match attr { - DelayNodeAttribute::Outflow => { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::input_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::input_sub_name().map(String::from), - })?; - MetricF64::NodeOutFlow(idx) - } - DelayNodeAttribute::Inflow => { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?; - MetricF64::NodeInFlow(idx) - } + DelayNodeAttribute::Outflow => UnresolvedMetricF64::NodeOutFlow(self.input_sub_name()), + DelayNodeAttribute::Inflow => UnresolvedMetricF64::NodeInFlow(self.output_sub_name()), }; Ok(metric) diff --git a/pywr-schema/src/nodes/loss_link.rs b/pywr-schema/src/nodes/loss_link.rs index 28aad8f0..34955e29 100644 --- a/pywr-schema/src/nodes/loss_link.rs +++ b/pywr-schema/src/nodes/loss_link.rs @@ -1,16 +1,21 @@ use crate::error::ComponentConversionError; -use crate::error::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; +use crate::nodes::NodeMeta; use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_node_attr, try_convert_node_meta}; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::{aggregated_node::Relationship, metric::MetricF64}; +use pywr_core::{ + aggregated_node::{ProportionalFactorsBuilder, RatioFactorsBuilder, RelationshipBuilder}, + metric::UnresolvedMetricF64, + node::UnresolvedNode, +}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::LossLinkNode as LossLinkNodeV1; @@ -37,10 +42,10 @@ impl LossFactor { /// not a constant zero. If a zero is loaded, then `None` is returned. pub fn load( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result>, SchemaError> { match self { LossFactor::Gross { factor } => { let lf = factor.load(network, args, parent)?; @@ -50,7 +55,10 @@ impl LossFactor { return Ok(None); } // Gross losses are configured as a proportion of the net flow - Ok(Some(Relationship::new_proportion_factors(&[lf]))) + let mut builder = ProportionalFactorsBuilder::default(); + builder.factor(lf); + + Ok(Some(Box::new(builder))) } LossFactor::Net { factor } => { let lf = factor.load(network, args, parent)?; @@ -60,7 +68,10 @@ impl LossFactor { return Ok(None); } // Net losses are configured as a ratio of the net flow - Ok(Some(Relationship::new_ratio_factors(&[1.0.into(), lf]))) + let mut builder = RatioFactorsBuilder::default(); + builder.factor(1.0.into()); + builder.factor(lf); + Ok(Some(Box::new(builder))) } } } @@ -116,169 +127,136 @@ impl LossLinkNode { const DEFAULT_ATTRIBUTE: LossLinkNodeAttribute = LossLinkNodeAttribute::Outflow; const DEFAULT_COMPONENT: LossLinkNodeComponent = LossLinkNodeComponent::Outflow; - fn loss_sub_name() -> Option<&'static str> { - Some("loss") + pub fn default_attribute(&self) -> LossLinkNodeAttribute { + Self::DEFAULT_ATTRIBUTE } - fn net_sub_name() -> Option<&'static str> { - Some("net") + pub fn default_component(&self) -> LossLinkNodeComponent { + Self::DEFAULT_COMPONENT } +} - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { +#[cfg(feature = "core")] +impl LossLinkNode { + fn loss_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("loss")) + } + + fn net_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("net")) + } + + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { // Gross inflow always goes to the net node ... - let mut input_connectors = vec![(self.meta.name.as_str(), Self::net_sub_name().map(|s| s.to_string()))]; + let mut input_connectors = vec![self.net_sub_name()]; // ... but only to the loss node if a loss is defined if self.loss_factor.is_some() { - input_connectors.push((self.meta.name.as_str(), Self::loss_sub_name().map(|s| s.to_string()))); + input_connectors.push(self.loss_sub_name()); } Ok(input_connectors) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { // Only net goes to the downstream. - Ok(vec![( - self.meta.name.as_str(), - Self::net_sub_name().map(|s| s.to_string()), - )]) + Ok(vec![self.net_sub_name()]) } } - - pub fn default_attribute(&self) -> LossLinkNodeAttribute { - Self::DEFAULT_ATTRIBUTE + fn agg_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("agg")) } - pub fn default_component(&self) -> LossLinkNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl LossLinkNode { - fn agg_sub_name() -> Option<&'static str> { - Some("agg") - } - - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default attribute if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let indices = match component { + let nodes = match component { LossLinkNodeComponent::Inflow => { // If the loss node is defined, we need to return both the net and loss nodes - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(loss_idx) => { - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - loss_idx, - ] - } - None => vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ], + if self.loss_factor.is_some() { + vec![self.net_sub_name(), self.loss_sub_name()] + } else { + vec![self.net_sub_name()] } } LossLinkNodeComponent::Outflow => { - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ] + vec![self.net_sub_name()] } LossLinkNodeComponent::Loss => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(idx) => vec![idx], - None => return Ok(vec![]), // No loss node defined, so return empty + if self.loss_factor.is_some() { + vec![self.loss_sub_name()] + } else { + vec![] } } }; - Ok(indices) - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - let idx_net = network.add_link_node(self.meta.name.as_str(), Self::net_sub_name())?; - // TODO make the loss node configurable (i.e. it could be a link if a network wanted to use the loss) - // The above would need to support slots in the connections. - - if self.loss_factor.is_some() { - let idx_loss = network.add_output_node(self.meta.name.as_str(), Self::loss_sub_name())?; - // This aggregated node will contain the factors to enforce the loss - network.add_aggregated_node( - self.meta.name.as_str(), - Self::agg_sub_name(), - &[vec![idx_net], vec![idx_loss]], - None, - )?; - } - Ok(()) + Ok(nodes) } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut net_node = pywr_core::node::NodeBuilder::link(self.net_sub_name()); + // TODO make the loss node configurable (i.e. it could be a link if a network wanted to use the loss) + // The above would need to support slots in the connections. + if let Some(cost) = &self.net_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::net_sub_name(), value.into())?; + net_node.cost(value); } if let Some(max_flow) = &self.max_net_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), Self::net_sub_name(), value.into())?; + net_node.max_flow(value); } if let Some(min_flow) = &self.min_net_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), Self::net_sub_name(), value.into())?; + net_node.min_flow(value); } if let Some(loss_factor) = &self.loss_factor { + let mut loss_node = pywr_core::node::NodeBuilder::output(self.loss_sub_name()); + // This aggregated node will contain the factors to enforce the loss + let mut agg_node = pywr_core::AggregatedNodeBuilder::new(self.agg_sub_name()); + + agg_node.nodes(vec![net_node.name().clone()]); + agg_node.nodes(vec![loss_node.name().clone()]); + let factors = loss_factor.load(network, args, Some(&self.meta.name))?; - if factors.is_none() { + if let Some(factors) = factors { + agg_node.relationship(factors); + } else { // 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()))?; + loss_node.max_flow(0.0.into()); } - network.set_aggregated_node_relationship(self.meta.name.as_str(), Self::agg_sub_name(), factors)?; + + network.agg_node(agg_node); + network.node(loss_node); } + network.node(net_node); Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -287,47 +265,23 @@ impl LossLinkNode { let metric = match attr { LossLinkNodeAttribute::Inflow => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - // Loss node is defined. The total inflow is the sum of the net and loss nodes; - Some(loss_idx) => { - let indices = vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - loss_idx, - ]; - MetricF64::MultiNodeInFlow { - indices, - name: self.meta.name.to_string(), - } + if self.loss_factor.is_some() { + let nodes = vec![self.net_sub_name(), self.loss_sub_name()]; + UnresolvedMetricF64::MultiNodeInFlow { + nodes, + name: self.meta.name.to_string(), } + } else { // No loss node defined, so just use the net node - None => MetricF64::NodeInFlow( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ), + UnresolvedMetricF64::NodeInFlow(self.net_sub_name()) } } - LossLinkNodeAttribute::Outflow => { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?; - MetricF64::NodeOutFlow(idx) - } + LossLinkNodeAttribute::Outflow => UnresolvedMetricF64::NodeOutFlow(self.net_sub_name()), LossLinkNodeAttribute::Loss => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(idx) => MetricF64::NodeInFlow(idx), - None => 0.0.into(), + if self.loss_factor.is_some() { + UnresolvedMetricF64::NodeInFlow(self.loss_sub_name()) + } else { + 0.0.into() } } }; diff --git a/pywr-schema/src/nodes/mod.rs b/pywr-schema/src/nodes/mod.rs index 40964b70..57a0a1e5 100644 --- a/pywr-schema/src/nodes/mod.rs +++ b/pywr-schema/src/nodes/mod.rs @@ -55,15 +55,14 @@ mod water_treatment_works; // `virtual` is a reserved keyword in Rust, so we use `virtual_nodes` as the module name mod virtual_nodes; -use crate::error::SchemaError; use crate::error::{ComponentConversionError, ConversionError}; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; use crate::network::NetworkSchema; use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; use crate::visit::{VisitMetrics, VisitPaths}; +#[cfg(feature = "core")] +use crate::{error::SchemaError, network::LoadArgs}; pub use abstraction::AbstractionNode; pub use attributes::NodeAttribute; pub use components::NodeComponent; @@ -80,7 +79,7 @@ pub use piecewise_link::{ pub use piecewise_storage::{PiecewiseStorageNode, PiecewiseStorageNodeAttribute, PiecewiseStore}; pub use placeholder::PlaceholderNode; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::PywrVisitAll; use pywr_v1_schema::nodes::{ CoreNode as CoreNodeV1, Node as NodeV1, NodeMeta as NodeMetaV1, NodePosition as NodePositionV1, @@ -340,38 +339,6 @@ impl Node { } } - /// Get the input connectors for this node. - /// - /// The `slot` argument is used for nodes that have multiple input connectors. If the node - /// does not have multiple input connectors, then a [`SchemaError`] is returned. - /// - /// The input connectors are returned as a vector of tuples, where the first element is the name of the - /// connector, and the second element is an optional sub-name. The sub-name is used for nodes - /// that have multiple internal nodes, such as a [`ReservoirNode`]. - /// - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - match self { - Node::Input(n) => n.input_connectors(slot), - Node::Link(n) => n.input_connectors(slot), - Node::Output(n) => n.input_connectors(slot), - Node::Storage(n) => n.input_connectors(slot), - Node::Catchment(n) => n.input_connectors(slot), - Node::RiverGauge(n) => n.input_connectors(slot), - Node::LossLink(n) => n.input_connectors(slot), - Node::River(n) => n.input_connectors(slot), - Node::RiverSplitWithGauge(n) => n.input_connectors(slot), - Node::WaterTreatmentWorks(n) => n.input_connectors(slot), - Node::PiecewiseLink(n) => n.input_connectors(slot), - Node::PiecewiseStorage(n) => n.input_connectors(slot), - Node::Delay(n) => n.input_connectors(slot), - Node::Turbine(n) => n.input_connectors(slot), - Node::Reservoir(n) => n.input_connectors(slot), - // Deliberately do not take a slot for Placeholder nodes so they can be used with any slot - Node::Placeholder(n) => n.input_connectors(), - Node::Abstraction(n) => n.input_connectors(slot), - } - } - /// Get any input (or "to") slots that this node has. pub fn iter_input_slots(&self) -> Option + '_>> { match self { @@ -395,38 +362,6 @@ impl Node { } } - /// Get the output connectors for this node. - /// - /// The `slot` argument is used for nodes that have multiple output connectors. If the node - /// does not have multiple output connectors, then a [`SchemaError`] is returned. - /// - /// The output connectors are returned as a vector of tuples, where the first element is the name of the - /// connector, and the second element is an optional sub-name. The sub-name is used for nodes - /// that have multiple internal nodes, such as a [`ReservoirNode`]. - /// - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - match self { - Node::Input(n) => n.output_connectors(slot), - Node::Link(n) => n.output_connectors(slot), - Node::Output(n) => n.output_connectors(slot), - Node::Storage(n) => n.output_connectors(slot), - Node::Catchment(n) => n.output_connectors(slot), - Node::RiverGauge(n) => n.output_connectors(slot), - Node::LossLink(n) => n.output_connectors(slot), - Node::River(n) => n.output_connectors(slot), - Node::RiverSplitWithGauge(n) => n.output_connectors(slot), - Node::WaterTreatmentWorks(n) => n.output_connectors(slot), - Node::PiecewiseLink(n) => n.output_connectors(slot), - Node::PiecewiseStorage(n) => n.output_connectors(slot), - Node::Delay(n) => n.output_connectors(slot), - Node::Turbine(n) => n.output_connectors(slot), - Node::Reservoir(n) => n.output_connectors(slot), - // Deliberately do not take a slot for Placeholder nodes so they can be used with any slot - Node::Placeholder(n) => n.output_connectors(), - Node::Abstraction(n) => n.output_connectors(slot), - } - } - /// Get any output (or "from") slots that this node has. pub fn iter_output_slots(&self) -> Option + '_>> { match self { @@ -524,29 +459,92 @@ impl Node { #[cfg(feature = "core")] impl Node { - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { + /// Get the input connectors for this node. + /// + /// The `slot` argument is used for nodes that have multiple input connectors. If the node + /// does not have multiple input connectors, then a [`SchemaError`] is returned. + /// + /// The input connectors are returned as a vector of unresolved node names. + /// + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { match self { - Node::Input(n) => n.add_to_model(network), - Node::Link(n) => n.add_to_model(network), - Node::Output(n) => n.add_to_model(network), - Node::Storage(n) => n.add_to_model(network), - Node::Catchment(n) => n.add_to_model(network), - Node::RiverGauge(n) => n.add_to_model(network), - Node::LossLink(n) => n.add_to_model(network), - Node::River(n) => n.add_to_model(network), - Node::RiverSplitWithGauge(n) => n.add_to_model(network), - Node::WaterTreatmentWorks(n) => n.add_to_model(network), - Node::PiecewiseLink(n) => n.add_to_model(network), - Node::PiecewiseStorage(n) => n.add_to_model(network), - Node::Delay(n) => n.add_to_model(network), - Node::Turbine(n) => n.add_to_model(network), - Node::Reservoir(n) => n.add_to_model(network), - Node::Placeholder(n) => n.add_to_model(), - Node::Abstraction(n) => n.add_to_model(network), + Node::Input(n) => n.input_connectors(slot), + Node::Link(n) => n.input_connectors(slot), + Node::Output(n) => n.input_connectors(slot), + Node::Storage(n) => n.input_connectors(slot), + Node::Catchment(n) => n.input_connectors(slot), + Node::RiverGauge(n) => n.input_connectors(slot), + Node::LossLink(n) => n.input_connectors(slot), + Node::River(n) => n.input_connectors(slot), + Node::RiverSplitWithGauge(n) => n.input_connectors(slot), + Node::WaterTreatmentWorks(n) => n.input_connectors(slot), + Node::PiecewiseLink(n) => n.input_connectors(slot), + Node::PiecewiseStorage(n) => n.input_connectors(slot), + Node::Delay(n) => n.input_connectors(slot), + Node::Turbine(n) => n.input_connectors(slot), + Node::Reservoir(n) => n.input_connectors(slot), + // Deliberately do not take a slot for Placeholder nodes so they can be used with any slot + Node::Placeholder(n) => n.input_connectors(), + Node::Abstraction(n) => n.input_connectors(slot), } } - /// Get the node indices for flow constraints that this node has added to the network. + /// Get the output connectors for this node. + /// + /// The `slot` argument is used for nodes that have multiple output connectors. If the node + /// does not have multiple output connectors, then a [`SchemaError`] is returned. + /// + /// The output connectors are returned as a vector of unresolved node names. + /// + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + match self { + Node::Input(n) => n.output_connectors(slot), + Node::Link(n) => n.output_connectors(slot), + Node::Output(n) => n.output_connectors(slot), + Node::Storage(n) => n.output_connectors(slot), + Node::Catchment(n) => n.output_connectors(slot), + Node::RiverGauge(n) => n.output_connectors(slot), + Node::LossLink(n) => n.output_connectors(slot), + Node::River(n) => n.output_connectors(slot), + Node::RiverSplitWithGauge(n) => n.output_connectors(slot), + Node::WaterTreatmentWorks(n) => n.output_connectors(slot), + Node::PiecewiseLink(n) => n.output_connectors(slot), + Node::PiecewiseStorage(n) => n.output_connectors(slot), + Node::Delay(n) => n.output_connectors(slot), + Node::Turbine(n) => n.output_connectors(slot), + Node::Reservoir(n) => n.output_connectors(slot), + // Deliberately do not take a slot for Placeholder nodes so they can be used with any slot + Node::Placeholder(n) => n.output_connectors(), + Node::Abstraction(n) => n.output_connectors(slot), + } + } + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { + match self { + Node::Input(n) => n.add_to_network(network, args), + Node::Link(n) => n.add_to_network(network, args), + Node::Output(n) => n.add_to_network(network, args), + Node::Storage(n) => n.add_to_network(network, args), + Node::Catchment(n) => n.add_to_model(network, args), + Node::RiverGauge(n) => n.add_to_network(network, args), + Node::LossLink(n) => n.add_to_network(network, args), + Node::River(n) => n.add_to_network(network, args), + Node::RiverSplitWithGauge(n) => n.add_to_network(network, args), + Node::WaterTreatmentWorks(n) => n.add_to_network(network, args), + Node::PiecewiseLink(n) => n.add_to_network(network, args), + Node::PiecewiseStorage(n) => n.add_to_network(network, args), + Node::Delay(n) => n.add_to_model(network, args), + Node::Turbine(n) => n.add_to_network(network, args), + Node::Reservoir(n) => n.add_to_network(network, args), + Node::Placeholder(n) => n.add_to_network(), + Node::Abstraction(n) => n.add_to_network(network, args), + } + } + + /// Get the nodes for flow constraints that this node has added to the network. /// /// This is used to determine which core nodes should be used when this node is used /// in a flow constraint. Depending on the node type, this may return multiple @@ -555,47 +553,43 @@ impl Node { /// to determine which component of the node is used in the flow constraint. /// /// If the node is not allowed in flow constraints it should return [`SchemaError::NodeNotAllowedInFlowConstraint`]. - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { match self { - Node::Input(n) => n.node_indices_for_flow_constraints(network, component), - Node::Link(n) => n.node_indices_for_flow_constraints(network, component), - Node::Output(n) => n.node_indices_for_flow_constraints(network, component), + Node::Input(n) => n.nodes_for_flow_constraints(component), + Node::Link(n) => n.nodes_for_flow_constraints(component), + Node::Output(n) => n.nodes_for_flow_constraints(component), Node::Storage(_) => Err(SchemaError::NodeNotAllowedInFlowConstraint), - Node::Catchment(n) => n.node_indices_for_flow_constraints(network, component), - Node::RiverGauge(n) => n.node_indices_for_flow_constraints(network, component), - Node::LossLink(n) => n.node_indices_for_flow_constraints(network, component), - Node::River(n) => n.node_indices_for_flow_constraints(network, component), - Node::RiverSplitWithGauge(n) => n.node_indices_for_flow_constraints(network, component), - Node::WaterTreatmentWorks(n) => n.node_indices_for_flow_constraints(network, component), - Node::PiecewiseLink(n) => n.node_indices_for_flow_constraints(network, component), + Node::Catchment(n) => n.nodes_for_flow_constraints(component), + Node::RiverGauge(n) => n.nodes_for_flow_constraints(component), + Node::LossLink(n) => n.nodes_for_flow_constraints(component), + Node::River(n) => n.nodes_for_flow_constraints(component), + Node::RiverSplitWithGauge(n) => n.nodes_for_flow_constraints(component), + Node::WaterTreatmentWorks(n) => n.nodes_for_flow_constraints(component), + Node::PiecewiseLink(n) => n.nodes_for_flow_constraints(component), Node::PiecewiseStorage(_) => Err(SchemaError::NodeNotAllowedInFlowConstraint), - Node::Delay(n) => n.node_indices_for_flow_constraints(network, component), - Node::Turbine(n) => n.node_indices_for_flow_constraints(network, component), - Node::Reservoir(n) => n.node_indices_for_flow_constraints(network, component), - Node::Placeholder(n) => n.node_indices_for_flow_constraints(), - Node::Abstraction(n) => n.node_indices_for_flow_constraints(network, component), + Node::Delay(n) => n.nodes_for_flow_constraints(component), + Node::Turbine(n) => n.nodes_for_flow_constraints(component), + Node::Reservoir(n) => n.nodes_for_flow_constraints(component), + Node::Placeholder(n) => n.nodes_for_flow_constraints(), + Node::Abstraction(n) => n.nodes_for_flow_constraints(component), } } - /// Get the node indices for storage nodes for this node. + /// Get the nodes for storage nodes for this node. /// /// This is used to determine which core nodes should be used when this node is used /// in an [`AggregatedStorageNode`]. /// /// If the node is not allowed in storage constraints it should return [`SchemaError::NodeNotAllowedInStorageConstraint`]. - pub fn node_indices_for_storage_constraints( - &self, - network: &pywr_core::network::Network, - ) -> Result, SchemaError> { + pub fn nodes_for_storage_constraints(&self) -> Result, SchemaError> { match self { Node::Input(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::Link(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::Output(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), - Node::Storage(n) => n.node_indices_for_storage_constraints(network), + Node::Storage(n) => n.nodes_for_storage_constraints(), Node::Catchment(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::RiverGauge(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::LossLink(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), @@ -603,65 +597,39 @@ impl Node { Node::RiverSplitWithGauge(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::WaterTreatmentWorks(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::PiecewiseLink(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), - Node::PiecewiseStorage(n) => n.node_indices_for_storage_constraints(network), + Node::PiecewiseStorage(n) => n.nodes_for_storage_constraints(), Node::Delay(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), Node::Turbine(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), - Node::Reservoir(n) => n.node_indices_for_storage_constraints(network), - Node::Placeholder(n) => n.node_indices_for_storage_constraints(), + Node::Reservoir(n) => n.node_indices_for_storage_constraints(), + Node::Placeholder(n) => n.nodes_for_storage_constraints(), Node::Abstraction(_) => Err(SchemaError::NodeNotAllowedInStorageConstraint), } } - pub fn set_constraints( - &self, - network: &mut pywr_core::network::Network, - args: &LoadArgs, - ) -> Result<(), SchemaError> { - match self { - Node::Input(n) => n.set_constraints(network, args), - Node::Link(n) => n.set_constraints(network, args), - Node::Output(n) => n.set_constraints(network, args), - Node::Storage(n) => n.set_constraints(network, args), - Node::Catchment(n) => n.set_constraints(network, args), - Node::RiverGauge(n) => n.set_constraints(network, args), - Node::LossLink(n) => n.set_constraints(network, args), - Node::River(n) => n.set_constraints(network, args), - Node::RiverSplitWithGauge(n) => n.set_constraints(network, args), - Node::WaterTreatmentWorks(n) => n.set_constraints(network, args), - Node::PiecewiseLink(n) => n.set_constraints(network, args), - Node::PiecewiseStorage(n) => n.set_constraints(network, args), - Node::Delay(n) => n.set_constraints(network, args), - Node::Turbine(n) => n.set_constraints(network, args), - Node::Reservoir(n) => n.set_constraints(network, args), - Node::Placeholder(n) => n.set_constraints(), - Node::Abstraction(n) => n.set_constraints(network, args), - } - } - /// Create a metric for the given attribute on this node. pub fn create_metric( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, attribute: Option, - ) -> Result { + ) -> Result { match self { - Node::Input(n) => n.create_metric(network, attribute), - Node::Link(n) => n.create_metric(network, attribute), + Node::Input(n) => n.create_metric(attribute), + Node::Link(n) => n.create_metric(attribute), Node::Output(n) => n.create_metric(network, attribute), - Node::Storage(n) => n.create_metric(network, attribute), - Node::Catchment(n) => n.create_metric(network, attribute), - Node::RiverGauge(n) => n.create_metric(network, attribute), - Node::LossLink(n) => n.create_metric(network, attribute), - Node::River(n) => n.create_metric(network, attribute), - Node::RiverSplitWithGauge(n) => n.create_metric(network, attribute), - Node::WaterTreatmentWorks(n) => n.create_metric(network, attribute), - Node::PiecewiseLink(n) => n.create_metric(network, attribute), - Node::PiecewiseStorage(n) => n.create_metric(network, attribute), - Node::Delay(n) => n.create_metric(network, attribute), - Node::Turbine(n) => n.create_metric(network, attribute), - Node::Reservoir(n) => n.create_metric(network, attribute), + Node::Storage(n) => n.create_metric(attribute), + Node::Catchment(n) => n.create_metric(attribute), + Node::RiverGauge(n) => n.create_metric(attribute), + Node::LossLink(n) => n.create_metric(attribute), + Node::River(n) => n.create_metric(attribute), + Node::RiverSplitWithGauge(n) => n.create_metric(attribute), + Node::WaterTreatmentWorks(n) => n.create_metric(attribute), + Node::PiecewiseLink(n) => n.create_metric(attribute), + Node::PiecewiseStorage(n) => n.create_metric(attribute), + Node::Delay(n) => n.create_metric(attribute), + Node::Turbine(n) => n.create_metric(attribute), + Node::Reservoir(n) => n.create_metric(attribute), Node::Placeholder(n) => n.create_metric(), - Node::Abstraction(n) => n.create_metric(network, attribute), + Node::Abstraction(n) => n.create_metric(attribute), } } } @@ -695,10 +663,14 @@ pub enum NodeOrVirtualNode { #[cfg(feature = "core")] impl NodeOrVirtualNode { - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { + pub fn add_to_model( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { match self { - NodeOrVirtualNode::Node(n) => n.add_to_model(network), - NodeOrVirtualNode::Virtual(n) => n.add_to_model(network, args), + NodeOrVirtualNode::Node(n) => n.add_to_network(network, args), + NodeOrVirtualNode::Virtual(n) => n.add_to_network(network, args), } } } diff --git a/pywr-schema/src/nodes/piecewise_link.rs b/pywr-schema/src/nodes/piecewise_link.rs index 9fe1b2a7..96127b29 100644 --- a/pywr-schema/src/nodes/piecewise_link.rs +++ b/pywr-schema/src/nodes/piecewise_link.rs @@ -1,16 +1,17 @@ use crate::error::ComponentConversionError; -use crate::error::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; +use crate::nodes::NodeMeta; use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_node_attr, try_convert_node_meta}; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::PiecewiseLinkNode as PiecewiseLinkNodeV1; @@ -66,11 +67,22 @@ impl PiecewiseLinkNode { const DEFAULT_ATTRIBUTE: PiecewiseLinkNodeAttribute = PiecewiseLinkNodeAttribute::Outflow; const DEFAULT_COMPONENT: PiecewiseLinkNodeComponent = PiecewiseLinkNodeComponent::Outflow; - fn step_sub_name(i: usize) -> Option { - Some(format!("step-{i:02}")) + pub fn default_attribute(&self) -> PiecewiseLinkNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } + + pub fn default_component(&self) -> PiecewiseLinkNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl PiecewiseLinkNode { + fn step_sub_name(&self, i: usize) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some(&format!("step-{i:02}"))) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { @@ -78,11 +90,11 @@ impl PiecewiseLinkNode { .steps .iter() .enumerate() - .map(|(i, _)| (self.meta.name.as_str(), Self::step_sub_name(i))) + .map(|(i, _)| self.step_sub_name(i)) .collect()) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { @@ -90,116 +102,81 @@ impl PiecewiseLinkNode { .steps .iter() .enumerate() - .map(|(i, _)| (self.meta.name.as_str(), Self::step_sub_name(i))) + .map(|(i, _)| self.step_sub_name(i)) .collect()) } } - - pub fn default_attribute(&self) -> PiecewiseLinkNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> PiecewiseLinkNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl PiecewiseLinkNode { - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let indices = match component { + let nodes = match component { PiecewiseLinkNodeComponent::Inflow | PiecewiseLinkNodeComponent::Outflow => self .steps .iter() .enumerate() - .map(|(i, _)| { - network - .get_node_index_by_name(self.meta.name.as_str(), Self::step_sub_name(i).as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::step_sub_name(i), - }) - }) - .collect::, _>>()?, + .map(|(i, _)| self.step_sub_name(i)) + .collect::>(), }; - Ok(indices) + Ok(nodes) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - // create a link node for each step - for (i, _) in self.steps.iter().enumerate() { - network.add_link_node(self.meta.name.as_str(), Self::step_sub_name(i).as_deref())?; - } - Ok(()) - } - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + // create a link node for each step for (i, step) in self.steps.iter().enumerate() { - let sub_name = Self::step_sub_name(i); + let mut link_node = pywr_core::node::NodeBuilder::link(self.step_sub_name(i)); if let Some(cost) = &step.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), sub_name.as_deref(), value.into())?; + link_node.cost(value); } if let Some(max_flow) = &step.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), sub_name.as_deref(), value.into())?; + link_node.max_flow(value); } if let Some(min_flow) = &step.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), sub_name.as_deref(), value.into())?; + link_node.min_flow(value); } - } + network.node(link_node); + } Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let indices = self + let nodes = self .steps .iter() .enumerate() - .map(|(i, _)| { - network - .get_node_index_by_name(self.meta.name.as_str(), Self::step_sub_name(i).as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::step_sub_name(i), - }) - }) - .collect::, _>>()?; + .map(|(i, _)| self.step_sub_name(i)) + .collect::>(); let metric = match attr { - PiecewiseLinkNodeAttribute::Inflow => MetricF64::MultiNodeInFlow { - indices, + PiecewiseLinkNodeAttribute::Inflow => UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), }, - PiecewiseLinkNodeAttribute::Outflow => MetricF64::MultiNodeOutFlow { - indices, + PiecewiseLinkNodeAttribute::Outflow => UnresolvedMetricF64::MultiNodeOutFlow { + nodes, name: self.meta.name.to_string(), }, }; diff --git a/pywr-schema/src/nodes/piecewise_storage.rs b/pywr-schema/src/nodes/piecewise_storage.rs index ed9b99a8..8a5b8829 100644 --- a/pywr-schema/src/nodes/piecewise_storage.rs +++ b/pywr-schema/src/nodes/piecewise_storage.rs @@ -1,16 +1,19 @@ -use crate::error::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::NodeAttribute; -use crate::nodes::{NodeMeta, NodeSlot, StorageInitialVolume}; +use crate::nodes::{NodeMeta, StorageInitialVolume}; use crate::parameters::Parameter; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum}; #[cfg(feature = "core")] use pywr_core::{ - metric::{MetricF64, SimpleMetricF64}, - parameters::{DifferenceParameter, ParameterIndex, ParameterName, VolumeBetweenControlCurvesParameter}, + metric::UnresolvedMetricF64, + node::{UnresolvedNode, UnresolvedStorageInitialVolume}, + parameters::ParameterName, + parameters::{DifferenceParameterBuilder, VolumeBetweenControlCurvesParameterBuilder}, }; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use schemars::JsonSchema; @@ -68,123 +71,68 @@ pub struct PiecewiseStorageNode { impl PiecewiseStorageNode { const DEFAULT_ATTRIBUTE: PiecewiseStorageNodeAttribute = PiecewiseStorageNodeAttribute::Volume; - fn step_sub_name(i: usize) -> Option { - Some(format!("store-{i:02}")) + pub fn default_attribute(&self) -> PiecewiseStorageNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } +} + +#[cfg(feature = "core")] +impl PiecewiseStorageNode { + fn step_sub_name(&self, i: usize) -> UnresolvedNode { + UnresolvedNode::new(self.meta.name.as_str(), Some(&format!("store-{i:02}"))) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![(self.meta.name.as_str(), Self::step_sub_name(self.steps.len()))]) + Ok((0..self.steps.len()).map(|i| self.step_sub_name(i)).collect()) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![(self.meta.name.as_str(), Self::step_sub_name(self.steps.len()))]) + Ok((0..self.steps.len()).map(|i| self.step_sub_name(i)).collect()) } } - - pub fn default_attribute(&self) -> PiecewiseStorageNodeAttribute { - Self::DEFAULT_ATTRIBUTE + fn agg_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("agg-store")) } -} -#[cfg(feature = "core")] -impl PiecewiseStorageNode { - fn agg_sub_name() -> Option<&'static str> { - Some("agg-store") - } - - pub fn node_indices_for_storage_constraints( - &self, - network: &pywr_core::network::Network, - ) -> Result, SchemaError> { + pub fn nodes_for_storage_constraints(&self) -> Result, SchemaError> { // Get the indices of all the sub-nodes for this piecewise storage node (including // the final one that represents the residual part above the last step). - let indices = (0..self.steps.len() + 1) - .map(|i| { - network - .get_node_index_by_name(self.meta.name.as_str(), Self::step_sub_name(i).as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::step_sub_name(i), - }) - }) - .collect::, _>>()?; - - Ok(indices) - } - - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - let mut store_node_indices = Vec::new(); - - // create a storage node for each step - for (i, _step) in self.steps.iter().enumerate() { - // Assume each store is full to start with - let initial_volume = pywr_core::node::StorageInitialVolume::Proportional(1.0); - - let idx = network.add_storage_node( - self.meta.name.as_str(), - Self::step_sub_name(i).as_deref(), - initial_volume, - None, - None, - )?; - - if let Some(prev_idx) = store_node_indices.last() { - // There was a lower store; connect to it in both directions - network.connect_nodes(idx, *prev_idx)?; - network.connect_nodes(*prev_idx, idx)?; - } - - store_node_indices.push(idx); - } - - // Assume each store is full to start with - let initial_volume = pywr_core::node::StorageInitialVolume::Proportional(1.0); + let nodes = (0..self.steps.len() + 1) + .map(|i| self.step_sub_name(i)) + .collect::>(); - // And one for the residual part above the less step - let idx = network.add_storage_node( - self.meta.name.as_str(), - Self::step_sub_name(self.steps.len()).as_deref(), - initial_volume, - None, - None, - )?; - - if let Some(prev_idx) = store_node_indices.last() { - // There was a lower store; connect to it in both directions - network.connect_nodes(idx, *prev_idx)?; - network.connect_nodes(*prev_idx, idx)?; - } - - store_node_indices.push(idx); - - // Finally, add an aggregate storage node covering all the individual stores - network.add_aggregated_storage_node(self.meta.name.as_str(), Self::agg_sub_name(), store_node_indices)?; - - Ok(()) + Ok(nodes) } - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut store_nodes: Vec = Vec::new(); + // These are the min and max volume of the overall node - let total_volume: SimpleMetricF64 = self.max_volume.load(network, args, Some(&self.meta.name))?.try_into()?; - let total_min_volume: Option = match &self.min_volume { - Some(min_volume) => Some(min_volume.load(network, args, Some(&self.meta.name))?.try_into()?), + let total_volume: UnresolvedMetricF64 = self.max_volume.load(network, args, Some(&self.meta.name))?; + let total_min_volume: Option = match &self.min_volume { + Some(min_volume) => Some(min_volume.load(network, args, Some(&self.meta.name))?), None => None, }; - let mut prior_max_volumes: Vec = Vec::new(); + let mut prior_max_volumes: Vec = Vec::new(); for (i, step) in self.steps.iter().enumerate() { - let sub_name = Self::step_sub_name(i); + // Create a storage node builder for each step + let mut storage = pywr_core::node::NodeBuilder::storage(self.step_sub_name(i)); + + // Assume each store is full to start with + let initial_volume = UnresolvedStorageInitialVolume::Proportional(1.0); + storage.initial_volume(initial_volume); // The volume of this step is the proportion between the last control curve // (or zero if first) and this control curve. @@ -192,8 +140,7 @@ impl PiecewiseStorageNode { Some( self.steps[i - 1] .control_curve - .load(network, args, Some(&self.meta.name))? - .try_into()?, + .load(network, args, Some(&self.meta.name))?, ) } else { None @@ -201,86 +148,141 @@ impl PiecewiseStorageNode { let upper = step.control_curve.load(network, args, Some(&self.meta.name))?; - let max_volume_parameter_idx = - self.set_sub_node_max_volume(i, total_volume.clone(), Some(upper.try_into()?), lower, network)?; + // Set the max volume + let max_volume_metric = self.set_sub_node_max_volume( + i, + total_volume.clone(), + Some(upper), + lower.clone(), + network, + &mut storage, + ); - let prior_max_volume = pywr_core::parameters::AggregatedParameter::new( - ParameterName::new( - format!("{}-prior-max-volume", Self::step_sub_name(i).unwrap()).as_str(), - Some(&self.meta.name), - ), - &prior_max_volumes, + let prior_max_volume_name = + ParameterName::new(&format!("store-{i:02}-prior-max-volume"), Some(&self.meta.name)); + let mut prior_max_volume = pywr_core::parameters::AggregatedParameterBuilder::new( + prior_max_volume_name.clone(), pywr_core::agg_funcs::AggFuncF64::Sum, ); - let prior_max_volume_idx = network.add_simple_parameter(Box::new(prior_max_volume))?; + let prior_max_volume_metric = UnresolvedMetricF64::new_parameter_before(prior_max_volume_name.clone()); + + // Add all the prior maxs to the aggregated node + for pmv in &prior_max_volumes { + prior_max_volume.metric(pmv.clone()); + } + network.parameters().f64(Box::new(prior_max_volume)); if let Some(total_min_volume) = total_min_volume.clone() { self.set_sub_node_min_volume( i, total_min_volume.clone(), - prior_max_volume_idx, - max_volume_parameter_idx, + prior_max_volume_metric.clone(), + max_volume_metric.clone(), network, - )?; + &mut storage, + ); } - self.set_sub_node_initial_volume(i, total_volume.clone(), prior_max_volume_idx, network)?; + self.set_sub_node_initial_volume(total_volume.clone(), prior_max_volume_metric, &mut storage); + + // Append the max volume parameter of this node to the prior list + prior_max_volumes.push(max_volume_metric); if let Some(cost) = &step.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), sub_name.as_deref(), value.into())?; + storage.cost(value); } - // Append the max volume parameter of this node to the prior list - prior_max_volumes.push(max_volume_parameter_idx.try_into()?); + let name = storage.name(); + + if let Some(prev_name) = store_nodes.last() { + // There was a lower store; connect to it in both directions + network.connect(name.clone(), prev_name.clone()); + network.connect(prev_name.clone(), name.clone()); + } + + store_nodes.push(name.clone()); + network.node(storage); } + // Assume each store is full to start with + let initial_volume = UnresolvedStorageInitialVolume::Proportional(1.0); + + // And one for the residual part above the less step + let mut storage = pywr_core::node::NodeBuilder::storage(self.step_sub_name(self.steps.len())); + storage.initial_volume(initial_volume); + // The volume of this store is the remain proportion above the last control curve let lower = match self.steps.last() { - Some(step) => Some( - step.control_curve - .load(network, args, Some(&self.meta.name))? - .try_into()?, - ), + Some(step) => Some(step.control_curve.load(network, args, Some(&self.meta.name))?), None => None, }; let upper = None; - let max_volume_parameter_idx = - self.set_sub_node_max_volume(self.steps.len(), total_volume.clone(), upper, lower, network)?; + let max_volume_parameter_metric = self.set_sub_node_max_volume( + self.steps.len(), + total_volume.clone(), + upper, + lower, + network, + &mut storage, + ); + + let prior_max_volume_name = ParameterName::new( + &format!("store-{:02}-prior-max-volume", self.steps.len()), + Some(&self.meta.name), + ); - let prior_max_volume = pywr_core::parameters::AggregatedParameter::new( - ParameterName::new( - format!("{}-prior-max-volume", Self::step_sub_name(self.steps.len()).unwrap()).as_str(), - Some(&self.meta.name), - ), - &prior_max_volumes, + let mut prior_max_volume = pywr_core::parameters::AggregatedParameterBuilder::new( + prior_max_volume_name.clone(), pywr_core::agg_funcs::AggFuncF64::Sum, ); - let prior_max_volume_idx = network.add_simple_parameter(Box::new(prior_max_volume))?; + + for pmv in &prior_max_volumes { + prior_max_volume.metric(pmv.clone()); + } + let prior_max_volume_metric = UnresolvedMetricF64::new_parameter_before(prior_max_volume_name.clone()); + + network.parameters().f64(Box::new(prior_max_volume)); if let Some(total_min_volume) = total_min_volume.clone() { self.set_sub_node_min_volume( self.steps.len(), total_min_volume.clone(), - prior_max_volume_idx, - max_volume_parameter_idx, + prior_max_volume_metric.clone(), + max_volume_parameter_metric, network, - )?; + &mut storage, + ); } - self.set_sub_node_initial_volume(self.steps.len(), total_volume.clone(), prior_max_volume_idx, network)?; + self.set_sub_node_initial_volume(total_volume.clone(), prior_max_volume_metric, &mut storage); // Set the cost for the last step if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost( - self.meta.name.as_str(), - Self::step_sub_name(self.steps.len()).as_deref(), - value.into(), - )?; + storage.cost(value); + } + + let name = storage.name(); + + if let Some(prev_name) = store_nodes.last() { + // There was a lower store; connect to it in both directions + network.connect(name.clone(), prev_name.clone()); + network.connect(prev_name.clone(), name.clone()); + } + + store_nodes.push(name.clone()); + network.node(storage); + + // Finally, add an aggregate storage node covering all the individual stores + let mut agg_storage = pywr_core::AggregatedStorageNodeBuilder::new(self.agg_sub_name()); + + for node in store_nodes { + agg_storage.node(node); } + network.agg_storage_node(agg_storage); Ok(()) } @@ -294,28 +296,32 @@ impl PiecewiseStorageNode { fn set_sub_node_max_volume( &self, step_index: usize, - total_volume: SimpleMetricF64, - upper: Option, - lower: Option, - network: &mut pywr_core::network::Network, - ) -> Result, SchemaError> { - let sub_name = Self::step_sub_name(step_index); - - let max_volume_parameter = VolumeBetweenControlCurvesParameter::new( + total_volume: UnresolvedMetricF64, + upper: Option, + lower: Option, + network: &mut pywr_core::network::NetworkBuilder, + node: &mut pywr_core::node::NodeBuilder, + ) -> UnresolvedMetricF64 { + let max_volume_name = ParameterName::new(&format!("store-{:02}-max-volume", step_index), Some(&self.meta.name)); + + let mut max_volume_parameter = VolumeBetweenControlCurvesParameterBuilder::new( // Node's name is the parent identifier - ParameterName::new( - format!("{}-max-volume", Self::step_sub_name(step_index).unwrap()).as_str(), - Some(&self.meta.name), - ), - total_volume.clone(), - upper, - lower, + max_volume_name.clone(), + total_volume, ); - let max_volume_parameter_idx = network.add_simple_parameter(Box::new(max_volume_parameter))?; - let max_volume = Some(max_volume_parameter_idx.try_into()?); - network.set_node_max_volume(self.meta.name.as_str(), sub_name.as_deref(), max_volume)?; - Ok(max_volume_parameter_idx) + if let Some(upper) = upper { + max_volume_parameter.upper(upper); + } + if let Some(lower) = lower { + max_volume_parameter.lower(lower); + } + + // Add the parameter and link it to the node's max volume. + network.parameters().f64(Box::new(max_volume_parameter)); + let max_volume = UnresolvedMetricF64::new_parameter_before(max_volume_name); + node.max_volume(max_volume.clone()); + max_volume } /// Set the minimum volume of the node at a specific step index. @@ -326,83 +332,63 @@ impl PiecewiseStorageNode { fn set_sub_node_min_volume( &self, step_index: usize, - total_min_volume: SimpleMetricF64, - prior_max_volume_idx: ParameterIndex, - max_volume_parameter_idx: ParameterIndex, - network: &mut pywr_core::network::Network, - ) -> Result<(), SchemaError> { - let sub_name = Self::step_sub_name(step_index); + total_min_volume: UnresolvedMetricF64, + prior_max_volume: UnresolvedMetricF64, + max_volume_parameter: UnresolvedMetricF64, + network: &mut pywr_core::network::NetworkBuilder, + node: &mut pywr_core::node::NodeBuilder, + ) { + let min_volume_name = ParameterName::new(&format!("store-{:02}-min-volume", step_index), Some(&self.meta.name)); // The minimum volume is the difference between the total volume and // the maximum volume of the previous steps, but limited to be between zero and the maximum volume of this step. - let min_volume_parameter = DifferenceParameter::new( - ParameterName::new( - format!("{}-min-volume", Self::step_sub_name(step_index).unwrap()).as_str(), - Some(&self.meta.name), - ), - total_min_volume, - prior_max_volume_idx.try_into()?, - Some(0.0.into()), - Some(max_volume_parameter_idx.try_into()?), - ); - let min_volume_parameter_idx = network.add_simple_parameter(Box::new(min_volume_parameter))?; - network.set_node_min_volume( - self.meta.name.as_str(), - sub_name.as_deref(), - Some(min_volume_parameter_idx.try_into()?), - )?; + let mut min_volume_parameter = + DifferenceParameterBuilder::new(min_volume_name.clone(), total_min_volume, prior_max_volume.clone()); - Ok(()) + min_volume_parameter.min(0.0.into()).max(max_volume_parameter); + + network.parameters().f64(Box::new(min_volume_parameter)); + node.min_volume(UnresolvedMetricF64::new_parameter_before(min_volume_name)); } /// Set the initial volume of the node at a specific step index. fn set_sub_node_initial_volume( &self, - step_index: usize, - total_volume: SimpleMetricF64, - prior_max_volume_idx: ParameterIndex, - network: &mut pywr_core::network::Network, - ) -> Result<(), SchemaError> { - let sub_name = Self::step_sub_name(step_index); + total_volume: UnresolvedMetricF64, + prior_max_volume: UnresolvedMetricF64, + node: &mut pywr_core::node::NodeBuilder, + ) { // Set the initial volume of this step let initial_volume = match &self.initial_volume { StorageInitialVolume::Proportional { proportion } => { - pywr_core::node::StorageInitialVolume::DistributedProportional { + UnresolvedStorageInitialVolume::DistributedProportional { total_volume: total_volume.clone(), proportion: *proportion, - prior_max_volume: prior_max_volume_idx.try_into()?, + prior_max_volume, } } - StorageInitialVolume::Absolute { volume } => pywr_core::node::StorageInitialVolume::DistributedAbsolute { + StorageInitialVolume::Absolute { volume } => UnresolvedStorageInitialVolume::DistributedAbsolute { absolute: *volume, - prior_max_volume: prior_max_volume_idx.try_into()?, + prior_max_volume, }, }; - network.set_node_initial_volume(self.meta.name.as_str(), sub_name.as_deref(), initial_volume)?; - Ok(()) + node.initial_volume(initial_volume); } - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_aggregated_storage_node_index_by_name(self.meta.name.as_str(), Self::agg_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::agg_sub_name().map(String::from), - })?; + let name = self.agg_sub_name(); let metric = match attr { - PiecewiseStorageNodeAttribute::Volume => MetricF64::AggregatedStorageNodeVolume(idx), - PiecewiseStorageNodeAttribute::ProportionalVolume => MetricF64::AggregatedNodeProportionalVolume(idx), + PiecewiseStorageNodeAttribute::Volume => UnresolvedMetricF64::AggregatedStorageNodeVolume(name), + PiecewiseStorageNodeAttribute::ProportionalVolume => { + UnresolvedMetricF64::AggregatedStorageNodeProportionalVolume(name) + } }; Ok(metric) diff --git a/pywr-schema/src/nodes/placeholder.rs b/pywr-schema/src/nodes/placeholder.rs index 8cbbb32a..d5a68985 100644 --- a/pywr-schema/src/nodes/placeholder.rs +++ b/pywr-schema/src/nodes/placeholder.rs @@ -1,5 +1,8 @@ +#[cfg(feature = "core")] use crate::SchemaError; use crate::nodes::{NodeAttribute, NodeMeta}; +#[cfg(feature = "core")] +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::PywrVisitAll; use schemars::JsonSchema; @@ -17,14 +20,6 @@ pub struct PlaceholderNode { } impl PlaceholderNode { - pub fn input_connectors(&self) -> Result)>, SchemaError> { - Ok(vec![(self.meta.name.as_str(), None)]) - } - - pub fn output_connectors(&self) -> Result)>, SchemaError> { - Ok(vec![(self.meta.name.as_str(), None)]) - } - pub fn default_attribute(&self) -> NodeAttribute { NodeAttribute::Outflow } @@ -32,31 +27,32 @@ impl PlaceholderNode { #[cfg(feature = "core")] impl PlaceholderNode { - pub fn add_to_model(&self) -> Result<(), SchemaError> { - Err(SchemaError::PlaceholderNodeNotAllowed { - name: self.meta.name.clone(), - }) + pub fn input_connectors(&self) -> Result, SchemaError> { + Ok(vec![self.meta.name.as_str().into()]) } - pub fn node_indices_for_flow_constraints(&self) -> Result, SchemaError> { + pub fn output_connectors(&self) -> Result, SchemaError> { + Ok(vec![self.meta.name.as_str().into()]) + } + pub fn add_to_network(&self) -> Result<(), SchemaError> { Err(SchemaError::PlaceholderNodeNotAllowed { name: self.meta.name.clone(), }) } - pub fn node_indices_for_storage_constraints(&self) -> Result, SchemaError> { + pub fn nodes_for_flow_constraints(&self) -> Result, SchemaError> { Err(SchemaError::PlaceholderNodeNotAllowed { name: self.meta.name.clone(), }) } - pub fn set_constraints(&self) -> Result<(), SchemaError> { + pub fn nodes_for_storage_constraints(&self) -> Result, SchemaError> { Err(SchemaError::PlaceholderNodeNotAllowed { name: self.meta.name.clone(), }) } - pub fn create_metric(&self) -> Result { + pub fn create_metric(&self) -> Result { Err(SchemaError::PlaceholderNodeNotAllowed { name: self.meta.name.clone(), }) @@ -79,7 +75,7 @@ mod test { }; // Attempt to add the placeholder node to a model - let result = placeholder.add_to_model(); + let result = placeholder.add_to_network(); assert!(result.is_err()); assert!(matches!(result, Err(SchemaError::PlaceholderNodeNotAllowed { .. }))); } diff --git a/pywr-schema/src/nodes/reservoir.rs b/pywr-schema/src/nodes/reservoir.rs index 4c5b7ce0..bc41509f 100644 --- a/pywr-schema/src/nodes/reservoir.rs +++ b/pywr-schema/src/nodes/reservoir.rs @@ -8,15 +8,7 @@ use crate::parameters::ConstantFloatVec; use crate::{SchemaError, mermaid}; use crate::{node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::agg_funcs::AggFuncF64; -#[cfg(feature = "core")] -use pywr_core::metric::ConstantMetricF64::Constant; -#[cfg(feature = "core")] -use pywr_core::metric::MetricF64; -#[cfg(feature = "core")] -use pywr_core::metric::SimpleMetricF64; -#[cfg(feature = "core")] -use pywr_core::parameters::ParameterName; +use pywr_core::{agg_funcs::AggFuncF64, metric::UnresolvedMetricF64, node::UnresolvedNode, parameters::ParameterName}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use schemars::JsonSchema; @@ -248,32 +240,52 @@ pub struct ReservoirNode { impl ReservoirNode { pub const DEFAULT_COMPONENT: ReservoirNodeComponent = ReservoirNodeComponent::Compensation; - const DEFAULT_OUTPUT_SLOT: ReservoirOutputNodeSlot = ReservoirOutputNodeSlot::Storage; /// Get the node's metadata. pub(crate) fn meta(&self) -> &NodeMeta { &self.storage.meta } + pub fn iter_output_slots(&self) -> impl Iterator + '_ { + [ + ReservoirOutputNodeSlot::Storage.into(), + ReservoirOutputNodeSlot::Compensation.into(), + ReservoirOutputNodeSlot::Spill.into(), + ] + .into_iter() + } + + pub fn default_attribute(&self) -> ReservoirNodeAttribute { + self.storage.default_attribute().into() + } + + pub fn default_component(&self) -> ReservoirNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl ReservoirNode { + const DEFAULT_OUTPUT_SLOT: ReservoirOutputNodeSlot = ReservoirOutputNodeSlot::Storage; /// The sub-name of the compensation link node. - fn compensation_node_sub_name() -> Option<&'static str> { - Some("compensation") + fn compensation_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.storage.meta.name, Some("compensation")) } /// The sub-name of the spill output node. - fn spill_node_sub_name() -> Option<&'static str> { - Some("spill") + fn spill_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.storage.meta.name, Some("spill")) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![(self.meta().name.as_str(), None)]) + Ok(vec![self.meta().name.as_str().into()]) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { // Use the default slot if none is specified let slot = match slot { Some(c) => c.clone().try_into()?, @@ -282,19 +294,15 @@ impl ReservoirNode { let indices = match slot { ReservoirOutputNodeSlot::Storage => { - vec![(self.meta().name.as_str(), None)] + vec![self.meta().name.as_str().into()] } ReservoirOutputNodeSlot::Compensation => { - vec![( - self.meta().name.as_str(), - Self::compensation_node_sub_name().map(|n| n.to_string()), - )] + vec![self.compensation_node_sub_name()] } ReservoirOutputNodeSlot::Spill => match self.spill { - Some(SpillNodeType::LinkNode) => vec![( - self.meta().name.as_str(), - Self::spill_node_sub_name().map(|n| n.to_string()), - )], + Some(SpillNodeType::LinkNode) => { + vec![self.spill_node_sub_name()] + } _ => { return Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.into() }); } @@ -304,114 +312,69 @@ impl ReservoirNode { Ok(indices) } - pub fn iter_output_slots(&self) -> impl Iterator + '_ { - [ - ReservoirOutputNodeSlot::Storage.into(), - ReservoirOutputNodeSlot::Compensation.into(), - ReservoirOutputNodeSlot::Spill.into(), - ] - .into_iter() - } - - pub fn default_attribute(&self) -> ReservoirNodeAttribute { - self.storage.default_attribute().into() - } - - pub fn default_component(&self) -> ReservoirNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl ReservoirNode { /// The sub-name of the rainfall node. - fn rainfall_node_sub_name() -> Option<&'static str> { - Some("rainfall") + fn rainfall_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.storage.meta.name, Some("rainfall")) } /// The sub-name of the evaporation node. - fn evaporation_node_sub_name() -> Option<&'static str> { - Some("evaporation") + fn evaporation_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.storage.meta.name, Some("evaporation")) } /// The sub-name of the leakage node. - fn leakage_node_sub_name() -> Option<&'static str> { - Some("leakage") + fn leakage_node_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.storage.meta.name, Some("leakage")) } - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let idx = match component { - ReservoirNodeComponent::Loss => network - .get_node_index_by_name(self.meta().name.as_str(), Self::leakage_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: Self::leakage_node_sub_name().map(String::from), - })?, - ReservoirNodeComponent::Compensation => network - .get_node_index_by_name(self.meta().name.as_str(), Self::compensation_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: Self::compensation_node_sub_name().map(String::from), - })?, - ReservoirNodeComponent::Rainfall => network - .get_node_index_by_name(self.meta().name.as_str(), Self::rainfall_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: Self::rainfall_node_sub_name().map(String::from), - })?, - ReservoirNodeComponent::Evaporation => network - .get_node_index_by_name(self.meta().name.as_str(), Self::evaporation_node_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: Self::evaporation_node_sub_name().map(String::from), - })?, + let node = match component { + ReservoirNodeComponent::Loss => self.leakage_node_sub_name(), + ReservoirNodeComponent::Compensation => self.compensation_node_sub_name(), + ReservoirNodeComponent::Rainfall => self.rainfall_node_sub_name(), + ReservoirNodeComponent::Evaporation => self.evaporation_node_sub_name(), }; - Ok(vec![idx]) + Ok(vec![node]) } - pub fn node_indices_for_storage_constraints( - &self, - network: &pywr_core::network::Network, - ) -> Result, SchemaError> { - let indices = vec![ - network - .get_node_index_by_name(self.meta().name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: Self::compensation_node_sub_name().map(String::from), - })?, - ]; - Ok(indices) + pub fn node_indices_for_storage_constraints(&self) -> Result, SchemaError> { + let nodes = vec![UnresolvedNode::new(self.meta().name.as_str(), None)]; + Ok(nodes) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - // add storage and spill - self.storage.add_to_model(network)?; - let storage = network - .get_node_index_by_name(self.meta().name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: Self::evaporation_node_sub_name().map(String::from), - })?; + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { + // Add storage node + self.storage.add_to_network(network, args)?; + // Storage node name + let storage_name = UnresolvedNode::new(self.meta().name.as_str(), None); // add compensation node and edge let comp_node = match &self.compensation { - Some(_) => { - let comp = network.add_link_node(self.meta().name.as_str(), Self::compensation_node_sub_name())?; + Some(compensation) => { + let mut comp = pywr_core::NodeBuilder::link(self.compensation_node_sub_name()); + + let value = compensation.load(network, args, Some(&self.meta().name))?; + comp.min_flow(value); + + let comp_name = comp.name().clone(); + network.connect(storage_name.clone(), comp_name.clone()); + network.node(comp); - network.connect_nodes(storage, comp)?; - Some(comp) + Some(comp_name) } None => None, }; @@ -421,16 +384,16 @@ impl ReservoirNode { None => None, Some(node_type) => { let spill = match node_type { - SpillNodeType::OutputNode => { - network.add_output_node(self.meta().name.as_str(), Self::spill_node_sub_name())? - } - SpillNodeType::LinkNode => { - network.add_link_node(self.meta().name.as_str(), Self::spill_node_sub_name())? - } + SpillNodeType::OutputNode => pywr_core::NodeBuilder::output(self.spill_node_sub_name()), + SpillNodeType::LinkNode => pywr_core::NodeBuilder::link(self.spill_node_sub_name()), }; - network.connect_nodes(storage, spill)?; - Some(spill) + let spill_name = spill.name().clone(); + network.connect(storage_name.clone(), spill_name.clone()); + + network.node(spill); + + Some(spill_name) } }; @@ -438,132 +401,108 @@ impl ReservoirNode { let connect_comp = self.connect_compensation_to_spill.unwrap_or(true); if connect_comp { if let (Some(spill), Some(comp)) = (spill_node, comp_node) { - network.connect_nodes(comp, spill)?; - } - } - // add rainfall node and edge - if self.rainfall.is_some() { - if self.surface_area.is_none() { - return Err(SchemaError::MissingNodeAttribute { - attr: "surface_area".to_string(), - name: self.meta().name.clone(), - }); + network.connect(comp, spill); } - - let rainfall = network.add_input_node(self.meta().name.as_str(), Self::rainfall_node_sub_name())?; - network.connect_nodes(rainfall, storage)?; } - // add evaporation node and edge - if self.evaporation.is_some() { - if self.surface_area.is_none() { - return Err(SchemaError::MissingNodeAttribute { - attr: "surface_area".to_string(), - name: self.meta().name.clone(), - }); - } - - let evaporation = network.add_output_node(self.meta().name.as_str(), Self::evaporation_node_sub_name())?; - network.connect_nodes(storage, evaporation)?; - } - - // add leakage node and edge - if self.leakage.is_some() { - let leakage = network.add_output_node(self.meta().name.as_str(), Self::leakage_node_sub_name())?; - network.connect_nodes(storage, leakage)?; - } - - Ok(()) - } - - pub fn set_constraints( - &self, - network: &mut pywr_core::network::Network, - args: &LoadArgs, - ) -> Result<(), SchemaError> { - self.storage.set_constraints(network, args)?; - - // Set compensation - if let Some(compensation) = &self.compensation { - let value = compensation.load(network, args, Some(&self.meta().name))?; - network.set_node_min_flow( - self.meta().name.as_str(), - Self::compensation_node_sub_name(), - value.into(), - )?; + // add rainfall node and edge + if self.rainfall.is_some() && self.surface_area.is_none() { + return Err(SchemaError::MissingNodeAttribute { + attr: "surface_area".to_string(), + name: self.meta().name.clone(), + }); } - // add leakage - if let Some(leakage) = &self.leakage { - let value = leakage.loss.load(network, args, Some(&self.meta().name))?; - network.set_node_max_flow(self.meta().name.as_str(), Self::leakage_node_sub_name(), value.into())?; - if let Some(cost) = &leakage.cost { - let value = cost.load(network, args, Some(&self.meta().name))?; - network.set_node_cost(self.meta().name.as_str(), Self::leakage_node_sub_name(), value.into())?; - } + if self.evaporation.is_some() && self.surface_area.is_none() { + return Err(SchemaError::MissingNodeAttribute { + attr: "surface_area".to_string(), + name: self.meta().name.clone(), + }); } // add rainfall and evaporation if let Some(bathymetry) = &self.surface_area { - // add the rainfall if let Some(rainfall) = &self.rainfall { + let mut rainfall_input = pywr_core::NodeBuilder::input(self.rainfall_node_sub_name()); + let use_max_area = rainfall.use_max_area.unwrap_or(false); let rainfall_area_metric = self.get_area_metric(network, args, "rainfall_area", bathymetry, use_max_area)?; let rainfall_metric = rainfall.data.load(network, args, Some(&self.meta().name))?; - let rainfall_flow_parameter = pywr_core::parameters::AggregatedParameter::new( - ParameterName::new("rainfall", Some(self.meta().name.as_str())), - &[rainfall_metric, rainfall_area_metric], + let rainfall_flow_parameter_name = ParameterName::new("rainfall", Some(self.meta().name.as_str())); + let mut rainfall_flow_parameter = pywr_core::parameters::AggregatedParameterBuilder::new( + rainfall_flow_parameter_name.clone(), AggFuncF64::Product, ); - let rainfall_idx = network.add_parameter(Box::new(rainfall_flow_parameter))?; - let rainfall_flow_metric: MetricF64 = rainfall_idx.into_metric_f64_before(); - - network.set_node_min_flow( - self.meta().name.as_str(), - Self::rainfall_node_sub_name(), - Some(rainfall_flow_metric.clone()), - )?; - network.set_node_max_flow( - self.meta().name.as_str(), - Self::rainfall_node_sub_name(), - Some(rainfall_flow_metric), - )?; + + rainfall_flow_parameter + .metric(rainfall_metric) + .metric(rainfall_area_metric.clone()); + + network.parameters().f64(Box::new(rainfall_flow_parameter)); + + let rainfall_flow_metric = UnresolvedMetricF64::new_parameter_before(rainfall_flow_parameter_name); + + rainfall_input + .min_flow(rainfall_flow_metric.clone()) + .max_flow(rainfall_flow_metric); + + network.connect(rainfall_input.name().clone(), storage_name.clone()); + network.node(rainfall_input); } - // add the evaporation + // add evaporation node and edge if let Some(evaporation) = &self.evaporation { + let mut evaporation_output = pywr_core::NodeBuilder::input(self.evaporation_node_sub_name()); + let use_max_area = evaporation.use_max_area.unwrap_or(false); let evaporation_area_metric = self.get_area_metric(network, args, "evaporation_area", bathymetry, use_max_area)?; // add volume to output node let evaporation_metric = evaporation.data.load(network, args, Some(&self.meta().name))?; - let evaporation_flow_parameter = pywr_core::parameters::AggregatedParameter::new( - ParameterName::new("evaporation", Some(self.meta().name.as_str())), - &[evaporation_metric, evaporation_area_metric], + let evaporation_flow_parameter_name = + ParameterName::new("evaporation", Some(self.meta().name.as_str())); + let mut evaporation_flow_parameter = pywr_core::parameters::AggregatedParameterBuilder::new( + evaporation_flow_parameter_name.clone(), AggFuncF64::Product, ); - let evaporation_idx = network.add_parameter(Box::new(evaporation_flow_parameter))?; - let evaporation_flow_metric: MetricF64 = evaporation_idx.into_metric_f64_before(); + evaporation_flow_parameter + .metric(evaporation_metric) + .metric(evaporation_area_metric.clone()); - network.set_node_max_flow( - self.meta().name.as_str(), - Self::evaporation_node_sub_name(), - Some(evaporation_flow_metric), - )?; + network.parameters().f64(Box::new(evaporation_flow_parameter)); + let evaporation_flow_metric = + UnresolvedMetricF64::new_parameter_before(evaporation_flow_parameter_name); + + evaporation_output.max_flow(evaporation_flow_metric); // set optional cost if let Some(cost) = &evaporation.cost { let value = cost.load(network, args, Some(&self.meta().name))?; - network.set_node_cost( - self.meta().name.as_str(), - Self::evaporation_node_sub_name(), - Some(value), - )?; + evaporation_output.cost(value); } + + network.connect(storage_name.clone(), evaporation_output.name().clone()); + network.node(evaporation_output); + } + } + + // add leakage node and edge + if let Some(leakage) = &self.leakage { + let mut leakage_output = pywr_core::NodeBuilder::output(self.leakage_node_sub_name()); + + let value = leakage.loss.load(network, args, Some(&self.meta().name))?; + leakage_output.max_flow(value); + + if let Some(cost) = &leakage.cost { + let value = cost.load(network, args, Some(&self.meta().name))?; + leakage_output.cost(value); } + + network.connect(storage_name.clone(), leakage_output.name().clone()); + network.node(leakage_output); } Ok(()) @@ -586,26 +525,21 @@ impl ReservoirNode { /// returns: `Result` fn get_area_metric( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, name: &str, bathymetry: &Bathymetry, use_max_area: bool, - ) -> Result { + ) -> Result { // get the current storage - let storage_node = network - .get_node_index_by_name(self.meta().name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: None, - })?; + let storage_node = UnresolvedNode::new(self.meta().name.as_str(), None); // the storage (absolute or relative) can be the current or max volume let current_storage = match (bathymetry.is_storage_proportional, use_max_area) { - (false, false) => MetricF64::NodeVolume(storage_node), - (true, false) => MetricF64::NodeProportionalVolume(storage_node), - (false, true) => MetricF64::NodeMaxVolume(storage_node), - (true, true) => MetricF64::Simple(SimpleMetricF64::Constant(Constant(1.0))), + (false, false) => UnresolvedMetricF64::NodeVolume(storage_node), + (true, false) => UnresolvedMetricF64::NodeProportionalVolume(storage_node), + (false, true) => UnresolvedMetricF64::NodeMaxVolume(storage_node), + (true, true) => UnresolvedMetricF64::Constant(1.0), }; // get the variable area metric @@ -620,39 +554,36 @@ impl ReservoirNode { .map(|(s, a)| (s.into(), a.into())) .collect::>(); - let interpolated_area_parameter = pywr_core::parameters::InterpolatedParameter::new( - ParameterName::new(name, Some(self.meta().name.as_str())), + let interpolated_area_parameter_name = ParameterName::new(name, Some(self.meta().name.as_str())); + let interpolated_area_parameter = pywr_core::parameters::InterpolatedParameterBuilder::new( + interpolated_area_parameter_name.clone(), current_storage, points, - true, ); - let area_idx = network.add_parameter(Box::new(interpolated_area_parameter))?; - let interpolated_area_metric: MetricF64 = area_idx.into_metric_f64_before(); - interpolated_area_metric + + network.parameters().f64(Box::new(interpolated_area_parameter)); + + UnresolvedMetricF64::new_parameter_before(interpolated_area_parameter_name) } BathymetryType::Polynomial(coeffs) => { - let poly_area_parameter = pywr_core::parameters::Polynomial1DParameter::new( - ParameterName::new(name, Some(self.meta().name.as_str())), + let poly_area_parameter_name = ParameterName::new(name, Some(self.meta().name.as_str())); + let poly_area_parameter = pywr_core::parameters::Polynomial1DParameterBuilder::new( + poly_area_parameter_name.clone(), current_storage, coeffs.clone(), - 1.0, - 0.0, ); - let area_idx = network.add_parameter(Box::new(poly_area_parameter))?; - let poly_area_metric: MetricF64 = area_idx.into_metric_f64_before(); - poly_area_metric + + network.parameters().f64(Box::new(poly_area_parameter)); + + UnresolvedMetricF64::new_parameter_before(poly_area_parameter_name) } }; Ok(area_metric) } - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { - match self.storage.create_metric(network, attribute) { + pub fn create_metric(&self, attribute: Option) -> Result { + match self.storage.create_metric(attribute) { Ok(m) => Ok(m), Err(SchemaError::NodeAttributeNotSupported { .. }) => { let attr = match attribute { @@ -661,50 +592,35 @@ impl ReservoirNode { }; let metric = match attr { - ReservoirNodeAttribute::Compensation => match network - .get_node_index_by_name(self.meta().name.as_str(), Self::compensation_node_sub_name()) - { - Some(idx) => MetricF64::NodeInFlow(idx), - None => 0.0.into(), - }, - ReservoirNodeAttribute::Rainfall => match network - .get_node_index_by_name(self.meta().name.as_str(), Self::rainfall_node_sub_name()) - { - Some(idx) => MetricF64::NodeInFlow(idx), - None => 0.0.into(), - }, - ReservoirNodeAttribute::Evaporation => match network - .get_node_index_by_name(self.meta().name.as_str(), Self::rainfall_node_sub_name()) - { - Some(idx) => MetricF64::NodeInFlow(idx), - None => 0.0.into(), - }, - ReservoirNodeAttribute::Volume => { - let idx = network - .get_node_index_by_name(self.meta().name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: None, - })?; - MetricF64::NodeVolume(idx) + ReservoirNodeAttribute::Compensation => { + if self.compensation.is_some() { + UnresolvedMetricF64::NodeInFlow(self.compensation_node_sub_name()) + } else { + 0.0.into() + } + } + ReservoirNodeAttribute::Rainfall => { + if self.rainfall.is_some() && self.surface_area.is_some() { + UnresolvedMetricF64::NodeInFlow(self.rainfall_node_sub_name()) + } else { + 0.0.into() + } } - ReservoirNodeAttribute::ProportionalVolume => { - let idx = network - .get_node_index_by_name(self.meta().name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: None, - })?; - MetricF64::NodeProportionalVolume(idx) + ReservoirNodeAttribute::Evaporation => { + if self.rainfall.is_some() && self.surface_area.is_some() { + UnresolvedMetricF64::NodeInFlow(self.evaporation_node_sub_name()) + } else { + 0.0.into() + } } + ReservoirNodeAttribute::Volume => { + UnresolvedMetricF64::NodeVolume(UnresolvedNode::new(self.meta().name.as_str(), None)) + } + ReservoirNodeAttribute::ProportionalVolume => UnresolvedMetricF64::NodeProportionalVolume( + UnresolvedNode::new(self.meta().name.as_str(), None), + ), ReservoirNodeAttribute::MaxVolume => { - let idx = network - .get_node_index_by_name(self.meta().name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta().name.clone(), - sub_name: None, - })?; - MetricF64::NodeMaxVolume(idx) + UnresolvedMetricF64::NodeMaxVolume(UnresolvedNode::new(self.meta().name.as_str(), None)) } }; @@ -728,9 +644,11 @@ mod tests { fn test_model_nodes_and_edges() { let data = reservoir_with_spill_str(); let schema: ModelSchema = serde_json::from_str(data).unwrap(); - let mut model: pywr_core::models::Model = schema.build_model(None, None).unwrap(); + let builder = schema.create_model_builder(None, None).unwrap(); + + let model = builder.build().unwrap(); - let network = model.network_mut(); + let network = model.network(); assert_eq!(network.nodes().len(), 5); assert_eq!(network.edges().len(), 5); } diff --git a/pywr-schema/src/nodes/river.rs b/pywr-schema/src/nodes/river.rs index a7ca1907..3b514b06 100644 --- a/pywr-schema/src/nodes/river.rs +++ b/pywr-schema/src/nodes/river.rs @@ -1,19 +1,19 @@ -use crate::error::SchemaError; use crate::error::{ComponentConversionError, ConversionError}; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -use crate::nodes::{LossFactor, NodeMeta, NodeSlot}; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; +use crate::nodes::{LossFactor, NodeMeta}; use crate::parameters::{ConstantValue, Parameter}; use crate::v1::try_convert_node_meta; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] use pywr_core::{ - aggregated_node::Relationship, - metric::MetricF64, - parameters::{MuskingumParameter, ParameterName}, + AggregatedNodeBuilder, aggregated_node::CoefficientFactorsBuilder, metric::UnresolvedMetricF64, + node::UnresolvedNode, parameters::MuskingumParameterBuilder, parameters::ParameterName, }; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::nodes::LinkNode as LinkNodeV1; @@ -123,80 +123,76 @@ impl RiverNode { const DEFAULT_ATTRIBUTE: RiverNodeAttribute = RiverNodeAttribute::Outflow; const DEFAULT_COMPONENT: RiverNodeComponent = RiverNodeComponent::Outflow; + pub fn default_attribute(&self) -> RiverNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } + + pub fn default_component(&self) -> RiverNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl RiverNode { /// The sub-name of the output node. - fn loss_sub_name() -> Option<&'static str> { - Some("loss") + fn loss_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("loss")) } /// The name of net flow node. - fn net_sub_name() -> Option<&'static str> { - Some("net") + fn net_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("net")) + } + fn output_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("inflow")) } - fn output_sub_name() -> Option<&'static str> { - Some("inflow") + + fn input_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("outflow")) + } + + /// The name of the aggregated node to handle the proportional loss. + fn agg_loss_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("aggregated_loss_node")) } - fn input_sub_name() -> Option<&'static str> { - Some("outflow") + fn agg_routing_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("aggregated_routing_node")) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { let connectors = match (self.loss_factor.is_some(), self.routing_method.is_some()) { - (false, false) => vec![(self.meta.name.as_str(), Self::net_sub_name().map(|s| s.to_string()))], - (true, false) => vec![ - (self.meta.name.as_str(), Self::net_sub_name().map(|s| s.to_string())), - (self.meta.name.as_str(), Self::loss_sub_name().map(|s| s.to_string())), - ], + (false, false) => vec![self.net_sub_name()], + (true, false) => vec![self.net_sub_name(), self.loss_sub_name()], // If there is routing directly to the output node - _ => vec![(self.meta.name.as_str(), Self::output_sub_name().map(|s| s.to_string()))], + _ => vec![self.output_sub_name()], }; Ok(connectors) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { let connectors = match (self.loss_factor.is_some(), self.routing_method.is_some()) { // If there is routing, but no loss connect directly from the input node - (false, true) => vec![(self.meta.name.as_str(), Self::input_sub_name().map(|s| s.to_string()))], - _ => vec![(self.meta.name.as_str(), Self::net_sub_name().map(|s| s.to_string()))], + (false, true) => vec![self.input_sub_name()], + _ => vec![self.net_sub_name()], }; Ok(connectors) } } - pub fn default_attribute(&self) -> RiverNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> RiverNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl RiverNode { - /// The name of the aggregated node to handle the proportional loss. - fn agg_loss_sub_name() -> Option<&'static str> { - Some("aggregated_loss_node") - } - - fn agg_routing_sub_name() -> Option<&'static str> { - Some("aggregated_routing_node") - } - - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default attribute if none is specified let component = match component { Some(c) => c.try_into()?, @@ -208,53 +204,19 @@ impl RiverNode { match (self.loss_factor.is_some(), self.routing_method.is_some()) { (false, false) => { // Simple link node - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ] + vec![self.net_sub_name()] } (true, false) => { // Loss, but no routing - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - network - .get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::loss_sub_name().map(String::from), - })?, - ] + vec![self.net_sub_name(), self.loss_sub_name()] } (false, true) => { // Routing, but no loss - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?, - ] + vec![self.output_sub_name()] } (true, true) => { // Both loss and routing - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?, - ] + vec![self.output_sub_name()] } } } @@ -262,32 +224,18 @@ impl RiverNode { match self.routing_method.is_some() { false => { // Simple link node - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ] + vec![self.net_sub_name()] } true => { // Routing - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::input_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::input_sub_name().map(String::from), - })?, - ] + vec![self.input_sub_name()] } } } RiverNodeComponent::Loss => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(idx) => vec![idx], - None => return Ok(vec![]), // No loss node defined, so return empty + match self.loss_factor.is_some() { + true => vec![self.loss_sub_name()], + false => vec![], // No loss node defined, so return empty } } }; @@ -295,112 +243,126 @@ impl RiverNode { Ok(indices) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { // Create nodes based on the presence of loss and routing method match (self.loss_factor.is_some(), self.routing_method.is_some()) { (false, false) => { // Simple link node - network.add_link_node(self.meta.name.as_str(), Self::net_sub_name())?; + let link_node = pywr_core::node::NodeBuilder::link(self.net_sub_name()); + network.node(link_node); } (true, false) => { // Loss, but no routing - let river_idx = network.add_link_node(self.meta.name.as_str(), Self::net_sub_name())?; - let loss_idx = network.add_output_node(self.meta.name.as_str(), Self::loss_sub_name())?; + let river_node = pywr_core::node::NodeBuilder::link(self.net_sub_name()); + let mut loss_node = pywr_core::node::NodeBuilder::output(self.loss_sub_name()); // The aggregated node factors to handle the loss - network.add_aggregated_node( - self.meta.name.as_str(), - Self::agg_loss_sub_name(), - &[vec![river_idx], vec![loss_idx]], - None, - )?; + self.add_loss_to_network(network, &mut loss_node, args)?; + + network.node(river_node); + network.node(loss_node); } (false, true) => { // Routing, but no loss - let inflow_idx = network.add_output_node(self.meta.name.as_str(), Self::output_sub_name())?; - let outflow_idx = network.add_input_node(self.meta.name.as_str(), Self::input_sub_name())?; - - // This aggregated node will contain the factors to enforce the Muskingum routing - network.add_aggregated_node( - self.meta.name.as_str(), - Self::agg_routing_sub_name(), - &[vec![outflow_idx], vec![inflow_idx]], - None, - )?; + let inflow_node = pywr_core::node::NodeBuilder::output(self.output_sub_name()); + let mut outflow_node = pywr_core::node::NodeBuilder::input(self.input_sub_name()); + + // Add aggregated node to enforce the routing + self.add_routing_to_network(network, &mut outflow_node, args)?; + + network.node(inflow_node); + network.node(outflow_node); } (true, true) => { // Both loss and routing - let river_idx = network.add_link_node(self.meta.name.as_str(), Self::net_sub_name())?; - let loss_idx = network.add_output_node(self.meta.name.as_str(), Self::loss_sub_name())?; + let river_node = pywr_core::node::NodeBuilder::link(self.net_sub_name()); + let mut loss_node = pywr_core::node::NodeBuilder::output(self.loss_sub_name()); + // The aggregated node factors to handle the loss - network.add_aggregated_node( - self.meta.name.as_str(), - Self::agg_loss_sub_name(), - &[vec![river_idx], vec![loss_idx]], - None, - )?; - let inflow_idx = network.add_output_node(self.meta.name.as_str(), Self::output_sub_name())?; - let outflow_idx = network.add_input_node(self.meta.name.as_str(), Self::input_sub_name())?; - - // This aggregated node will contain the factors to enforce the Muskingum routing - network.add_aggregated_node( - self.meta.name.as_str(), - Self::agg_routing_sub_name(), - &[vec![outflow_idx], vec![inflow_idx]], - None, - )?; + self.add_loss_to_network(network, &mut loss_node, args)?; + + let inflow_node = pywr_core::node::NodeBuilder::output(self.output_sub_name()); + let mut outflow_node = pywr_core::node::NodeBuilder::input(self.input_sub_name()); + // Add aggregated node to enforce the routing + self.add_routing_to_network(network, &mut outflow_node, args)?; // The input node needs connecting to the main link node and loss node. - network.connect_nodes(outflow_idx, river_idx)?; - network.connect_nodes(outflow_idx, loss_idx)?; + network.connect(self.input_sub_name(), self.net_sub_name()); + network.connect(self.input_sub_name(), self.loss_sub_name()); + + network.node(river_node); + network.node(loss_node); + network.node(inflow_node); + network.node(outflow_node); } } Ok(()) } - pub fn set_constraints( + /// If there is a loss factor defined add the relevant aggregated node and corresponding parameters and factors. + fn add_loss_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, + loss_node: &mut pywr_core::node::NodeBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { if let Some(loss_factor) = &self.loss_factor { let factors = loss_factor.load(network, args, Some(&self.meta.name))?; - 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()))?; + + match factors { + Some(relationship) => { + let mut agg_node = AggregatedNodeBuilder::new(self.agg_loss_sub_name()); + + agg_node + .nodes(vec![self.net_sub_name()]) + .nodes(vec![self.loss_sub_name()]) + .relationship(relationship); + + network.agg_node(agg_node); + } + None => { + // Loaded a constant zero factor; ensure that the loss node has zero flow + loss_node.max_flow(0.0.into()); + } } - network.set_aggregated_node_relationship(self.meta.name.as_str(), Self::agg_loss_sub_name(), factors)?; } + Ok(()) + } + + /// If there is routing defined add the relevant aggregated node and corresponding parametrs + /// and factors. + fn add_routing_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + input_node: &mut pywr_core::node::NodeBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { if let Some(routing_method) = &self.routing_method { match routing_method { RoutingMethod::Delay { delay, initial_value } => { // Create a delay parameter using the node's name as the parent identifier let name = ParameterName::new("delay", Some(self.meta.name.as_str())); - let inflow_idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?; - let metric = MetricF64::NodeInFlow(inflow_idx); - - let p = pywr_core::parameters::DelayParameter::new( - name, + + let metric = UnresolvedMetricF64::NodeInFlow(self.output_sub_name()); + + let p = pywr_core::parameters::DelayParameterBuilder::new( + name.clone(), metric, *delay, initial_value.load(args.tables)?, ); - let delay_idx = network.add_parameter(Box::new(p))?; + network.parameters().f64(Box::new(p)); // Apply it as a constraint on the input node. - let metric: MetricF64 = delay_idx.into_metric_f64_before(); - network.set_node_max_flow( - self.meta.name.as_str(), - Self::input_sub_name(), - metric.clone().into(), - )?; - network.set_node_min_flow(self.meta.name.as_str(), Self::input_sub_name(), metric.into())?; + let metric = UnresolvedMetricF64::new_parameter_before(name); + + input_node.min_flow(metric.clone()); + input_node.max_flow(metric.clone()); } RoutingMethod::Muskingum { travel_time, @@ -409,47 +371,40 @@ impl RiverNode { } => { // Create a Muskingum parameter using the node's name as the parent identifier let name = ParameterName::new("muskingum", Some(self.meta.name.as_str())); - let inflow_idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::output_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::output_sub_name().map(String::from), - })?; - let outflow_idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::input_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::input_sub_name().map(String::from), - })?; + let inflow_metric = UnresolvedMetricF64::NodeInFlow(self.output_sub_name()); + let outflow_metric = UnresolvedMetricF64::NodeOutFlow(self.input_sub_name()); let travel_time = travel_time.load(network, args, Some(&self.meta.name))?; let weight = weight.load(network, args, Some(&self.meta.name))?; - let muskingum_parameter = MuskingumParameter::new( + let muskingum_parameter = MuskingumParameterBuilder::new( name.clone(), - MetricF64::NodeInFlow(inflow_idx), - MetricF64::NodeOutFlow(outflow_idx), + inflow_metric, + outflow_metric, travel_time, weight, initial_condition.clone().into(), ); - let muskingum_idx = network.add_multi_value_parameter(Box::new(muskingum_parameter))?; + network.parameters().multi(Box::new(muskingum_parameter)); // Set the relationship on the aggregated node to enforce the Muskingum routing - let factors = Relationship::new_coefficient_factors( - &[ - 1.0.into(), - muskingum_idx.clone().into_metric_f64_before("inflow_factor"), - ], - Some(muskingum_idx.into_metric_f64_before("rhs")), - ); - - network.set_aggregated_node_relationship( - self.meta.name.as_str(), - Self::agg_routing_sub_name(), - Some(factors), - )?; + let mut factors = CoefficientFactorsBuilder::default(); + factors + .factor(1.0.into()) + .factor(UnresolvedMetricF64::new_parameter_before_key( + name.clone(), + "inflow_factor", + )) + .rhs(UnresolvedMetricF64::new_parameter_before_key(name, "rhs")); + + let mut agg_node = pywr_core::AggregatedNodeBuilder::new(self.agg_routing_sub_name()); + agg_node + .nodes(vec![self.input_sub_name()]) + .nodes(vec![self.output_sub_name()]) + .relationship(Box::new(factors)); + + network.agg_node(agg_node); } } } @@ -457,11 +412,7 @@ impl RiverNode { Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -470,27 +421,26 @@ impl RiverNode { let metric = match attr { RiverNodeAttribute::Inflow => { - let indices = - self.node_indices_for_flow_constraints(network, Some(RiverNodeComponent::Inflow.into()))?; + let nodes = self.nodes_for_flow_constraints(Some(RiverNodeComponent::Inflow.into()))?; - MetricF64::MultiNodeInFlow { - indices, + UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), } } RiverNodeAttribute::Outflow => { - let indices = - self.node_indices_for_flow_constraints(network, Some(RiverNodeComponent::Outflow.into()))?; + let nodes = self.nodes_for_flow_constraints(Some(RiverNodeComponent::Outflow.into()))?; - MetricF64::MultiNodeOutFlow { - indices, + UnresolvedMetricF64::MultiNodeOutFlow { + nodes, name: self.meta.name.to_string(), } } RiverNodeAttribute::Loss => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(loss_idx) => MetricF64::NodeInFlow(loss_idx), - None => 0.0.into(), + if self.loss_factor.is_some() { + UnresolvedMetricF64::NodeInFlow(self.loss_sub_name()) + } else { + 0.0.into() } } }; diff --git a/pywr-schema/src/nodes/river_gauge.rs b/pywr-schema/src/nodes/river_gauge.rs index 158a0c1d..7917fa89 100644 --- a/pywr-schema/src/nodes/river_gauge.rs +++ b/pywr-schema/src/nodes/river_gauge.rs @@ -1,16 +1,17 @@ use crate::error::ComponentConversionError; -use crate::error::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; +use crate::nodes::NodeMeta; use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_node_attr, try_convert_node_meta}; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::RiverGaugeNode as RiverGaugeNodeV1; @@ -58,141 +59,104 @@ impl RiverGaugeNode { const DEFAULT_ATTRIBUTE: RiverGaugeNodeAttribute = RiverGaugeNodeAttribute::Outflow; const DEFAULT_COMPONENT: RiverGaugeNodeComponent = RiverGaugeNodeComponent::Outflow; - fn mrf_sub_name() -> Option<&'static str> { - Some("mrf") + pub fn default_attribute(&self) -> RiverGaugeNodeAttribute { + Self::DEFAULT_ATTRIBUTE } - fn bypass_sub_name() -> Option<&'static str> { - Some("bypass") + pub fn default_component(&self) -> RiverGaugeNodeComponent { + Self::DEFAULT_COMPONENT } +} - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { +#[cfg(feature = "core")] +impl RiverGaugeNode { + fn mrf_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("mrf")) + } + + fn bypass_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("bypass")) + } + + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![ - (self.meta.name.as_str(), Self::mrf_sub_name().map(|s| s.to_string())), - (self.meta.name.as_str(), Self::bypass_sub_name().map(|s| s.to_string())), - ]) + Ok(vec![self.mrf_sub_name(), self.bypass_sub_name()]) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { - Ok(vec![ - (self.meta.name.as_str(), Self::mrf_sub_name().map(|s| s.to_string())), - (self.meta.name.as_str(), Self::bypass_sub_name().map(|s| s.to_string())), - ]) + Ok(vec![self.mrf_sub_name(), self.bypass_sub_name()]) } } - - pub fn default_attribute(&self) -> RiverGaugeNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> RiverGaugeNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl RiverGaugeNode { - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let indices = match component { + let nodes = match component { // Inflow and Outflow components both use the same nodes. RiverGaugeNodeComponent::Inflow | RiverGaugeNodeComponent::Outflow => { - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::mrf_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::mrf_sub_name().map(String::from), - })?, - network - .get_node_index_by_name(self.meta.name.as_str(), Self::bypass_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::bypass_sub_name().map(String::from), - })?, - ] + vec![self.mrf_sub_name(), self.bypass_sub_name()] } }; - Ok(indices) - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_link_node(self.meta.name.as_str(), Self::mrf_sub_name())?; - network.add_link_node(self.meta.name.as_str(), Self::bypass_sub_name())?; - - Ok(()) + Ok(nodes) } - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut mrf_node = pywr_core::node::NodeBuilder::link(self.mrf_sub_name()); + let bypass_node = pywr_core::node::NodeBuilder::link(self.bypass_sub_name()); + // MRF applies as a maximum on the MRF node. if let Some(cost) = &self.mrf_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::mrf_sub_name(), value.into())?; + mrf_node.cost(value); } if let Some(mrf) = &self.mrf { let value = mrf.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), Self::mrf_sub_name(), value.into())?; + mrf_node.max_flow(value); } if let Some(cost) = &self.bypass_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::bypass_sub_name(), value.into())?; + mrf_node.min_flow(value); } + network.node(mrf_node); + network.node(bypass_node); + Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let indices = vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::mrf_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::mrf_sub_name().map(String::from), - })?, - network - .get_node_index_by_name(self.meta.name.as_str(), Self::bypass_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::bypass_sub_name().map(String::from), - })?, - ]; + let nodes = vec![self.mrf_sub_name(), self.bypass_sub_name()]; let metric = match attr { - RiverGaugeNodeAttribute::Inflow => MetricF64::MultiNodeInFlow { - indices, + RiverGaugeNodeAttribute::Inflow => UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), }, - RiverGaugeNodeAttribute::Outflow => MetricF64::MultiNodeOutFlow { - indices, + RiverGaugeNodeAttribute::Outflow => UnresolvedMetricF64::MultiNodeOutFlow { + nodes, name: self.meta.name.to_string(), }, }; diff --git a/pywr-schema/src/nodes/river_split_with_gauge.rs b/pywr-schema/src/nodes/river_split_with_gauge.rs index 08520e56..5b5ed5d1 100644 --- a/pywr-schema/src/nodes/river_split_with_gauge.rs +++ b/pywr-schema/src/nodes/river_split_with_gauge.rs @@ -11,7 +11,7 @@ use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_node_attr, try_convert_node_meta}; use crate::{ConversionError, TryIntoV2, mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::{aggregated_node::Relationship, metric::MetricF64, node::NodeIndex}; +use pywr_core::{aggregated_node::ProportionalFactorsBuilder, metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::RiverSplitWithGaugeNode as RiverSplitWithGaugeNodeV1; @@ -102,38 +102,54 @@ pub struct RiverSplitWithGaugeNode { impl RiverSplitWithGaugeNode { const DEFAULT_ATTRIBUTE: RiverSplitWithGaugeNodeAttribute = RiverSplitWithGaugeNodeAttribute::Outflow; const DEFAULT_COMPONENT: RiverSplitWithGaugeNodeComponent = RiverSplitWithGaugeNodeComponent::Outflow; + + pub fn iter_output_slots(&self) -> impl Iterator + '_ { + [AbstractionOutputNodeSlot::River.into()] + .into_iter() + .chain(self.splits.iter().enumerate().map(|(i, split)| match &split.slot_name { + Some(name) => NodeSlot::User { name: name.clone() }, + None => NodeSlot::Split { position: i }, + })) + } + + pub fn default_attribute(&self) -> RiverSplitWithGaugeNodeAttribute { + Self::DEFAULT_ATTRIBUTE + } + + pub fn default_component(&self) -> RiverSplitWithGaugeNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl RiverSplitWithGaugeNode { const DEFAULT_OUTPUT_SLOT: RiverSplitWithGaugeOutputNodeSlot = RiverSplitWithGaugeOutputNodeSlot::River; + fn mrf_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("mrf")) + } - fn mrf_sub_name() -> Option<&'static str> { - Some("mrf") + fn bypass_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("bypass")) } - fn bypass_sub_name() -> Option<&'static str> { - Some("bypass") + fn split_sub_name(&self, i: usize) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some(&format!("split-{i}"))) } - fn split_sub_name(i: usize) -> Option { - Some(format!("split-{i}")) + fn split_agg_sub_name(&self, i: usize) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some(&format!("split-agg-{i}"))) } /// These connectors are used for both incoming and Output edges on the default slot. - fn default_connectors(&self) -> Vec<(&str, Option)> { - let mut connectors = vec![ - (self.meta.name.as_str(), Self::mrf_sub_name().map(|s| s.to_string())), - (self.meta.name.as_str(), Self::bypass_sub_name().map(|s| s.to_string())), - ]; - - connectors.extend( - self.splits - .iter() - .enumerate() - .map(|(i, _)| (self.meta.name.as_str(), Self::split_sub_name(i))), - ); + fn default_connectors(&self) -> Vec { + let mut connectors = vec![self.mrf_sub_name(), self.bypass_sub_name()]; + + connectors.extend(self.splits.iter().enumerate().map(|(i, _)| self.split_sub_name(i))); connectors } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { @@ -141,7 +157,7 @@ impl RiverSplitWithGaugeNode { } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { let slot = match slot { Some(s) => s.clone().try_into()?, None => Self::DEFAULT_OUTPUT_SLOT, @@ -151,7 +167,7 @@ impl RiverSplitWithGaugeNode { RiverSplitWithGaugeOutputNodeSlot::River => self.default_connectors(), RiverSplitWithGaugeOutputNodeSlot::Split { position } => { if *position < self.splits.len() { - vec![(self.meta.name.as_str(), Self::split_sub_name(*position))] + vec![self.split_sub_name(*position)] } else { return Err(SchemaError::NodeConnectionSlotNotFound { node: self.meta.name.clone(), @@ -165,7 +181,7 @@ impl RiverSplitWithGaugeNode { .iter() .position(|split| split.slot_name.as_ref().is_some_and(|s| s == name)) { - Some(i) => vec![(self.meta.name.as_str(), Self::split_sub_name(i))], + Some(i) => vec![self.split_sub_name(i)], None => { return Err(SchemaError::NodeConnectionSlotNotFound { node: self.meta.name.clone(), @@ -179,35 +195,10 @@ impl RiverSplitWithGaugeNode { Ok(indices) } - pub fn iter_output_slots(&self) -> impl Iterator + '_ { - [AbstractionOutputNodeSlot::River.into()] - .into_iter() - .chain(self.splits.iter().enumerate().map(|(i, split)| match &split.slot_name { - Some(name) => NodeSlot::User { name: name.clone() }, - None => NodeSlot::Split { position: i }, - })) - } - - pub fn default_attribute(&self) -> RiverSplitWithGaugeNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> RiverSplitWithGaugeNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl RiverSplitWithGaugeNode { - fn split_agg_sub_name(i: usize) -> Option { - Some(format!("split-agg-{i}")) - } - - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, @@ -219,94 +210,55 @@ impl RiverSplitWithGaugeNode { // There's currently no way to isolate the flows to the individual splits // Therefore, the only components are gross inflow and outflow RiverSplitWithGaugeNodeComponent::Inflow | RiverSplitWithGaugeNodeComponent::Outflow => { - let mut indices = vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::mrf_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::mrf_sub_name().map(String::from), - })?, - network - .get_node_index_by_name(self.meta.name.as_str(), Self::bypass_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::bypass_sub_name().map(String::from), - })?, - ]; - - let split_idx: Vec = self - .splits - .iter() - .enumerate() - .map(|(i, _)| { - network - .get_node_index_by_name(self.meta.name.as_str(), Self::split_sub_name(i).as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::split_sub_name(i), - }) - }) - .collect::>()?; - - indices.extend(split_idx); - Ok(indices) + Ok(self.default_connectors()) } } } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - // TODO do this properly - network.add_link_node(self.meta.name.as_str(), Self::mrf_sub_name())?; - let bypass_idx = network.add_link_node(self.meta.name.as_str(), Self::bypass_sub_name())?; - - for (i, _) in self.splits.iter().enumerate() { - // Each split has a link node and an aggregated node to enforce the factors - let split_idx = network.add_link_node(self.meta.name.as_str(), Self::split_sub_name(i).as_deref())?; - - // The factors will be set during the `set_constraints` method - network.add_aggregated_node( - self.meta.name.as_str(), - Self::split_agg_sub_name(i).as_deref(), - &[vec![bypass_idx], vec![split_idx]], - None, - )?; - } - - Ok(()) - } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut mrf_node = pywr_core::node::NodeBuilder::link(self.mrf_sub_name()); + let bypass_node = pywr_core::node::NodeBuilder::link(self.bypass_sub_name()); + // MRF applies as a maximum on the MRF node. if let Some(cost) = &self.mrf_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::mrf_sub_name(), value.into())?; + mrf_node.cost(value); } if let Some(mrf) = &self.mrf { let value = mrf.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), Self::mrf_sub_name(), value.into())?; + mrf_node.max_flow(value); } for (i, split) in self.splits.iter().enumerate() { + // Each split has a link node and an aggregated node to enforce the factors + let split_node = pywr_core::node::NodeBuilder::link(self.split_sub_name(i)); + // Set the factors for each split - let r = Relationship::new_proportion_factors(&[split.factor.load(network, args, Some(&self.meta.name))?]); - network.set_aggregated_node_relationship( - self.meta.name.as_str(), - Self::split_agg_sub_name(i).as_deref(), - Some(r), - )?; + let mut r = ProportionalFactorsBuilder::default(); + r.factor(split.factor.load(network, args, Some(&self.meta.name))?); + + // The factors will be set during the `set_constraints` method + let mut agg_node = pywr_core::AggregatedNodeBuilder::new(self.split_agg_sub_name(i)); + agg_node + .nodes(vec![self.bypass_sub_name()]) + .nodes(vec![self.split_sub_name(i)]) + .relationship(Box::new(r)); + + network.agg_node(agg_node); + network.node(split_node); } + network.node(mrf_node); + network.node(bypass_node); + Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -316,44 +268,15 @@ impl RiverSplitWithGaugeNode { // This gets the indices of all the link nodes // There's currently no way to isolate the flows to the individual splits // Therefore, the only metrics are gross inflow and outflow - let mut indices = vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::mrf_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::mrf_sub_name().map(String::from), - })?, - network - .get_node_index_by_name(self.meta.name.as_str(), Self::bypass_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::bypass_sub_name().map(String::from), - })?, - ]; - - let split_idx: Vec = self - .splits - .iter() - .enumerate() - .map(|(i, _)| { - network - .get_node_index_by_name(self.meta.name.as_str(), Self::split_sub_name(i).as_deref()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::split_sub_name(i), - }) - }) - .collect::>()?; - - indices.extend(split_idx); + let nodes = self.default_connectors(); let metric = match attr { - RiverSplitWithGaugeNodeAttribute::Inflow => MetricF64::MultiNodeInFlow { - indices, + RiverSplitWithGaugeNodeAttribute::Inflow => UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), }, - RiverSplitWithGaugeNodeAttribute::Outflow => MetricF64::MultiNodeOutFlow { - indices, + RiverSplitWithGaugeNodeAttribute::Outflow => UnresolvedMetricF64::MultiNodeOutFlow { + nodes, name: self.meta.name.to_string(), }, }; diff --git a/pywr-schema/src/nodes/turbine.rs b/pywr-schema/src/nodes/turbine.rs index 95c2a313..cc274676 100644 --- a/pywr-schema/src/nodes/turbine.rs +++ b/pywr-schema/src/nodes/turbine.rs @@ -1,15 +1,17 @@ -use crate::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; +use crate::nodes::NodeMeta; use crate::parameters::Parameter; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] use pywr_core::{ - metric::MetricF64, + metric::UnresolvedMetricF64, + node::UnresolvedNode, parameters::{HydropowerTargetData, ParameterName}, }; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; @@ -123,21 +125,6 @@ impl TurbineNode { const DEFAULT_ATTRIBUTE: TurbineNodeAttribute = TurbineNodeAttribute::Outflow; const DEFAULT_COMPONENT: TurbineNodeComponent = TurbineNodeComponent::Outflow; - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { - if let Some(slot) = slot { - Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) - } else { - Ok(vec![(self.meta.name.as_str(), None)]) - } - } - pub fn default_attribute(&self) -> TurbineNodeAttribute { Self::DEFAULT_ATTRIBUTE } @@ -149,40 +136,49 @@ impl TurbineNode { #[cfg(feature = "core")] impl TurbineNode { - pub fn node_indices_for_flow_constraints( + fn name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, None) + } + + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![self.name()]) + } + } + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { + if let Some(slot) = slot { + Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) + } else { + Ok(vec![self.name()]) + } + } + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default component if none is specified let component = match component { Some(c) => c.try_into()?, None => Self::DEFAULT_COMPONENT, }; - let idx = match component { - TurbineNodeComponent::Inflow | TurbineNodeComponent::Outflow => network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?, + let name = match component { + TurbineNodeComponent::Inflow | TurbineNodeComponent::Outflow => self.name(), }; - Ok(vec![idx]) - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_link_node(self.meta.name.as_str(), None)?; - Ok(()) + Ok(vec![name]) } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut link_node = pywr_core::node::NodeBuilder::link(self.name()); + if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), None, value.into())?; + link_node.cost(value); } let name = ParameterName::new("power", Some(self.meta.name.as_str())); @@ -198,14 +194,7 @@ impl TurbineNode { .map(|t| t.load(network, args, Some(&self.meta.name))) .transpose()?; - let inflow_metric: MetricF64 = MetricF64::NodeInFlow( - network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?, - ); + let inflow_metric = UnresolvedMetricF64::NodeInFlow(self.name()); let turbine_data = HydropowerTargetData { actual_flow: Some(inflow_metric), @@ -221,63 +210,48 @@ impl TurbineNode { energy_unit_conversion: Some(self.energy_unit_conversion), }; - let p = pywr_core::parameters::HydropowerTargetParameter::new(name, turbine_data); - let power_idx = network.add_parameter(Box::new(p))?; + let p = pywr_core::parameters::HydropowerTargetParameterBuilder::new(name.clone(), turbine_data); + network.parameters().f64(Box::new(p)); // Only set flow constraints if a target is defined if self.target.is_some() { - let metric: MetricF64 = power_idx.into_metric_f64_before(); + let metric = UnresolvedMetricF64::new_parameter_before(name); match self.target_type.clone().unwrap_or_default() { TargetType::MaxFlow => { - network.set_node_max_flow(self.meta.name.as_str(), None, metric.clone().into())?; + link_node.max_flow(metric); } TargetType::MinFlow => { - network.set_node_min_flow(self.meta.name.as_str(), None, metric.clone().into())?; + link_node.min_flow(metric); } TargetType::Both => { - network.set_node_max_flow(self.meta.name.as_str(), None, metric.clone().into())?; - network.set_node_min_flow(self.meta.name.as_str(), None, metric.clone().into())? + link_node.min_flow(metric.clone()); + link_node.max_flow(metric); } } } + network.node(link_node); + Ok(()) } - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = self.name(); let metric = match attr { - TurbineNodeAttribute::Outflow => MetricF64::NodeOutFlow(idx), - TurbineNodeAttribute::Inflow => MetricF64::NodeInFlow(idx), + TurbineNodeAttribute::Outflow => UnresolvedMetricF64::NodeOutFlow(name), + TurbineNodeAttribute::Inflow => UnresolvedMetricF64::NodeInFlow(name), TurbineNodeAttribute::Power => { // Retrieve the parameter created for the hydropower calculation let name = ParameterName::new("power", Some(self.meta.name.as_str())); - let power_param = - network - .get_parameter_index_by_name(&name) - .ok_or_else(|| SchemaError::CoreParameterNotFound { - name: name.to_string(), - key: None, - })?; - - power_param.into_metric_f64_after() + UnresolvedMetricF64::new_parameter_after(name) } }; diff --git a/pywr-schema/src/nodes/virtual_nodes/aggregated.rs b/pywr-schema/src/nodes/virtual_nodes/aggregated.rs index 11f40b40..1f29cd88 100644 --- a/pywr-schema/src/nodes/virtual_nodes/aggregated.rs +++ b/pywr-schema/src/nodes/virtual_nodes/aggregated.rs @@ -1,16 +1,17 @@ use crate::error::ComponentConversionError; -use crate::error::SchemaError; use crate::metric::{Metric, NodeComponentReference}; -#[cfg(feature = "core")] -use crate::network::LoadArgs; use crate::node_attribute_subset_enum; -#[cfg(feature = "core")] -use crate::nodes::NodeAttribute; use crate::nodes::NodeMeta; use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_node_attr, try_convert_node_meta, try_convert_parameter_attr}; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use crate::{error::SchemaError, network::LoadArgs, nodes::NodeAttribute}; +#[cfg(feature = "core")] +use pywr_core::{ + aggregated_node::{ExclusivityBuilder, RelationshipBuilder}, + metric::UnresolvedMetricF64, + node::UnresolvedNode, +}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::{AggregatedNode as AggregatedNodeV1, AggregatedStorageNode as AggregatedStorageNodeV1}; @@ -114,17 +115,6 @@ pub struct AggregatedNode { impl AggregatedNode { const DEFAULT_ATTRIBUTE: AggregatedNodeAttribute = AggregatedNodeAttribute::Outflow; - pub fn input_connectors(&self) -> Result)>, SchemaError> { - // Not connectable - // TODO this should be a trait? And error if you try to connect to a non-connectable node. - Ok(vec![]) - } - - pub fn output_connectors(&self) -> Result)>, SchemaError> { - // Not connectable - Ok(vec![]) - } - pub fn default_attribute(&self) -> AggregatedNodeAttribute { Self::DEFAULT_ATTRIBUTE } @@ -132,107 +122,108 @@ impl AggregatedNode { #[cfg(feature = "core")] impl AggregatedNode { - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { - let nodes: Vec> = self - .nodes - .iter() - .map(|node_ref| { - let node = args - .schema - .get_node_by_name(&node_ref.name) - .ok_or_else(|| SchemaError::NodeNotFound { - name: node_ref.name.to_string(), - })?; - - node.node_indices_for_flow_constraints(network, node_ref.component) - }) - .collect::, _>>()?; - - // We initialise with no factors, but will update them in the `set_constraints` method - // once all the parameters are loaded. - network.add_aggregated_node(self.meta.name.as_str(), None, nodes.as_slice(), None)?; - Ok(()) + fn name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, None) } - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + // Create the aggregated node builder + let mut agg_node = pywr_core::AggregatedNodeBuilder::new(self.name()); + + // Add the nodes + for node_ref in &self.nodes { + let node = args + .schema + .get_node_by_name(&node_ref.name) + .ok_or_else(|| SchemaError::NodeNotFound { + name: node_ref.name.to_string(), + })?; + + // Add each node's nodes to the aggregated node. + agg_node.nodes(node.nodes_for_flow_constraints(node_ref.component)?); + } + if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_max_flow(self.meta.name.as_str(), None, value.into())?; + agg_node.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_aggregated_node_min_flow(self.meta.name.as_str(), None, value.into())?; + agg_node.min_flow(value); } if let Some(relationship) = &self.relationship { - let r = match relationship { + let r: Box = match relationship { Relationship::Proportion { factors } => { - pywr_core::aggregated_node::Relationship::new_proportion_factors( - &factors - .iter() - .map(|f| f.load(network, args, Some(&self.meta.name))) - .collect::, _>>()?, - ) + let mut r = pywr_core::aggregated_node::ProportionalFactorsBuilder::default(); + + for factor in factors { + r.factor(factor.load(network, args, Some(&self.meta.name))?); + } + + Box::new(r) + } + Relationship::Ratio { factors } => { + let mut r = pywr_core::aggregated_node::RatioFactorsBuilder::default(); + + for factor in factors { + r.factor(factor.load(network, args, Some(&self.meta.name))?); + } + + Box::new(r) } - Relationship::Ratio { factors } => pywr_core::aggregated_node::Relationship::new_ratio_factors( - &factors - .iter() - .map(|f| f.load(network, args, Some(&self.meta.name))) - .collect::, _>>()?, - ), Relationship::Coefficients { factors, rhs } => { - let rhs_value = match rhs { - Some(r) => Some(r.load(network, args, Some(&self.meta.name))?), - None => None, - }; - pywr_core::aggregated_node::Relationship::new_coefficient_factors( - &factors - .iter() - .map(|f| f.load(network, args, Some(&self.meta.name))) - .collect::, _>>()?, - rhs_value, - ) + let mut r = pywr_core::aggregated_node::CoefficientFactorsBuilder::default(); + + for factor in factors { + r.factor(factor.load(network, args, Some(&self.meta.name))?); + } + + if let Some(rhs_value) = rhs { + let rhs = rhs_value.load(network, args, Some(&self.meta.name))?; + r.rhs(rhs); + } + + Box::new(r) } Relationship::Exclusive { min_active, max_active } => { - pywr_core::aggregated_node::Relationship::new_exclusive( - min_active.unwrap_or(0), - max_active.unwrap_or(1), - ) + let mut r = ExclusivityBuilder::default(); + + if let Some(min_active) = min_active { + r.min_active(*min_active); + } + + if let Some(max_active) = max_active { + r.max_active(*max_active); + } + Box::new(r) } }; - network.set_aggregated_node_relationship(self.meta.name.as_str(), None, Some(r))?; + agg_node.relationship(r); } + network.agg_node(agg_node); Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_aggregated_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = self.name(); let metric = match attr { - AggregatedNodeAttribute::Outflow => MetricF64::AggregatedNodeOutFlow(idx), - AggregatedNodeAttribute::Inflow => MetricF64::AggregatedNodeInFlow(idx), + AggregatedNodeAttribute::Outflow => UnresolvedMetricF64::AggregatedNodeOutFlow(name), + AggregatedNodeAttribute::Inflow => UnresolvedMetricF64::AggregatedNodeInFlow(name), }; Ok(metric) @@ -321,17 +312,6 @@ pub struct AggregatedStorageNode { impl AggregatedStorageNode { const DEFAULT_ATTRIBUTE: AggregatedStorageNodeAttribute = AggregatedStorageNodeAttribute::Volume; - pub fn input_connectors(&self) -> Result)>, SchemaError> { - // Not connectable - // TODO this should be a trait? And error if you try to connect to a non-connectable node. - Ok(vec![]) - } - - pub fn output_connectors(&self) -> Result)>, SchemaError> { - // Not connectable - Ok(vec![]) - } - pub fn default_attribute(&self) -> AggregatedStorageNodeAttribute { Self::DEFAULT_ATTRIBUTE } @@ -339,7 +319,15 @@ impl AggregatedStorageNode { #[cfg(feature = "core")] impl AggregatedStorageNode { - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { + fn name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, None) + } + + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { let nodes = self .storage_nodes .iter() @@ -351,38 +339,37 @@ impl AggregatedStorageNode { name: node_ref.name.to_string(), })?; - node.node_indices_for_storage_constraints(network) + node.nodes_for_storage_constraints() }) .collect::, _>>()? .into_iter() .flatten() - .collect(); + .collect::>(); + + let mut agg_storage_node = pywr_core::AggregatedStorageNodeBuilder::new(self.name()); - network.add_aggregated_storage_node(self.meta.name.as_str(), None, nodes)?; + for node in nodes { + agg_storage_node.node(node); + } + + network.agg_storage_node(agg_storage_node); Ok(()) } - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_aggregated_storage_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = self.name(); let metric = match attr { - AggregatedStorageNodeAttribute::Volume => MetricF64::AggregatedStorageNodeVolume(idx), - AggregatedStorageNodeAttribute::ProportionalVolume => MetricF64::AggregatedNodeProportionalVolume(idx), + AggregatedStorageNodeAttribute::Volume => UnresolvedMetricF64::AggregatedStorageNodeVolume(name), + AggregatedStorageNodeAttribute::ProportionalVolume => { + UnresolvedMetricF64::AggregatedStorageNodeProportionalVolume(name) + } }; Ok(metric) diff --git a/pywr-schema/src/nodes/virtual_nodes/mod.rs b/pywr-schema/src/nodes/virtual_nodes/mod.rs index c0269d5e..3adbfd8e 100644 --- a/pywr-schema/src/nodes/virtual_nodes/mod.rs +++ b/pywr-schema/src/nodes/virtual_nodes/mod.rs @@ -11,7 +11,7 @@ pub use aggregated::{ AggregatedNode, AggregatedNodeAttribute, AggregatedStorageNode, AggregatedStorageNodeAttribute, Relationship, }; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use pywr_core::metric::UnresolvedMetricF64; use schemars::JsonSchema; use std::path::{Path, PathBuf}; use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, IntoStaticStr}; @@ -93,38 +93,25 @@ impl VirtualNode { #[cfg(feature = "core")] impl VirtualNode { - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { - match self { - VirtualNode::Aggregated(n) => n.add_to_model(network, args), - VirtualNode::AggregatedStorage(n) => n.add_to_model(network, args), - VirtualNode::VirtualStorage(n) => n.add_to_model(network, args), - VirtualNode::Placeholder(n) => n.add_to_model(), - } - } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { match self { - VirtualNode::Aggregated(n) => n.set_constraints(network, args), - VirtualNode::AggregatedStorage(_) => Ok(()), // No constraints on aggregated storage nodes. - VirtualNode::VirtualStorage(n) => n.set_constraints(network, args), - VirtualNode::Placeholder(n) => n.set_constraints(), + VirtualNode::Aggregated(n) => n.add_to_network(network, args), + VirtualNode::AggregatedStorage(n) => n.add_to_network(network, args), + VirtualNode::VirtualStorage(n) => n.add_to_network(network, args), + VirtualNode::Placeholder(n) => n.add_to_network(), } } /// Create a metric for the given attribute on this node. - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { match self { - VirtualNode::Aggregated(n) => n.create_metric(network, attribute), - VirtualNode::AggregatedStorage(n) => n.create_metric(network, attribute), - VirtualNode::VirtualStorage(n) => n.create_metric(network, attribute), + VirtualNode::Aggregated(n) => n.create_metric(attribute), + VirtualNode::AggregatedStorage(n) => n.create_metric(attribute), + VirtualNode::VirtualStorage(n) => n.create_metric(attribute), VirtualNode::Placeholder(n) => n.create_metric(), } } diff --git a/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs b/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs index ea2f6044..3702d1f1 100644 --- a/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs +++ b/pywr-schema/src/nodes/virtual_nodes/virtual_storage.rs @@ -11,7 +11,9 @@ use crate::parameters::Parameter; use crate::v1::{ConversionData, TryFromV1, try_convert_initial_storage, try_convert_node_attr, try_convert_node_meta}; use crate::{ConversionError, node_attribute_subset_enum}; #[cfg(feature = "core")] -use pywr_core::{metric::MetricF64, timestep::TimeDomain, virtual_storage::VirtualStorageNodeBuilder}; +use pywr_core::{ + metric::UnresolvedMetricF64, node::UnresolvedNode, timestep::TimeDomain, virtual_storage::VirtualStorageNodeBuilder, +}; use pywr_schema_macros::PywrVisitAll; use pywr_schema_macros::skip_serializing_none; use pywr_v1_schema::nodes::{ @@ -226,12 +228,8 @@ impl VirtualStorageNode { /// /// Note that this is a private function, as it is not supported using this node itself /// inside a flow constraint. - fn node_indices_for_flow_constraints( - &self, - network: &pywr_core::network::Network, - args: &LoadArgs, - ) -> Result, SchemaError> { - let indices = self + fn nodes_for_flow_constraints(&self, args: &LoadArgs) -> Result, SchemaError> { + let nodes = self .nodes .iter() .map(|node_ref| { @@ -240,27 +238,47 @@ impl VirtualStorageNode { .ok_or_else(|| SchemaError::NodeNotFound { name: node_ref.name.to_string(), })? - .node_indices_for_flow_constraints(network, node_ref.component) + .nodes_for_flow_constraints(node_ref.component) }) .collect::, _>>()? .into_iter() .flatten() .collect(); - Ok(indices) + Ok(nodes) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network, args: &LoadArgs) -> Result<(), SchemaError> { - let node_idxs = self.node_indices_for_flow_constraints(network, args)?; + pub fn add_to_network( + &self, + network: &mut pywr_core::network::NetworkBuilder, + args: &LoadArgs, + ) -> Result<(), SchemaError> { + let nodes = self.nodes_for_flow_constraints(args)?; + + let mut builder = VirtualStorageNodeBuilder::new(self.meta.name.as_str(), &nodes); + + builder.initial_volume(self.initial_volume.into()); + + if let Some(cost) = &self.cost { + let value = cost.load(network, args, Some(&self.meta.name))?; + builder.cost(value); + } + + if let Some(min_volume) = &self.min_volume { + let value = min_volume.load(network, args, Some(&self.meta.name))?; + builder.min_volume(value); + } - let mut builder = VirtualStorageNodeBuilder::new(self.meta.name.as_str(), &node_idxs) - .initial_volume(self.initial_volume.into()); + if let Some(max_volume) = &self.max_volume { + let value = max_volume.load(network, args, Some(&self.meta.name))?; + builder.max_volume(value); + } if let Some(r) = self.reset.clone() { let reset = r.try_into()?; - builder = builder.reset(reset); + builder.reset(reset); } if let Some(rv) = &self.reset_volume { - builder = builder.reset_volume((*rv).into()); + builder.reset_volume((*rv).into()); } // Set the active period if this is a seasonal reset @@ -273,7 +291,7 @@ impl VirtualStorageNode { end_day: seasonal.end_day as u32, end_month, }; - builder = builder.active_period(period); + builder.active_period(period); } if let Some(window) = &self.window { @@ -283,61 +301,31 @@ impl VirtualStorageNode { .ok_or_else(|| SchemaError::InvalidRollingWindow { name: self.meta.name.clone(), })?; - builder = builder.rolling_window(rolling_window); + builder.rolling_window(rolling_window); } if let Some(factors) = &self.factors { - builder = builder.factors(factors); + builder.factors(factors); } - network.add_virtual_storage_node(builder)?; + network.virtual_storage_node(builder); Ok(()) } - pub fn set_constraints( - &self, - network: &mut pywr_core::network::Network, - args: &LoadArgs, - ) -> Result<(), SchemaError> { - if let Some(cost) = &self.cost { - let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_virtual_storage_cost(self.meta.name.as_str(), None, value.into())?; - } - - if let Some(min_volume) = &self.min_volume { - let value = min_volume.load(network, args, Some(&self.meta.name))?; - network.set_virtual_storage_min_volume(self.meta.name.as_str(), None, Some(value.try_into()?))?; - } - - if let Some(max_volume) = &self.max_volume { - let value = max_volume.load(network, args, Some(&self.meta.name))?; - network.set_virtual_storage_max_volume(self.meta.name.as_str(), None, Some(value.try_into()?))?; - } - - Ok(()) - } - - pub fn create_metric( - &self, - network: &mut pywr_core::network::Network, - attribute: Option, - ) -> Result { + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, None => Self::DEFAULT_ATTRIBUTE, }; - let idx = network - .get_virtual_storage_node_index_by_name(self.meta.name.as_str(), None) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: None, - })?; + let name = UnresolvedNode::new(self.meta.name.as_str(), None); let metric = match attr { - VirtualStorageNodeAttribute::Volume => MetricF64::VirtualStorageVolume(idx), - VirtualStorageNodeAttribute::ProportionalVolume => MetricF64::VirtualStorageProportionalVolume(idx), + VirtualStorageNodeAttribute::Volume => UnresolvedMetricF64::VirtualStorageVolume(name), + VirtualStorageNodeAttribute::ProportionalVolume => { + UnresolvedMetricF64::VirtualStorageProportionalVolume(name) + } }; Ok(metric) diff --git a/pywr-schema/src/nodes/water_treatment_works.rs b/pywr-schema/src/nodes/water_treatment_works.rs index 7ac5278d..ce486bd2 100644 --- a/pywr-schema/src/nodes/water_treatment_works.rs +++ b/pywr-schema/src/nodes/water_treatment_works.rs @@ -1,15 +1,16 @@ -use crate::error::SchemaError; use crate::metric::Metric; -#[cfg(feature = "core")] -use crate::network::LoadArgs; +use crate::nodes::NodeMeta; use crate::nodes::loss_link::LossFactor; -#[cfg(feature = "core")] -use crate::nodes::{NodeAttribute, NodeComponent}; -use crate::nodes::{NodeMeta, NodeSlot}; use crate::parameters::Parameter; +#[cfg(feature = "core")] +use crate::{ + error::SchemaError, + network::LoadArgs, + nodes::{NodeAttribute, NodeComponent, NodeSlot}, +}; use crate::{mermaid, node_attribute_subset_enum, node_component_subset_enum}; #[cfg(feature = "core")] -use pywr_core::metric::MetricF64; +use pywr_core::{metric::UnresolvedMetricF64, node::UnresolvedNode}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use schemars::JsonSchema; @@ -76,74 +77,65 @@ impl WaterTreatmentWorksNode { const DEFAULT_ATTRIBUTE: WaterTreatmentWorksNodeAttribute = WaterTreatmentWorksNodeAttribute::Outflow; const DEFAULT_COMPONENT: WaterTreatmentWorksNodeComponent = WaterTreatmentWorksNodeComponent::Outflow; - fn loss_sub_name() -> Option<&'static str> { - Some("loss") + pub fn default_attribute(&self) -> WaterTreatmentWorksNodeAttribute { + Self::DEFAULT_ATTRIBUTE } - fn net_sub_name() -> Option<&'static str> { - Some("net") + pub fn default_component(&self) -> WaterTreatmentWorksNodeComponent { + Self::DEFAULT_COMPONENT + } +} + +#[cfg(feature = "core")] +impl WaterTreatmentWorksNode { + fn loss_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("loss")) } - fn net_soft_min_flow_sub_name() -> Option<&'static str> { - Some("net_soft_min_flow") + fn net_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("net")) } - fn net_above_soft_min_flow_sub_name() -> Option<&'static str> { - Some("net_above_soft_min_flow") + fn net_soft_min_flow_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("net_soft_min_flow")) } - pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + fn net_above_soft_min_flow_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("net_above_soft_min_flow")) + } + fn agg_sub_name(&self) -> UnresolvedNode { + UnresolvedNode::new(&self.meta.name, Some("agg")) + } + pub fn input_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::InputNodeSlotNotSupported { slot: slot.clone() }) } else { // Connect directly to the total net - let mut connectors = vec![(self.meta.name.as_str(), Self::net_sub_name().map(|s| s.to_string()))]; + let mut connectors = vec![self.net_sub_name()]; // Only connect to the loss link if it is created if self.loss_factor.is_some() { - connectors.push((self.meta.name.as_str(), Self::loss_sub_name().map(|s| s.to_string()))) + connectors.push(self.loss_sub_name()) } Ok(connectors) } } - pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result)>, SchemaError> { + pub fn output_connectors(&self, slot: Option<&NodeSlot>) -> Result, SchemaError> { if let Some(slot) = slot { Err(SchemaError::OutputNodeSlotNotSupported { slot: slot.clone() }) } else { // Connect to the split of the net flow. Ok(vec![ - ( - self.meta.name.as_str(), - Self::net_soft_min_flow_sub_name().map(|s| s.to_string()), - ), - ( - self.meta.name.as_str(), - Self::net_above_soft_min_flow_sub_name().map(|s| s.to_string()), - ), + self.net_soft_min_flow_sub_name(), + self.net_above_soft_min_flow_sub_name(), ]) } } - pub fn default_attribute(&self) -> WaterTreatmentWorksNodeAttribute { - Self::DEFAULT_ATTRIBUTE - } - - pub fn default_component(&self) -> WaterTreatmentWorksNodeComponent { - Self::DEFAULT_COMPONENT - } -} - -#[cfg(feature = "core")] -impl WaterTreatmentWorksNode { - fn agg_sub_name() -> Option<&'static str> { - Some("agg") - } - - pub fn node_indices_for_flow_constraints( + pub fn nodes_for_flow_constraints( &self, - network: &pywr_core::network::Network, component: Option, - ) -> Result, SchemaError> { + ) -> Result, SchemaError> { // Use the default attribute if none is specified let component = match component { Some(c) => c.try_into()?, @@ -153,127 +145,91 @@ impl WaterTreatmentWorksNode { let indices = match component { WaterTreatmentWorksNodeComponent::Inflow => { // If the loss node is defined, we need to return both the net and loss nodes - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(loss_idx) => { - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - loss_idx, - ] - } - None => vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ], + match self.loss_factor.is_some() { + true => vec![self.net_sub_name(), self.loss_sub_name()], + false => vec![self.net_sub_name()], } } WaterTreatmentWorksNodeComponent::Outflow => { - vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ] + vec![self.net_sub_name()] } WaterTreatmentWorksNodeComponent::Loss => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(idx) => vec![idx], - None => return Ok(vec![]), // No loss node defined, so return empty - } + vec![self.loss_sub_name()] } }; Ok(indices) } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - let idx_net = network.add_link_node(self.meta.name.as_str(), Self::net_sub_name())?; - let idx_soft_min_flow = network.add_link_node(self.meta.name.as_str(), Self::net_soft_min_flow_sub_name())?; - let idx_above_soft_min_flow = - network.add_link_node(self.meta.name.as_str(), Self::net_above_soft_min_flow_sub_name())?; - - // Create the internal connections - network.connect_nodes(idx_net, idx_soft_min_flow)?; - network.connect_nodes(idx_net, idx_above_soft_min_flow)?; - - if self.loss_factor.is_some() { - let idx_loss = network.add_output_node(self.meta.name.as_str(), Self::loss_sub_name())?; - // This aggregated node will contain the factors to enforce the loss - network.add_aggregated_node( - self.meta.name.as_str(), - Self::agg_sub_name(), - &[vec![idx_net], vec![idx_loss]], - None, - )?; - } - - Ok(()) - } - - pub fn set_constraints( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, ) -> Result<(), SchemaError> { + let mut net_node = pywr_core::node::NodeBuilder::link(self.net_sub_name()); + let mut soft_min_flow_node = pywr_core::node::NodeBuilder::link(self.net_soft_min_flow_sub_name()); + let above_soft_min_flow = pywr_core::node::NodeBuilder::link(self.net_above_soft_min_flow_sub_name()); + if let Some(cost) = &self.cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost(self.meta.name.as_str(), Self::net_sub_name(), value.into())?; + net_node.cost(value); } if let Some(max_flow) = &self.max_flow { let value = max_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow(self.meta.name.as_str(), Self::net_sub_name(), value.into())?; + net_node.max_flow(value); } if let Some(min_flow) = &self.min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_min_flow(self.meta.name.as_str(), Self::net_sub_name(), value.into())?; + net_node.min_flow(value); } // soft min flow constraints; This typically applies a negative cost upto a maximum // defined by the `soft_min_flow` if let Some(cost) = &self.soft_min_flow_cost { let value = cost.load(network, args, Some(&self.meta.name))?; - network.set_node_cost( - self.meta.name.as_str(), - Self::net_soft_min_flow_sub_name(), - value.into(), - )?; + soft_min_flow_node.cost(value); } if let Some(min_flow) = &self.soft_min_flow { let value = min_flow.load(network, args, Some(&self.meta.name))?; - network.set_node_max_flow( - self.meta.name.as_str(), - Self::net_soft_min_flow_sub_name(), - value.into(), - )?; + soft_min_flow_node.max_flow(value); } + // Create the internal connections + network.connect(self.net_sub_name(), self.net_soft_min_flow_sub_name()); + network.connect(self.net_sub_name(), self.net_above_soft_min_flow_sub_name()); + if let Some(loss_factor) = &self.loss_factor { + let mut loss_node = pywr_core::node::NodeBuilder::output(self.loss_sub_name()); + let factors = loss_factor.load(network, args, Some(&self.meta.name))?; - 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()))?; + match factors { + Some(relationship) => { + // This aggregated node will contain the factors to enforce the loss + let mut agg_node = pywr_core::AggregatedNodeBuilder::new(self.agg_sub_name()); + agg_node + .nodes(vec![self.net_sub_name()]) + .nodes(vec![self.loss_sub_name()]) + .relationship(relationship); + + network.agg_node(agg_node); + } + None => { + loss_node.max_flow(0.0.into()); + } } - network.set_aggregated_node_relationship(self.meta.name.as_str(), Self::agg_sub_name(), factors)?; + + network.node(loss_node); } + network.node(net_node); + network.node(soft_min_flow_node); + network.node(above_soft_min_flow); + Ok(()) } - pub fn create_metric( - &self, - network: &pywr_core::network::Network, - attribute: Option, - ) -> Result { + + pub fn create_metric(&self, attribute: Option) -> Result { // Use the default attribute if none is specified let attr = match attribute { Some(attr) => attr.try_into()?, @@ -282,49 +238,24 @@ impl WaterTreatmentWorksNode { let metric = match attr { WaterTreatmentWorksNodeAttribute::Inflow => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { + match self.loss_factor.is_some() { // Loss node is defined. The total inflow is the sum of the net and loss nodes; - Some(loss_idx) => { - let indices = vec![ - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - loss_idx, - ]; - MetricF64::MultiNodeInFlow { - indices, + true => { + let nodes = vec![self.net_sub_name(), self.loss_sub_name()]; + UnresolvedMetricF64::MultiNodeInFlow { + nodes, name: self.meta.name.to_string(), } } // No loss node defined, so just use the net node - None => MetricF64::NodeInFlow( - network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?, - ), - } - } - WaterTreatmentWorksNodeAttribute::Outflow => { - let idx = network - .get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name()) - .ok_or_else(|| SchemaError::CoreNodeNotFound { - name: self.meta.name.clone(), - sub_name: Self::net_sub_name().map(String::from), - })?; - MetricF64::NodeOutFlow(idx) - } - WaterTreatmentWorksNodeAttribute::Loss => { - match network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name()) { - Some(idx) => MetricF64::NodeInFlow(idx), - None => 0.0.into(), + false => UnresolvedMetricF64::NodeInFlow(self.net_sub_name()), } } + WaterTreatmentWorksNodeAttribute::Outflow => UnresolvedMetricF64::NodeOutFlow(self.net_sub_name()), + WaterTreatmentWorksNodeAttribute::Loss => match self.loss_factor.is_some() { + true => UnresolvedMetricF64::NodeInFlow(self.loss_sub_name()), + false => 0.0.into(), + }, }; Ok(metric) diff --git a/pywr-schema/src/outputs/csv.rs b/pywr-schema/src/outputs/csv.rs index da2c113d..665045ce 100644 --- a/pywr-schema/src/outputs/csv.rs +++ b/pywr-schema/src/outputs/csv.rs @@ -1,7 +1,7 @@ #[cfg(feature = "core")] use crate::error::SchemaError; #[cfg(feature = "core")] -use pywr_core::recorders::{CsvLongFmtOutput, CsvWideFmtOutput, Recorder}; +use pywr_core::recorders::{CsvLongFmtOutputBuilder, CsvWideFmtOutputBuilder, RecorderBuilder}; use pywr_schema_macros::{PywrVisitPaths, skip_serializing_none}; use schemars::JsonSchema; #[cfg(feature = "core")] @@ -56,7 +56,7 @@ pub struct CsvOutput { impl CsvOutput { pub fn add_to_model( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, output_path: Option<&Path>, ) -> Result<(), SchemaError> { let filename = match (output_path, self.filename.is_relative()) { @@ -64,11 +64,10 @@ impl CsvOutput { _ => self.filename.to_path_buf(), }; - let recorder: Box = match self.format { + let recorder: Box = match self.format { CsvFormat::Wide => match &self.metric_set { CsvMetricSet::Single(metric_set) => { - let metric_set_idx = network.get_metric_set_index_by_name(metric_set)?; - Box::new(CsvWideFmtOutput::new(&self.name, filename, metric_set_idx)) + Box::new(CsvWideFmtOutputBuilder::new(&self.name, filename, metric_set)) } CsvMetricSet::Multiple(_) => { return Err(SchemaError::MissingMetricSet( @@ -77,24 +76,25 @@ impl CsvOutput { } }, CsvFormat::Long => { - let metric_set_indices = match &self.metric_set { - CsvMetricSet::Single(metric_set) => vec![network.get_metric_set_index_by_name(metric_set)?], - CsvMetricSet::Multiple(metric_sets) => metric_sets - .iter() - .map(|ms| network.get_metric_set_index_by_name(ms)) - .collect::, _>>()?, - }; + let mut builder = + CsvLongFmtOutputBuilder::new(&self.name, filename, self.decimal_places.and_then(NonZeroU32::new)); - Box::new(CsvLongFmtOutput::new( - &self.name, - filename, - &metric_set_indices, - self.decimal_places.and_then(NonZeroU32::new), - )) + match &self.metric_set { + CsvMetricSet::Single(metric_set) => { + builder.metric_set(metric_set); + } + CsvMetricSet::Multiple(metric_sets) => { + for metric_set in metric_sets { + builder.metric_set(metric_set); + } + } + } + + Box::new(builder) } }; - network.add_recorder(recorder)?; + network.recorder(recorder); Ok(()) } diff --git a/pywr-schema/src/outputs/hdf.rs b/pywr-schema/src/outputs/hdf.rs index 63aee961..4b95f397 100644 --- a/pywr-schema/src/outputs/hdf.rs +++ b/pywr-schema/src/outputs/hdf.rs @@ -1,7 +1,7 @@ #[cfg(feature = "core")] use crate::error::SchemaError; #[cfg(all(feature = "core", feature = "hdf5"))] -use pywr_core::recorders::HDF5Recorder; +use pywr_core::recorders::HDF5RecorderBuilder; use pywr_schema_macros::PywrVisitPaths; use schemars::JsonSchema; #[cfg(feature = "core")] @@ -20,7 +20,7 @@ pub struct Hdf5Output { impl Hdf5Output { pub fn add_to_model( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, output_path: Option<&Path>, ) -> Result<(), SchemaError> { let filename = match (output_path, self.filename.is_relative()) { @@ -28,11 +28,9 @@ impl Hdf5Output { _ => self.filename.to_path_buf(), }; - let metric_set_idx = network.get_metric_set_index_by_name(&self.metric_set)?; + let recorder = HDF5RecorderBuilder::new(&self.name, filename, &self.metric_set); - let recorder = HDF5Recorder::new(&self.name, filename, metric_set_idx); - - network.add_recorder(Box::new(recorder))?; + network.recorder(Box::new(recorder)); Ok(()) } @@ -92,7 +90,8 @@ mod tests { let temp_dir = TempDir::new().unwrap(); - let model = schema.build_model(None, Some(temp_dir.path())).unwrap(); + let builder = schema.create_model_builder(None, Some(temp_dir.path())).unwrap(); + let model = builder.build().unwrap(); model.run::(&ClpSolverSettings::default()).unwrap(); diff --git a/pywr-schema/src/outputs/memory.rs b/pywr-schema/src/outputs/memory.rs index 6fae9ab0..664d999c 100644 --- a/pywr-schema/src/outputs/memory.rs +++ b/pywr-schema/src/outputs/memory.rs @@ -2,7 +2,7 @@ use crate::SchemaError; use crate::agg_funcs::AggFunc; #[cfg(feature = "core")] -use pywr_core::recorders::MemoryRecorder; +use pywr_core::recorders::MemoryRecorderBuilder; use pywr_schema_macros::{PywrVisitPaths, skip_serializing_none}; use schemars::JsonSchema; #[cfg(feature = "core")] @@ -57,19 +57,17 @@ pub struct MemoryOutput { impl MemoryOutput { pub fn add_to_model( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, data_path: Option<&Path>, ) -> Result<(), SchemaError> { - let metric_set_idx = network.get_metric_set_index_by_name(&self.metric_set)?; - - let recorder = MemoryRecorder::new( + let recorder = MemoryRecorderBuilder::new( &self.name, - metric_set_idx, + &self.metric_set, self.aggregation.clone().unwrap_or_default().load(data_path)?, self.order.map(|o| o.into()).unwrap_or_default(), ); - network.add_recorder(Box::new(recorder))?; + network.recorder(Box::new(recorder)); Ok(()) } @@ -109,7 +107,8 @@ mod tests { let temp_dir = TempDir::new().unwrap(); - let model = schema.build_model(None, Some(temp_dir.path())).unwrap(); + let builder = schema.create_model_builder(None, Some(temp_dir.path())).unwrap(); + let model = builder.build().unwrap(); let result = model.run::(&ClpSolverSettings::default()).unwrap(); diff --git a/pywr-schema/src/outputs/mod.rs b/pywr-schema/src/outputs/mod.rs index bd539b88..ac420b16 100644 --- a/pywr-schema/src/outputs/mod.rs +++ b/pywr-schema/src/outputs/mod.rs @@ -29,7 +29,7 @@ pub enum Output { impl Output { pub fn add_to_model( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, data_path: Option<&Path>, output_path: Option<&Path>, ) -> Result<(), SchemaError> { diff --git a/pywr-schema/src/parameters/aggregated.rs b/pywr-schema/src/parameters/aggregated.rs index 3042e2fc..c6cb7dd8 100644 --- a/pywr-schema/src/parameters/aggregated.rs +++ b/pywr-schema/src/parameters/aggregated.rs @@ -8,7 +8,7 @@ use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::PywrVisitAll; use pywr_v1_schema::parameters::{ AggregatedIndexParameter as AggregatedIndexParameterV1, AggregatedParameter as AggregatedParameterV1, @@ -49,25 +49,25 @@ pub struct AggregatedParameter { #[cfg(feature = "core")] impl AggregatedParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let metrics = self - .metrics - .iter() - .map(|v| v.load(network, args, None)) - .collect::, _>>()?; - - let p = pywr_core::parameters::AggregatedParameter::new( + ) -> Result<(), SchemaError> { + let mut builder = pywr_core::parameters::AggregatedParameterBuilder::new( ParameterName::new(&self.meta.name, parent), - &metrics, self.agg_func.load(args.data_path)?, ); - Ok(network.add_parameter(Box::new(p))?) + for metric in &self.metrics { + let m = metric.load(network, args, parent)?; + builder.metric(m); + } + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } @@ -121,25 +121,25 @@ impl AggregatedIndexParameter { #[cfg(feature = "core")] impl AggregatedIndexParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let metrics = self - .metrics - .iter() - .map(|v| v.load(network, args, None)) - .collect::, _>>()?; - - let p = pywr_core::parameters::AggregatedIndexParameter::new( + ) -> Result<(), SchemaError> { + let mut builder = pywr_core::parameters::AggregatedIndexParameterBuilder::new( ParameterName::new(&self.meta.name, parent), - &metrics, self.agg_func.load(args.data_path)?, ); - Ok(network.add_index_parameter(Box::new(p))?) + for metric in &self.metrics { + let m = metric.load(network, args, parent)?; + builder.metric(m); + } + + network.parameters().u64(Box::new(builder)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/asymmetric_switch.rs b/pywr-schema/src/parameters/asymmetric_switch.rs index 4aa38ad6..3be1a238 100644 --- a/pywr-schema/src/parameters/asymmetric_switch.rs +++ b/pywr-schema/src/parameters/asymmetric_switch.rs @@ -8,7 +8,7 @@ use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::PywrVisitAll; use pywr_v1_schema::parameters::AsymmetricSwitchIndexParameter as AsymmetricSwitchIndexParameterV1; use schemars::JsonSchema; @@ -23,22 +23,24 @@ pub struct AsymmetricSwitchIndexParameter { #[cfg(feature = "core")] impl AsymmetricSwitchIndexParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let on_index_parameter = self.on_index_parameter.load(network, args, None)?; let off_index_parameter = self.off_index_parameter.load(network, args, None)?; - let p = pywr_core::parameters::AsymmetricSwitchIndexParameter::new( + let p = pywr_core::parameters::AsymmetricSwitchIndexParameterBuilder::new( ParameterName::new(&self.meta.name, parent), on_index_parameter, off_index_parameter, ); - Ok(network.add_index_parameter(Box::new(p))?) + network.parameters().u64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/control_curves.rs b/pywr-schema/src/parameters/control_curves.rs index b6dc372f..6354327e 100644 --- a/pywr-schema/src/parameters/control_curves.rs +++ b/pywr-schema/src/parameters/control_curves.rs @@ -9,7 +9,7 @@ use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_control_curves, try_convert_parameter_attr}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::{ParameterName, PiecewiseInterpolatedParameterBuilder}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::{ ControlCurveIndexParameter as ControlCurveIndexParameterV1, @@ -30,33 +30,42 @@ pub struct ControlCurveInterpolatedParameter { #[cfg(feature = "core")] impl ControlCurveInterpolatedParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.storage_metric.load(network, args, None)?; let control_curves = self .control_curves .iter() .map(|cc| cc.load(network, args, None)) - .collect::>()?; + .collect::, _>>()?; let values = self .values .iter() .map(|val| val.load(network, args, None)) - .collect::>()?; + .collect::, _>>()?; - let p = pywr_core::parameters::ControlCurveInterpolatedParameter::new( + let mut p = pywr_core::parameters::ControlCurveInterpolatedParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, - control_curves, - values, ); - Ok(network.add_parameter(Box::new(p))?) + + for cc in control_curves { + p.control_curve(cc); + } + + for v in values { + p.value(v); + } + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -140,26 +149,25 @@ pub struct ControlCurveIndexParameter { #[cfg(feature = "core")] impl ControlCurveIndexParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let metric = self.storage_metric.load(network, args, None)?; - - let control_curves = self - .control_curves - .iter() - .map(|cc| cc.load(network, args, None)) - .collect::>()?; - - let p = pywr_core::parameters::ControlCurveIndexParameter::new( + ) -> Result<(), SchemaError> { + let metric = self.storage_metric.load(network, args, parent)?; + let mut builder = pywr_core::parameters::ControlCurveIndexParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, - control_curves, ); - Ok(network.add_index_parameter(Box::new(p))?) + + for cc in self.control_curves.iter() { + builder.control_curve(cc.load(network, args, parent)?); + } + + network.parameters().u64(Box::new(builder)); + + Ok(()) } } @@ -259,33 +267,30 @@ pub struct ControlCurveParameter { #[cfg(feature = "core")] impl ControlCurveParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.storage_metric.load(network, args, None)?; - let control_curves = self - .control_curves - .iter() - .map(|cc| cc.load(network, args, None)) - .collect::>()?; - - let values = self - .values - .iter() - .map(|val| val.load(network, args, None)) - .collect::>()?; - - let p = pywr_core::parameters::ControlCurveParameter::new( + let mut builder = pywr_core::parameters::ControlCurveParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, - control_curves, - values, ); - Ok(network.add_parameter(Box::new(p))?) + + for cc in self.control_curves.iter() { + builder.control_curve(cc.load(network, args, parent)?); + } + + for v in self.values.iter() { + builder.value(v.load(network, args, parent)?); + } + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } @@ -363,34 +368,33 @@ pub struct ControlCurvePiecewiseInterpolatedParameter { #[cfg(feature = "core")] impl ControlCurvePiecewiseInterpolatedParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let metric = self.storage_metric.load(network, args, None)?; - - let control_curves = self - .control_curves - .iter() - .map(|cc| cc.load(network, args, None)) - .collect::>()?; - - let values = match &self.values { - None => Vec::new(), - Some(values) => values.clone(), - }; + ) -> Result<(), SchemaError> { + let metric = self.storage_metric.load(network, args, parent)?; - let p = pywr_core::parameters::PiecewiseInterpolatedParameter::new( + let mut builder = PiecewiseInterpolatedParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, - control_curves, - values, self.maximum.unwrap_or(1.0), self.minimum.unwrap_or(0.0), ); - Ok(network.add_parameter(Box::new(p))?) + + for cc in &self.control_curves { + builder.control_curve(cc.load(network, args, parent)?); + } + + if let Some(values) = &self.values { + for value in values { + builder.value(*value); + } + } + + network.parameters().f64(Box::new(builder)); + Ok(()) } } diff --git a/pywr-schema/src/parameters/core.rs b/pywr-schema/src/parameters/core.rs index 5d2900d8..d81ea436 100644 --- a/pywr-schema/src/parameters/core.rs +++ b/pywr-schema/src/parameters/core.rs @@ -7,7 +7,7 @@ use crate::network::LoadArgs; use crate::parameters::{ConstantFloatVec, ConstantValue, ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr, try_convert_values}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::{ ConstantParameter as ConstantParameterV1, ConstantScenarioParameter as ConstantScenarioParameterV1, @@ -171,15 +171,16 @@ pub struct ConstantParameter { #[cfg(feature = "core")] impl ConstantParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let name = ParameterName::new(&self.meta.name, parent); - let p = pywr_core::parameters::ConstantParameter::new(name, self.value.load(args.tables)?); - Ok(network.add_const_parameter(Box::new(p))?) + let builder = pywr_core::parameters::ConstantParameterBuilder::new(name, self.value.load(args.tables)?); + network.parameters().f64(Box::new(builder)); + Ok(()) } } @@ -232,28 +233,21 @@ pub struct ConstantScenarioParameter { #[cfg(feature = "core")] impl ConstantScenarioParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let name = ParameterName::new(&self.meta.name, parent); - let scenario_group_index = args.domain.scenarios().group_index(&self.scenario_group)?; + let values = self.values.load(args.tables)?; - let scenario_group_size = args.domain.scenarios().group_size(&self.scenario_group)?; - if values.len() != scenario_group_size { - return Err(SchemaError::ScenarioValuesLengthMismatch { - name: name.to_string(), - values: values.len(), - scenarios: scenario_group_size, - group: self.scenario_group.clone(), - }); - } + let p = pywr_core::parameters::ConstantScenarioParameterBuilder::new(name, values, &self.scenario_group); + + network.parameters().f64(Box::new(p)); - let p = pywr_core::parameters::ConstantScenarioParameter::new(name, values, scenario_group_index); - Ok(network.add_const_parameter(Box::new(p))?) + Ok(()) } } @@ -289,17 +283,24 @@ pub struct MaxParameter { #[cfg(feature = "core")] impl MaxParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let idx = self.parameter.load(network, args, None)?; let threshold = self.threshold.unwrap_or(0.0); - let p = pywr_core::parameters::MaxParameter::new(ParameterName::new(&self.meta.name, parent), idx, threshold); - Ok(network.add_parameter(Box::new(p))?) + let p = pywr_core::parameters::MaxParameterBuilder::new( + ParameterName::new(&self.meta.name, parent), + idx, + threshold, + ); + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -347,17 +348,19 @@ pub struct DivisionParameter { #[cfg(feature = "core")] impl DivisionParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let n = self.numerator.load(network, args, None)?; let d = self.denominator.load(network, args, None)?; - let p = pywr_core::parameters::DivisionParameter::new(ParameterName::new(&self.meta.name, parent), n, d); - Ok(network.add_parameter(Box::new(p))?) + let p = pywr_core::parameters::DivisionParameterBuilder::new(ParameterName::new(&self.meta.name, parent), n, d); + + network.parameters().f64(Box::new(p)); + Ok(()) } } @@ -408,17 +411,23 @@ pub struct MinParameter { #[cfg(feature = "core")] impl MinParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let idx = self.parameter.load(network, args, None)?; + ) -> Result<(), SchemaError> { + let metric = self.parameter.load(network, args, None)?; let threshold = self.threshold.unwrap_or(0.0); - let p = pywr_core::parameters::MinParameter::new(ParameterName::new(&self.meta.name, parent), idx, threshold); - Ok(network.add_parameter(Box::new(p))?) + let p = pywr_core::parameters::MinParameterBuilder::new( + ParameterName::new(&self.meta.name, parent), + metric, + threshold, + ); + + network.parameters().f64(Box::new(p)); + Ok(()) } } @@ -453,16 +462,20 @@ pub struct NegativeParameter { #[cfg(feature = "core")] impl NegativeParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let idx = self.parameter.load(network, args, None)?; + ) -> Result<(), SchemaError> { + let metric = self.parameter.load(network, args, None)?; + + let p = + pywr_core::parameters::NegativeParameterBuilder::new(ParameterName::new(&self.meta.name, parent), metric); + + network.parameters().f64(Box::new(p)); - let p = pywr_core::parameters::NegativeParameter::new(ParameterName::new(&self.meta.name, parent), idx); - Ok(network.add_parameter(Box::new(p))?) + Ok(()) } } @@ -509,21 +522,24 @@ pub struct NegativeMaxParameter { #[cfg(feature = "core")] impl NegativeMaxParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let idx = self.metric.load(network, args, None)?; + ) -> Result<(), SchemaError> { + let metric = self.metric.load(network, args, None)?; let threshold = self.threshold.unwrap_or(0.0); - let p = pywr_core::parameters::NegativeMaxParameter::new( + let p = pywr_core::parameters::NegativeMaxParameterBuilder::new( ParameterName::new(&self.meta.name, parent), - idx, + metric, threshold, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -574,21 +590,24 @@ pub struct NegativeMinParameter { #[cfg(feature = "core")] impl NegativeMinParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let idx = self.metric.load(network, args, None)?; + ) -> Result<(), SchemaError> { + let metric = self.metric.load(network, args, None)?; let threshold = self.threshold.unwrap_or(0.0); - let p = pywr_core::parameters::NegativeMinParameter::new( + let p = pywr_core::parameters::NegativeMinParameterBuilder::new( ParameterName::new(&self.meta.name, parent), - idx, + metric, threshold, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/delay.rs b/pywr-schema/src/parameters/delay.rs index 9bebc7d1..7278898b 100644 --- a/pywr-schema/src/parameters/delay.rs +++ b/pywr-schema/src/parameters/delay.rs @@ -4,9 +4,8 @@ use crate::metric::{IndexMetric, Metric}; #[cfg(feature = "core")] use crate::network::LoadArgs; use crate::parameters::ParameterMeta; - #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::PywrVisitAll; use schemars::JsonSchema; @@ -22,20 +21,23 @@ pub struct DelayParameter { #[cfg(feature = "core")] impl DelayParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; - let p = pywr_core::parameters::DelayParameter::new( + let p = pywr_core::parameters::DelayParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, self.delay, self.initial_value, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -51,19 +53,22 @@ pub struct DelayIndexParameter { #[cfg(feature = "core")] impl DelayIndexParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; - let p = pywr_core::parameters::DelayParameter::new( + let p = pywr_core::parameters::DelayParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, self.delay, self.initial_value, ); - Ok(network.add_index_parameter(Box::new(p))?) + + network.parameters().u64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/discount_factor.rs b/pywr-schema/src/parameters/discount_factor.rs index da9fe428..1df71078 100644 --- a/pywr-schema/src/parameters/discount_factor.rs +++ b/pywr-schema/src/parameters/discount_factor.rs @@ -6,9 +6,8 @@ use crate::metric::Metric; use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2}; - #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::PywrVisitAll; use pywr_v1_schema::parameters::DiscountFactorParameter as DiscountFactorParameterV1; use schemars::JsonSchema; @@ -24,19 +23,22 @@ pub struct DiscountFactorParameter { #[cfg(feature = "core")] impl DiscountFactorParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let discount_rate = self.discount_rate.load(network, args, None)?; - let p = pywr_core::parameters::DiscountFactorParameter::new( + let p = pywr_core::parameters::DiscountFactorParameterBuilder::new( ParameterName::new(&self.meta.name, parent), discount_rate, self.base_year, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/hydropower.rs b/pywr-schema/src/parameters/hydropower.rs index d356fb53..9d7d1a98 100644 --- a/pywr-schema/src/parameters/hydropower.rs +++ b/pywr-schema/src/parameters/hydropower.rs @@ -6,9 +6,8 @@ use crate::metric::Metric; use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; - #[cfg(feature = "core")] -use pywr_core::parameters::{HydropowerTargetData, ParameterIndex, ParameterName}; +use pywr_core::parameters::{HydropowerTargetData, ParameterName}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::HydropowerTargetParameter as HydropowerTargetParameterV1; use schemars::JsonSchema; @@ -85,12 +84,12 @@ pub struct HydropowerTargetParameter { #[cfg(feature = "core")] impl HydropowerTargetParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let target = self.target.as_ref().map(|t| t.load(network, args, None)).transpose()?; let water_elevation = self .water_elevation @@ -126,11 +125,14 @@ impl HydropowerTargetParameter { flow_unit_conversion: self.flow_unit_conversion, energy_unit_conversion: self.energy_unit_conversion, }; - let p = pywr_core::parameters::HydropowerTargetParameter::new( + let p = pywr_core::parameters::HydropowerTargetParameterBuilder::new( ParameterName::new(&self.meta.name, parent), turbine_data, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/indexed_array.rs b/pywr-schema/src/parameters/indexed_array.rs index 9690da21..0e064a53 100644 --- a/pywr-schema/src/parameters/indexed_array.rs +++ b/pywr-schema/src/parameters/indexed_array.rs @@ -6,9 +6,8 @@ use crate::metric::{IndexMetric, Metric}; use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; - #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::PywrVisitAll; use pywr_v1_schema::parameters::IndexedArrayParameter as IndexedArrayParameterV1; use schemars::JsonSchema; @@ -24,27 +23,26 @@ pub struct IndexedArrayParameter { #[cfg(feature = "core")] impl IndexedArrayParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let index_parameter = self.index_parameter.load(network, args, None)?; - let metrics = self - .metrics - .iter() - .map(|v| v.load(network, args, None)) - .collect::, _>>()?; - - let p = pywr_core::parameters::IndexedArrayParameter::new( + let mut builder = pywr_core::parameters::IndexedArrayParameterBuilder::new( ParameterName::new(&self.meta.name, parent), index_parameter, - &metrics, ); - Ok(network.add_parameter(Box::new(p))?) + for m in &self.metrics { + builder.metric(m.load(network, args, parent)?); + } + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/interpolated.rs b/pywr-schema/src/parameters/interpolated.rs index 24ee1e17..0893c946 100644 --- a/pywr-schema/src/parameters/interpolated.rs +++ b/pywr-schema/src/parameters/interpolated.rs @@ -7,9 +7,8 @@ use crate::metric::{Metric, NodeAttrReference}; use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; - #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::{ InterpolatedFlowParameter as InterpolatedFlowParameterV1, @@ -35,12 +34,12 @@ pub struct InterpolatedParameter { #[cfg(feature = "core")] impl InterpolatedParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let x = self.x.load(network, args, None)?; // Sense check the points @@ -64,13 +63,17 @@ impl InterpolatedParameter { let points = xp.into_iter().zip(fp).collect::>(); - let p = pywr_core::parameters::InterpolatedParameter::new( + let mut builder = pywr_core::parameters::InterpolatedParameterBuilder::new( ParameterName::new(&self.meta.name, parent), x, points, - self.error_on_bounds.unwrap_or(true), ); - Ok(network.add_parameter(Box::new(p))?) + + builder.error_on_bounds(self.error_on_bounds.unwrap_or(true)); + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/mod.rs b/pywr-schema/src/parameters/mod.rs index 271d6e4b..7e3bbb6f 100644 --- a/pywr-schema/src/parameters/mod.rs +++ b/pywr-schema/src/parameters/mod.rs @@ -2,7 +2,7 @@ //! //! The enum [`Parameter`] contains all of the valid Pywr parameter schemas. The parameter //! variants define separate schemas for different parameter types. When a network is generated -//! from a schema the parameter schemas are added to the network using [`Parameter::add_to_model`]. +//! from a schema the parameter schemas are added to the network using [`Parameter::add_to_network`]. //! This typically adds a struct from [`crate::parameters`] to the network using the data //! defined in the schema. //! @@ -182,102 +182,50 @@ impl Parameter { #[cfg(feature = "core")] impl Parameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { - let ty = match self { - Self::Constant(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::ConstantScenario(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::ControlCurveInterpolated(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Aggregated(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::AggregatedIndex(p) => { - pywr_core::parameters::ParameterType::Index(p.add_to_model(network, args, parent)?) - } - Self::AsymmetricSwitchIndex(p) => { - pywr_core::parameters::ParameterType::Index(p.add_to_model(network, args, parent)?) - } - Self::ControlCurvePiecewiseInterpolated(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::ControlCurveIndex(p) => { - pywr_core::parameters::ParameterType::Index(p.add_to_model(network, args, parent)?) - } - Self::ControlCurve(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::DailyProfile(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::IndexedArray(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::MonthlyProfile(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::WeeklyProfile(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::UniformDrawdownProfile(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Max(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?), - Self::Min(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?), - Self::Negative(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Polynomial1D(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Threshold(p) => p.add_to_model(network, args, parent)?, - Self::TablesArray(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Python(p) => p.add_to_model(network, args, parent)?, - Self::Delay(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?), - Self::DelayIndex(p) => pywr_core::parameters::ParameterType::Index(p.add_to_model(network, args, parent)?), - Self::Division(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Offset(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?), - Self::DiscountFactor(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Interpolated(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::RbfProfile(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, parent)?), - Self::NegativeMax(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::NegativeMin(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::HydropowerTarget(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - Self::Rolling(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?), - Self::RollingIndex(p) => { - pywr_core::parameters::ParameterType::Index(p.add_to_model(network, args, parent)?) - } - Self::Placeholder(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model()?), - Self::MultiThreshold(p) => p.add_to_model(network, args, parent)?, - Self::DiurnalProfile(p) => { - pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args, parent)?) - } - }; - - Ok(ty) + ) -> Result<(), SchemaError> { + match self { + Self::Constant(p) => p.add_to_network(network, args, parent), + Self::ConstantScenario(p) => p.add_to_network(network, args, parent), + Self::ControlCurveInterpolated(p) => p.add_to_network(network, args, parent), + Self::Aggregated(p) => p.add_to_network(network, args, parent), + Self::AggregatedIndex(p) => p.add_to_network(network, args, parent), + Self::AsymmetricSwitchIndex(p) => p.add_to_network(network, args, parent), + Self::ControlCurvePiecewiseInterpolated(p) => p.add_to_network(network, args, parent), + Self::ControlCurveIndex(p) => p.add_to_network(network, args, parent), + Self::ControlCurve(p) => p.add_to_network(network, args, parent), + Self::DailyProfile(p) => p.add_to_network(network, args, parent), + Self::IndexedArray(p) => p.add_to_network(network, args, parent), + Self::MonthlyProfile(p) => p.add_to_network(network, args, parent), + Self::WeeklyProfile(p) => p.add_to_network(network, args, parent), + Self::UniformDrawdownProfile(p) => p.add_to_network(network, args, parent), + Self::Max(p) => p.add_to_network(network, args, parent), + Self::Min(p) => p.add_to_network(network, args, parent), + Self::Negative(p) => p.add_to_network(network, args, parent), + Self::Polynomial1D(p) => p.add_to_network(network, args, parent), + Self::Threshold(p) => p.add_to_network(network, args, parent), + Self::TablesArray(p) => p.add_to_network(network, args, parent), + Self::Python(p) => p.add_to_network(network, args, parent), + Self::Delay(p) => p.add_to_network(network, args, parent), + Self::DelayIndex(p) => p.add_to_network(network, args, parent), + Self::Division(p) => p.add_to_network(network, args, parent), + Self::Offset(p) => p.add_to_network(network, args, parent), + Self::DiscountFactor(p) => p.add_to_network(network, args, parent), + Self::Interpolated(p) => p.add_to_network(network, args, parent), + Self::RbfProfile(p) => p.add_to_network(network, parent), + Self::NegativeMax(p) => p.add_to_network(network, args, parent), + Self::NegativeMin(p) => p.add_to_network(network, args, parent), + Self::HydropowerTarget(p) => p.add_to_network(network, args, parent), + Self::Rolling(p) => p.add_to_network(network, args, parent), + Self::RollingIndex(p) => p.add_to_network(network, args, parent), + Self::Placeholder(p) => p.add_to_network(), + Self::MultiThreshold(p) => p.add_to_network(network, args, parent), + Self::DiurnalProfile(p) => p.add_to_network(network, args, parent), + } } } diff --git a/pywr-schema/src/parameters/offset.rs b/pywr-schema/src/parameters/offset.rs index 22368953..06c04748 100644 --- a/pywr-schema/src/parameters/offset.rs +++ b/pywr-schema/src/parameters/offset.rs @@ -4,9 +4,8 @@ use crate::metric::Metric; #[cfg(feature = "core")] use crate::network::LoadArgs; use crate::parameters::{ConstantValue, ParameterMeta, VariableSettings}; - #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use schemars::JsonSchema; @@ -44,19 +43,22 @@ pub struct OffsetParameter { #[cfg(feature = "core")] impl OffsetParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let idx = self.metric.load(network, args, None)?; + ) -> Result<(), SchemaError> { + let metric = self.metric.load(network, args, None)?; - let p = pywr_core::parameters::OffsetParameter::new( + let p = pywr_core::parameters::OffsetParameterBuilder::new( ParameterName::new(&self.meta.name, parent), - idx, + metric, self.offset.load(args.tables)?, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/placeholder.rs b/pywr-schema/src/parameters/placeholder.rs index d959a8eb..21304c4e 100644 --- a/pywr-schema/src/parameters/placeholder.rs +++ b/pywr-schema/src/parameters/placeholder.rs @@ -1,8 +1,6 @@ #[cfg(feature = "core")] use crate::SchemaError; use crate::parameters::ParameterMeta; -#[cfg(feature = "core")] -use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; use schemars::JsonSchema; @@ -21,7 +19,7 @@ pub struct PlaceholderParameter { #[cfg(feature = "core")] impl PlaceholderParameter { - pub fn add_to_model(&self) -> Result, SchemaError> { + pub fn add_to_network(&self) -> Result<(), SchemaError> { Err(SchemaError::PlaceholderParameterNotAllowed { name: self.meta.name.clone(), }) @@ -43,7 +41,7 @@ mod test { }; // Attempt to add the placeholder parameter to a model - let result = placeholder.add_to_model(); + let result = placeholder.add_to_network(); assert!(result.is_err()); assert!(matches!( result, diff --git a/pywr-schema/src/parameters/polynomial.rs b/pywr-schema/src/parameters/polynomial.rs index 9989b252..e7d8ee7d 100644 --- a/pywr-schema/src/parameters/polynomial.rs +++ b/pywr-schema/src/parameters/polynomial.rs @@ -7,9 +7,8 @@ use crate::network::LoadArgs; use crate::nodes::NodeAttribute; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2}; - #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::Polynomial1DParameter as Polynomial1DParameterV1; use schemars::JsonSchema; @@ -27,22 +26,27 @@ pub struct Polynomial1DParameter { #[cfg(feature = "core")] impl Polynomial1DParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; - let p = pywr_core::parameters::Polynomial1DParameter::new( + let mut builder = pywr_core::parameters::Polynomial1DParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, self.coefficients.clone(), - self.scale.unwrap_or(1.0), - self.offset.unwrap_or(0.0), ); - Ok(network.add_parameter(Box::new(p))?) + + builder + .scale(self.scale.unwrap_or(1.0)) + .offset(self.offset.unwrap_or(0.0)); + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/profiles.rs b/pywr-schema/src/parameters/profiles.rs index 07084cdf..729b0ba0 100644 --- a/pywr-schema/src/parameters/profiles.rs +++ b/pywr-schema/src/parameters/profiles.rs @@ -6,7 +6,7 @@ use crate::network::LoadArgs; use crate::parameters::{ConstantFloatVec, ConstantValue, ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_values}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName, WeeklyProfileError, WeeklyProfileValues}; +use pywr_core::parameters::{ParameterName, WeeklyProfileError, WeeklyProfileValues}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::{ DailyProfileParameter as DailyProfileParameterV1, MonthInterpDay as MonthInterpDayV1, @@ -31,12 +31,12 @@ pub struct DailyProfileParameter { #[cfg(feature = "core")] impl DailyProfileParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let mut values = self.values.load(args.tables)?; match values.len() { @@ -54,11 +54,14 @@ impl DailyProfileParameter { } } - let p = pywr_core::parameters::DailyProfileParameter::new( + let p = pywr_core::parameters::DailyProfileParameterBuilder::new( ParameterName::new(&self.meta.name, parent), values.try_into().expect("Failed to convert values to [f64; 366]"), ); - Ok(network.add_simple_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -106,12 +109,12 @@ pub struct MonthlyProfileParameter { #[cfg(feature = "core")] impl MonthlyProfileParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let values = self.values.load(args.tables)?; let values: [f64; 12] = values.try_into().map_err(|v: Vec<_>| SchemaError::DataLengthMismatch { @@ -119,12 +122,18 @@ impl MonthlyProfileParameter { found: v.len(), })?; - let p = pywr_core::parameters::MonthlyProfileParameter::new( + let mut builder = pywr_core::parameters::MonthlyProfileParameterBuilder::new( ParameterName::new(&self.meta.name, parent), values, - self.interp_day.map(|id| id.into()), ); - Ok(network.add_simple_parameter(Box::new(p))?) + + if let Some(interp_day) = self.interp_day { + builder.interp_day(interp_day.into()); + } + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } @@ -171,12 +180,12 @@ pub struct UniformDrawdownProfileParameter { #[cfg(feature = "core")] impl UniformDrawdownProfileParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let reset_day = match &self.reset_day { Some(v) => v.load(args.tables)? as u32, None => 1, @@ -190,13 +199,16 @@ impl UniformDrawdownProfileParameter { None => 0, }; - let p = pywr_core::parameters::UniformDrawdownProfileParameter::new( + let p = pywr_core::parameters::UniformDrawdownProfileParameterBuilder::new( ParameterName::new(&self.meta.name, parent), reset_day, reset_month, residual_days, ); - Ok(network.add_simple_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -368,19 +380,22 @@ pub struct RbfProfileParameter { #[cfg(feature = "core")] impl RbfProfileParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let function = self.function.into_core_rbf(&self.points)?; - let p = pywr_core::parameters::RbfProfileParameter::new( + let p = pywr_core::parameters::RbfProfileParameterBuilder::new( ParameterName::new(&self.meta.name, parent), self.points.clone(), function, ); - Ok(network.add_simple_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -568,13 +583,13 @@ pub struct WeeklyProfileParameter { #[cfg(feature = "core")] impl WeeklyProfileParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - let p = pywr_core::parameters::WeeklyProfileParameter::new( + ) -> Result<(), SchemaError> { + let mut builder = pywr_core::parameters::WeeklyProfileParameterBuilder::new( ParameterName::new(&self.meta.name, parent), WeeklyProfileValues::try_from(self.values.load(args.tables)?.as_slice()).map_err( |err: WeeklyProfileError| SchemaError::LoadParameter { @@ -582,9 +597,15 @@ impl WeeklyProfileParameter { error: err.to_string(), }, )?, - self.interp_day.map(|id| id.into()), ); - Ok(network.add_simple_parameter(Box::new(p))?) + + if let Some(interp_day) = self.interp_day { + builder.interp_day(interp_day.into()); + } + + network.parameters().f64(Box::new(builder)); + + Ok(()) } } @@ -630,12 +651,12 @@ pub struct DirunalProfileParameter { #[cfg(feature = "core")] impl DirunalProfileParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let values = self.values.load(args.tables)?; let values: [f64; 24] = values.try_into().map_err(|v: Vec<_>| SchemaError::DataLengthMismatch { @@ -643,20 +664,26 @@ impl DirunalProfileParameter { found: v.len(), })?; - let p = - pywr_core::parameters::DiurnalProfileParameter::new(ParameterName::new(&self.meta.name, parent), values); - Ok(network.add_simple_parameter(Box::new(p))?) + let p = pywr_core::parameters::DiurnalProfileParameterBuilder::new( + ParameterName::new(&self.meta.name, parent), + values, + ); + + network.parameters().f64(Box::new(p)); + + Ok(()) } } -#[cfg(test)] +#[cfg(all(test, feature = "core"))] mod tests { use super::*; use crate::network::NetworkSchema; use crate::parameters::ParameterMeta; use crate::parameters::{ConstantFloatVec, Parameter}; use pywr_core::models::ModelDomain; - use pywr_core::test_utils::default_time_domain; + use pywr_core::network::NetworkBuilder; + use pywr_core::test_utils::default_domain; #[test] fn add_to_model_with_366_values() { @@ -667,13 +694,14 @@ mod tests { }; let values = ConstantFloatVec::Literal { values: vec![1.0; 366] }; let param = DailyProfileParameter { meta, values }; - let domain: ModelDomain = default_time_domain().into(); + let domain: ModelDomain = default_domain(); let network = NetworkSchema { parameters: Some(vec![Parameter::DailyProfile(param)]), ..Default::default() }; - let result = network.build_network(&domain, None, None, &[]); + let mut builder = NetworkBuilder::default(); + let result = network.add_to_network(&mut builder, &domain, None, None, &[]); assert!(result.is_ok()); } @@ -689,13 +717,14 @@ mod tests { let values = vec![1.0; 365]; let values = ConstantFloatVec::Literal { values }; let param = DailyProfileParameter { meta, values }; - let domain: ModelDomain = default_time_domain().into(); + let domain: ModelDomain = default_domain(); let network = NetworkSchema { parameters: Some(vec![Parameter::DailyProfile(param)]), ..Default::default() }; - let result = network.build_network(&domain, None, None, &[]); + let mut builder = NetworkBuilder::default(); + let result = network.add_to_network(&mut builder, &domain, None, None, &[]); assert!(result.is_ok()); } @@ -709,13 +738,14 @@ mod tests { }; let values = ConstantFloatVec::Literal { values: vec![1.0; 364] }; let param = DailyProfileParameter { meta, values }; - let domain: ModelDomain = default_time_domain().into(); + let domain: ModelDomain = default_domain(); let network = NetworkSchema { parameters: Some(vec![Parameter::DailyProfile(param)]), ..Default::default() }; - let result = network.build_network(&domain, None, None, &[]); + let mut builder = NetworkBuilder::default(); + let result = network.add_to_network(&mut builder, &domain, None, None, &[]); assert!(result.is_err()); } diff --git a/pywr-schema/src/parameters/python.rs b/pywr-schema/src/parameters/python.rs index 9f5d5338..c9fe3bad 100644 --- a/pywr-schema/src/parameters/python.rs +++ b/pywr-schema/src/parameters/python.rs @@ -13,10 +13,8 @@ use pyo3::{ Bound, Python, prelude::{Py, PyAny, PyAnyMethods, PyModule}, }; -#[cfg(feature = "core")] -use pywr_core::parameters::ParameterType; #[cfg(all(feature = "core", feature = "pyo3"))] -use pywr_core::parameters::{ParameterName, PyClassParameter, PyFuncParameter}; +use pywr_core::parameters::{ParameterName, PyClassParameterBuilder, PyFuncParameterBuilder}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use schemars::JsonSchema; use serde_json::Value; @@ -205,12 +203,12 @@ impl PythonParameter { } #[cfg(all(feature = "core", feature = "pyo3"))] impl PythonParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { + ) -> Result<(), SchemaError> { Python::initialize(); let object = Python::attach(|py| { @@ -239,42 +237,54 @@ impl PythonParameter { None => HashMap::new(), }; - let pt = match object { + match object { PyObj::Class(py_class) => { - let p = PyClassParameter::new( + let mut builder = PyClassParameterBuilder::new( ParameterName::new(&self.meta.name, parent), py_class, py_args, py_kwargs, - &metrics, - &indices, ); - match self.return_type { - PythonReturnType::Float => network.add_parameter(Box::new(p))?.into(), - PythonReturnType::Int => ParameterType::Index(network.add_index_parameter(Box::new(p))?), - PythonReturnType::Dict => ParameterType::Multi(network.add_multi_value_parameter(Box::new(p))?), + for (k, m) in metrics.into_iter() { + builder.metric(&k, m); } + + for (k, v) in indices.into_iter() { + builder.index(&k, v); + } + + match self.return_type { + PythonReturnType::Float => network.parameters().f64(Box::new(builder)), + PythonReturnType::Int => network.parameters().u64(Box::new(builder)), + PythonReturnType::Dict => network.parameters().multi(Box::new(builder)), + }; } PyObj::Function(py_function) => { - let p = PyFuncParameter::new( + let mut builder = PyFuncParameterBuilder::new( ParameterName::new(&self.meta.name, parent), py_function, py_args, py_kwargs, - &metrics, - &indices, ); - match self.return_type { - PythonReturnType::Float => network.add_parameter(Box::new(p))?.into(), - PythonReturnType::Int => ParameterType::Index(network.add_index_parameter(Box::new(p))?), - PythonReturnType::Dict => ParameterType::Multi(network.add_multi_value_parameter(Box::new(p))?), + for (k, m) in metrics.into_iter() { + builder.metric(&k, m); + } + + for (k, v) in indices.into_iter() { + builder.index(&k, v); } + + match self.return_type { + PythonReturnType::Float => network.parameters().f64(Box::new(builder)), + PythonReturnType::Int => network.parameters().u64(Box::new(builder)), + PythonReturnType::Dict => network.parameters().multi(Box::new(builder)), + }; } }; - Ok(pt) + Ok(()) } } @@ -287,9 +297,10 @@ mod tests { use crate::timeseries::LoadedTimeseriesCollection; use pyo3::Python; use pywr_core::models::ModelDomain; - use pywr_core::network::Network; - use pywr_core::test_utils::default_time_domain; + use pywr_core::network::NetworkBuilder; + use pywr_core::test_utils::default_domain; use serde_json::json; + use std::collections::HashMap; use std::path::PathBuf; #[test] @@ -322,9 +333,9 @@ mod tests { let param: PythonParameter = serde_json::from_str(data.as_str()).unwrap(); // ... add it to an empty network // this should trigger loading the module and extracting the class - let domain: ModelDomain = default_time_domain().into(); + let domain: ModelDomain = default_domain(); let schema = NetworkSchema::default(); - let mut network = Network::default(); + let mut network = NetworkBuilder::default(); let tables = LoadedTableCollection::from_schema(None, None).unwrap(); let ts = LoadedTimeseriesCollection::default(); @@ -337,7 +348,9 @@ mod tests { inter_network_transfers: &[], }; - param.add_to_model(&mut network, &args, None).unwrap(); + param.add_to_network(&mut network, &args, None).unwrap(); + + let (network, _) = network.build(&domain, &HashMap::new()).unwrap(); assert!(network.get_parameter_by_name(&"my-float-parameter".into()).is_some()); } @@ -373,9 +386,9 @@ mod tests { let param: PythonParameter = serde_json::from_str(data.as_str()).unwrap(); // ... add it to an empty network // this should trigger loading the module and extracting the class - let domain: ModelDomain = default_time_domain().into(); + let domain: ModelDomain = default_domain(); let schema = NetworkSchema::default(); - let mut network = Network::default(); + let mut network = NetworkBuilder::default(); let tables = LoadedTableCollection::from_schema(None, None).unwrap(); let ts = LoadedTimeseriesCollection::default(); @@ -388,7 +401,9 @@ mod tests { inter_network_transfers: &[], }; - param.add_to_model(&mut network, &args, None).unwrap(); + param.add_to_network(&mut network, &args, None).unwrap(); + + let (network, _) = network.build(&domain, &HashMap::new()).unwrap(); assert!( network diff --git a/pywr-schema/src/parameters/rolling.rs b/pywr-schema/src/parameters/rolling.rs index a7169652..7f7927c4 100644 --- a/pywr-schema/src/parameters/rolling.rs +++ b/pywr-schema/src/parameters/rolling.rs @@ -8,7 +8,7 @@ use crate::parameters::ParameterMeta; use crate::v1::TryIntoV2; use crate::{ComponentConversionError, ConversionData, ConversionError, TryFromV1}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterIndex, ParameterName}; +use pywr_core::parameters::ParameterName; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::RollingMeanFlowNodeParameter as RollingMeanFlowNodeParameterV1; use schemars::JsonSchema; @@ -34,14 +34,14 @@ pub struct RollingParameter { #[cfg(feature = "core")] impl RollingParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; - let p = pywr_core::parameters::RollingParameter::new( + let p = pywr_core::parameters::RollingParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, self.window_size as usize, @@ -49,7 +49,10 @@ impl RollingParameter { self.min_values.unwrap_or(self.window_size) as usize, self.agg_func.load(args.data_path)?, ); - Ok(network.add_parameter(Box::new(p))?) + + network.parameters().f64(Box::new(p)); + + Ok(()) } } @@ -74,14 +77,14 @@ pub struct RollingIndexParameter { #[cfg(feature = "core")] impl RollingIndexParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; - let p = pywr_core::parameters::RollingParameter::new( + let p = pywr_core::parameters::RollingParameterBuilder::new( ParameterName::new(&self.meta.name, parent), metric, self.window_size as usize, @@ -89,7 +92,9 @@ impl RollingIndexParameter { self.min_values.unwrap_or(self.window_size) as usize, self.agg_func.load(args.data_path)?, ); - Ok(network.add_index_parameter(Box::new(p))?) + network.parameters().u64(Box::new(p)); + + Ok(()) } } diff --git a/pywr-schema/src/parameters/tables.rs b/pywr-schema/src/parameters/tables.rs index 23eab627..0aa5ce9b 100644 --- a/pywr-schema/src/parameters/tables.rs +++ b/pywr-schema/src/parameters/tables.rs @@ -4,8 +4,6 @@ use crate::error::SchemaError; #[cfg(feature = "core")] use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; -#[cfg(all(feature = "core", feature = "hdf5"))] -use crate::timeseries::subset_array2; use crate::v1::{TryIntoV2, try_convert_parameter_attr}; use crate::{ComponentConversionError, TryFromV1}; #[cfg(all(feature = "core", feature = "hdf5"))] @@ -33,13 +31,13 @@ pub struct TablesArrayParameter { #[cfg(all(feature = "core", feature = "hdf5"))] impl TablesArrayParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result, SchemaError> { - // 1. Load the file from the HDF5 file (NB this is not Pandas format). + ) -> Result<(), SchemaError> { + // Load the file from the HDF5 file (NB this is not Pandas format). // Handle the case of an optional data path with a relative url. let pth = if let Some(dp) = args.data_path { @@ -65,36 +63,33 @@ impl TablesArrayParameter { .dataset(&self.node) .map_err(|e| SchemaError::HDF5Error(e.to_string()))?; // find the dataset - let mut array = ds.read_2d::().map_err(|e| SchemaError::HDF5Error(e.to_string()))?; - // 2. TODO Validate the shape of the data array. I.e. check number of columns matches scenario - // and number of rows matches time-steps. + let array = ds.read_2d::().map_err(|e| SchemaError::HDF5Error(e.to_string()))?; - // 3. Create an ArrayParameter using the loaded array. + // Create an ArrayParameter using the loaded array. if let Some(scenario) = &self.scenario { - let scenario_group_index = args.domain.scenarios().group_index(scenario)?; - - // If there is a scenario subset then we can reduce the data to align with the scenarios - // that are actually used in the model. - if let Some(subset) = args.domain.scenarios().group_scenario_subset(scenario)? { - array = subset_array2(&array, subset)?; - } - - let p = pywr_core::parameters::Array2Parameter::new( + let mut builder = pywr_core::parameters::Array2ParameterBuilder::new( ParameterName::new(&self.meta.name, parent), array, - scenario_group_index, - self.timestep_offset, + scenario, ); - Ok(network.add_simple_parameter(Box::new(p))?) + + if let Some(to) = &self.timestep_offset { + builder.timestep_offset(*to); + } + + network.parameters().f64(Box::new(builder)); } else { let array = array.slice_move(s![.., 0]); - let p = pywr_core::parameters::Array1Parameter::new( - ParameterName::new(&self.meta.name, parent), - array, - self.timestep_offset, - ); - Ok(network.add_simple_parameter(Box::new(p))?) + let mut builder = + pywr_core::parameters::Array1ParameterBuilder::new(ParameterName::new(&self.meta.name, parent), array); + if let Some(to) = &self.timestep_offset { + builder.timestep_offset(*to); + } + + network.parameters().f64(Box::new(builder)); } + + Ok(()) } } diff --git a/pywr-schema/src/parameters/thresholds.rs b/pywr-schema/src/parameters/thresholds.rs index 7b9c5d70..52fdf903 100644 --- a/pywr-schema/src/parameters/thresholds.rs +++ b/pywr-schema/src/parameters/thresholds.rs @@ -8,7 +8,7 @@ use crate::network::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; #[cfg(feature = "core")] -use pywr_core::parameters::{ParameterName, ParameterType}; +use pywr_core::{metric::UnresolvedMetricU64, parameters::ParameterName}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; use pywr_v1_schema::parameters::{ MultipleThresholdIndexParameter as MultiThresholdIndexParameterV1, @@ -105,12 +105,12 @@ pub struct ThresholdParameter { #[cfg(feature = "core")] impl ThresholdParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; let threshold = self.threshold.load(network, args, None)?; @@ -124,32 +124,32 @@ impl ThresholdParameter { ParameterName::new(&self.meta.name, parent) }; - let p = pywr_core::parameters::ThresholdParameter::new( - name, + let mut builder = pywr_core::parameters::ThresholdParameterBuilder::new( + name.clone(), metric, threshold, self.predicate.into(), - self.ratchet, ); - let p_idx = network.add_index_parameter(Box::new(p))?; - - match self.returned_metrics { - Some(ref values) => { - let metrics = values - .iter() - .map(|v| v.load(network, args, None)) - .collect::, _>>()?; - - let values_param = pywr_core::parameters::IndexedArrayParameter::new( - ParameterName::new(&self.meta.name, parent), - p_idx.into_metric_u64_before(), - &metrics, - ); - Ok(network.add_parameter(Box::new(values_param))?.into()) + if self.ratchet { + builder.ratchet(); + } + + network.parameters().u64(Box::new(builder)); + + if let Some(values) = &self.returned_metrics { + let mut values_builder = pywr_core::parameters::IndexedArrayParameterBuilder::new( + ParameterName::new(&self.meta.name, parent), + UnresolvedMetricU64::new_parameter_before(name), + ); + + for v in values { + values_builder.metric(v.load(network, args, None)?); } - None => Ok(p_idx.into()), + network.parameters().f64(Box::new(values_builder)); } + + Ok(()) } } @@ -329,18 +329,13 @@ pub struct MultiThresholdParameter { #[cfg(feature = "core")] impl MultiThresholdParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, args: &LoadArgs, parent: Option<&str>, - ) -> Result { + ) -> Result<(), SchemaError> { let metric = self.metric.load(network, args, None)?; - let thresholds = self - .thresholds - .iter() - .map(|v| v.load(network, args, None)) - .collect::, _>>()?; let name = if self.returned_metrics.is_some() { ParameterName::new_with_subname(&self.meta.name, Some("threshold"), Some(&self.meta.name)) @@ -348,31 +343,33 @@ impl MultiThresholdParameter { self.meta.name.as_str().into() }; - let p = pywr_core::parameters::MultiThresholdParameter::new( - name, - metric, - &thresholds, - self.predicate.into(), - self.ratchet, - ); + let mut builder = + pywr_core::parameters::MultiThresholdParameterBuilder::new(name.clone(), metric, self.predicate.into()); + + for t in &self.thresholds { + builder.threshold(t.load(network, args, parent)?); + } + + if self.ratchet { + builder.ratchet(); + } - let p_idx = network.add_index_parameter(Box::new(p))?; - - match self.returned_metrics { - Some(ref values) => { - let metrics = values - .iter() - .map(|v| v.load(network, args, None)) - .collect::, _>>()?; - let values_param = pywr_core::parameters::IndexedArrayParameter::new( - ParameterName::new(&self.meta.name, parent), - p_idx.into_metric_u64_before(), - &metrics, - ); - Ok(network.add_parameter(Box::new(values_param))?.into()) + network.parameters().u64(Box::new(builder)); + + if let Some(values) = &self.returned_metrics { + let mut values_builder = pywr_core::parameters::IndexedArrayParameterBuilder::new( + ParameterName::new(&self.meta.name, parent), + UnresolvedMetricU64::new_parameter_before(name), + ); + + for v in values { + values_builder.metric(v.load(network, args, None)?); } - None => Ok(p_idx.into()), + + network.parameters().f64(Box::new(values_builder)); } + + Ok(()) } } diff --git a/pywr-schema/src/timeseries/align_and_resample.rs b/pywr-schema/src/timeseries/align_and_resample.rs index 894a74d9..01b4cd18 100644 --- a/pywr-schema/src/timeseries/align_and_resample.rs +++ b/pywr-schema/src/timeseries/align_and_resample.rs @@ -111,28 +111,26 @@ fn slice_end(df: DataFrame, time_col: &str, domain: &TimeDomain) -> Result Result, TimeseriesError> { + ) -> Result { let df = self .timeseries .get(name) @@ -203,21 +203,20 @@ impl LoadedTimeseriesCollection { let array = series.cast(&Float64)?.f64()?.to_ndarray()?.to_owned(); let name = ParameterName::new(col, Some(name)); - match network.get_parameter_index_by_name(&name) { - Some(idx) => Ok(idx), - None => { - let p = Array1Parameter::new(name, array, None); - Ok(network.add_simple_parameter(Box::new(p))?) - } + if !network.parameters().contains_name(&name) { + let p = Array1ParameterBuilder::new(name.clone(), array); + network.parameters().f64(Box::new(p)); } + + Ok(name) } pub fn load_column_usize( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, name: &str, col: &str, - ) -> Result, TimeseriesError> { + ) -> Result { let df = self .timeseries .get(name) @@ -227,20 +226,19 @@ impl LoadedTimeseriesCollection { let array = series.cast(&UInt64)?.u64()?.to_ndarray()?.to_owned(); let name = ParameterName::new(col, Some(name)); - match network.get_index_parameter_index_by_name(&name) { - Some(idx) => Ok(idx), - None => { - let p = Array1Parameter::new(name, array, None); - Ok(network.add_simple_index_parameter(Box::new(p))?) - } + if !network.parameters().contains_name(&name) { + let p = Array1ParameterBuilder::new(name.clone(), array); + network.parameters().u64(Box::new(p)); } + + Ok(name) } pub fn load_single_column_f64( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, name: &str, - ) -> Result, TimeseriesError> { + ) -> Result { let df = self .timeseries .get(name) @@ -262,20 +260,19 @@ impl LoadedTimeseriesCollection { let array = series.cast(&Float64)?.f64()?.to_ndarray()?.to_owned(); let name = ParameterName::new(col, Some(name)); - match network.get_parameter_index_by_name(&name) { - Some(idx) => Ok(idx), - None => { - let p = Array1Parameter::new(name, array, None); - Ok(network.add_simple_parameter(Box::new(p))?) - } + if !network.parameters().contains_name(&name) { + let p = Array1ParameterBuilder::new(name.clone(), array); + network.parameters().f64(Box::new(p)); } + + Ok(name) } pub fn load_single_column_usize( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, name: &str, - ) -> Result, TimeseriesError> { + ) -> Result { let df = self .timeseries .get(name) @@ -297,98 +294,62 @@ impl LoadedTimeseriesCollection { let array = series.cast(&UInt64)?.u64()?.to_ndarray()?.to_owned(); let name = ParameterName::new(col, Some(name)); - match network.get_index_parameter_index_by_name(&name) { - Some(idx) => Ok(idx), - None => { - let p = Array1Parameter::new(name, array, None); - Ok(network.add_simple_index_parameter(Box::new(p))?) - } + if !network.parameters().contains_name(&name) { + let p = Array1ParameterBuilder::new(name.clone(), array); + network.parameters().u64(Box::new(p)); } + + Ok(name) } /// Load a timeseries dataframe as a 2D array F64 parameter. pub fn load_df_f64( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, name: &str, - domain: &ModelDomain, scenario: &str, - ) -> Result, TimeseriesError> { - let scenario_group_index = domain.scenarios().group_index(scenario)?; + ) -> Result { let df = self .timeseries .get(name) .ok_or(TimeseriesError::TimeseriesNotFound(name.to_string()))?; // Original array as loaded from the timeseries - let mut array: Array2 = df.to_ndarray::(IndexOrder::default())?; - - // If there is a scenario subset then we can reduce the data to align with the scenarios - // that are actually used in the model. - if let Some(subset) = domain.scenarios().group_scenario_subset(scenario)? { - array = subset_array2(&array, subset)?; - } + let array: Array2 = df.to_ndarray::(IndexOrder::default())?; let name = ParameterName::new(scenario, Some(name)); - - match network.get_parameter_index_by_name(&name) { - Some(idx) => Ok(idx), - None => { - let p = Array2Parameter::new(name, array, scenario_group_index, None); - Ok(network.add_simple_parameter(Box::new(p))?) - } + if !network.parameters().contains_name(&name) { + let p = Array2ParameterBuilder::new(name.clone(), array, scenario); + network.parameters().f64(Box::new(p)); } + + Ok(name) } /// Load a timeseries dataframe as a 2D array Usize parameter. pub fn load_df_usize( &self, - network: &mut pywr_core::network::Network, + network: &mut pywr_core::network::NetworkBuilder, name: &str, - domain: &ModelDomain, - scenario: &str, - ) -> Result, TimeseriesError> { - let scenario_group_index = domain.scenarios().group_index(scenario)?; + scenario: &str, + ) -> Result { let df = self .timeseries .get(name) .ok_or(TimeseriesError::TimeseriesNotFound(name.to_string()))?; - let mut array: Array2 = df.to_ndarray::(IndexOrder::default())?; - - // If there is a scenario subset then we can reduce the data to align with the scenarios - // that are actually used in the model. - if let Some(subset) = domain.scenarios().group_scenario_subset(scenario)? { - array = subset_array2(&array, subset)?; - } + let array: Array2 = df.to_ndarray::(IndexOrder::default())?; let name = ParameterName::new(scenario, Some(name)); - match network.get_index_parameter_index_by_name(&name) { - Some(idx) => Ok(idx), - None => { - let p = Array2Parameter::new(name, array, scenario_group_index, None); - Ok(network.add_simple_index_parameter(Box::new(p))?) - } + if !network.parameters().contains_name(&name) { + let p = Array2ParameterBuilder::new(name.clone(), array, scenario); + network.parameters().u64(Box::new(p)); } - } -} -/// Create a subset of a 2D array based on the column indices. -/// -/// This function is used to reduce the size of a timeseries dataframe to only include the columns -/// that are used in a simulation. -#[cfg(feature = "core")] -pub fn subset_array2(array: &Array2, subset: &[usize]) -> Result, ShapeError> -where - T: Copy, -{ - // Slice the array to only include the columns that are used in the scenario - let slices = subset.iter().map(|c| array.slice(s![.., *c])).collect::>(); - // Stack the slices to create a new array; this should be infallible because - // the slices are all the same length. - ndarray::stack(ndarray::Axis(1), &slices) + Ok(name) + } } /// Convert timeseries inputs to this schema. diff --git a/pywr-schema/tests/30-day-licence.json b/pywr-schema/tests/30-day-licence.json index 489ff310..0602358a 100644 --- a/pywr-schema/tests/30-day-licence.json +++ b/pywr-schema/tests/30-day-licence.json @@ -4,7 +4,7 @@ "description": "An example of a 30-day rolling licence", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/abstraction1.json b/pywr-schema/tests/abstraction1.json index 6c55e051..01e9ab98 100644 --- a/pywr-schema/tests/abstraction1.json +++ b/pywr-schema/tests/abstraction1.json @@ -3,7 +3,7 @@ "title": "Abstraction 1", "comment": "Test behaviour of an abstraction node constrained by an MRF and licence" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-07", "timestep": { diff --git a/pywr-schema/tests/abstraction2.json b/pywr-schema/tests/abstraction2.json index 9287bfec..3dab4822 100644 --- a/pywr-schema/tests/abstraction2.json +++ b/pywr-schema/tests/abstraction2.json @@ -3,7 +3,7 @@ "title": "Abstraction 1", "comment": "Test behaviour of an abstraction node with no MRF" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-07", "timestep": { diff --git a/pywr-schema/tests/agg-storage1.json b/pywr-schema/tests/agg-storage1.json index 865c250f..535d5a0b 100644 --- a/pywr-schema/tests/agg-storage1.json +++ b/pywr-schema/tests/agg-storage1.json @@ -4,7 +4,7 @@ "description": "Test behaviour of aggregated storage node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-10", "timestep": { diff --git a/pywr-schema/tests/agg-storage2.json b/pywr-schema/tests/agg-storage2.json index 2555d225..a0f3538a 100644 --- a/pywr-schema/tests/agg-storage2.json +++ b/pywr-schema/tests/agg-storage2.json @@ -4,7 +4,7 @@ "description": "Test behaviour of aggregated storage node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-10", "timestep": { diff --git a/pywr-schema/tests/agg-storage3.json b/pywr-schema/tests/agg-storage3.json index 894bbc41..1e330682 100644 --- a/pywr-schema/tests/agg-storage3.json +++ b/pywr-schema/tests/agg-storage3.json @@ -4,7 +4,7 @@ "description": "Test behaviour of aggregated storage node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-10", "timestep": { diff --git a/pywr-schema/tests/csv1.json b/pywr-schema/tests/csv1.json index ac685a16..74ebbf9e 100644 --- a/pywr-schema/tests/csv1.json +++ b/pywr-schema/tests/csv1.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/csv2.json b/pywr-schema/tests/csv2.json index 7ad9455c..9b8cb7e9 100644 --- a/pywr-schema/tests/csv2.json +++ b/pywr-schema/tests/csv2.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/csv3.json b/pywr-schema/tests/csv3.json index 8c3edf27..e3fae2e6 100644 --- a/pywr-schema/tests/csv3.json +++ b/pywr-schema/tests/csv3.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/daily-profile1.json b/pywr-schema/tests/daily-profile1.json index 8249a883..fe16e56d 100644 --- a/pywr-schema/tests/daily-profile1.json +++ b/pywr-schema/tests/daily-profile1.json @@ -4,7 +4,7 @@ "description": "An example of using daily profiles with 365 and 366 values.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2016-03-31", "timestep": { diff --git a/pywr-schema/tests/delay1.json b/pywr-schema/tests/delay1.json index af28e782..d6f8ed30 100644 --- a/pywr-schema/tests/delay1.json +++ b/pywr-schema/tests/delay1.json @@ -4,7 +4,7 @@ "description": "A test of the DelayNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-31", "timestep": { diff --git a/pywr-schema/tests/hdf1.json b/pywr-schema/tests/hdf1.json index 24561f7d..8c38fa6f 100644 --- a/pywr-schema/tests/hdf1.json +++ b/pywr-schema/tests/hdf1.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/invalid/agg-storage-with-flow-node.json b/pywr-schema/tests/invalid/agg-storage-with-flow-node.json index 01664df6..4fdf33b9 100644 --- a/pywr-schema/tests/invalid/agg-storage-with-flow-node.json +++ b/pywr-schema/tests/invalid/agg-storage-with-flow-node.json @@ -4,7 +4,7 @@ "description": "Test that this model is invalid because it uses a flow node with aggregated storage.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-10", "timestep": { diff --git a/pywr-schema/tests/link_with_soft_max.json b/pywr-schema/tests/link_with_soft_max.json index a48c9273..6382cea9 100644 --- a/pywr-schema/tests/link_with_soft_max.json +++ b/pywr-schema/tests/link_with_soft_max.json @@ -4,7 +4,7 @@ "description": "Test LinkNode with soft_max", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/link_with_soft_min.json b/pywr-schema/tests/link_with_soft_min.json index a581acce..76f5b82b 100644 --- a/pywr-schema/tests/link_with_soft_min.json +++ b/pywr-schema/tests/link_with_soft_min.json @@ -4,7 +4,7 @@ "description": "Test LinkNode with soft_min", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/local-parameter1.json b/pywr-schema/tests/local-parameter1.json index a0460d96..618820bc 100644 --- a/pywr-schema/tests/local-parameter1.json +++ b/pywr-schema/tests/local-parameter1.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-31", "timestep": { diff --git a/pywr-schema/tests/local-parameter2.json b/pywr-schema/tests/local-parameter2.json index 10a9c7ce..3aab1358 100644 --- a/pywr-schema/tests/local-parameter2.json +++ b/pywr-schema/tests/local-parameter2.json @@ -4,7 +4,7 @@ "description": "Examples of using local parameters to define cost on a Storage node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-31", "timestep": { diff --git a/pywr-schema/tests/loss_link1.json b/pywr-schema/tests/loss_link1.json index 7bb89880..8eb02f21 100644 --- a/pywr-schema/tests/loss_link1.json +++ b/pywr-schema/tests/loss_link1.json @@ -4,7 +4,7 @@ "description": "Test LossLink nodes", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/loss_link2.json b/pywr-schema/tests/loss_link2.json index 3fcd3ef4..61fafbad 100644 --- a/pywr-schema/tests/loss_link2.json +++ b/pywr-schema/tests/loss_link2.json @@ -4,7 +4,7 @@ "description": "Test LossLink with zero loss", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/memory1.json b/pywr-schema/tests/memory1.json index 306bf4e3..2526f564 100644 --- a/pywr-schema/tests/memory1.json +++ b/pywr-schema/tests/memory1.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/multi1/model.json b/pywr-schema/tests/multi1/model.json index 6fb6e8a8..210903bc 100644 --- a/pywr-schema/tests/multi1/model.json +++ b/pywr-schema/tests/multi1/model.json @@ -4,7 +4,7 @@ "description": "A simple multi-model that passes data from sub-model1 to sub-model2.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/multi2/model.json b/pywr-schema/tests/multi2/model.json index 7ca49ca5..5e4d1754 100644 --- a/pywr-schema/tests/multi2/model.json +++ b/pywr-schema/tests/multi2/model.json @@ -4,7 +4,7 @@ "description": "A simple multi-model that passes data from sub-model1 to sub-model2, and back again", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/muskingum1.json b/pywr-schema/tests/muskingum1.json index 7b8373f5..c6553825 100644 --- a/pywr-schema/tests/muskingum1.json +++ b/pywr-schema/tests/muskingum1.json @@ -4,7 +4,7 @@ "description": "An example of using Muskingum routing in a River node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-02-28", "timestep": { diff --git a/pywr-schema/tests/mutual-exclusivity1.json b/pywr-schema/tests/mutual-exclusivity1.json index 94b1a336..1bf0434f 100644 --- a/pywr-schema/tests/mutual-exclusivity1.json +++ b/pywr-schema/tests/mutual-exclusivity1.json @@ -4,7 +4,7 @@ "description": "Test mutual exclusivities work with simple link node types", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/mutual-exclusivity2.json b/pywr-schema/tests/mutual-exclusivity2.json index f6f7d3ac..7452002c 100644 --- a/pywr-schema/tests/mutual-exclusivity2.json +++ b/pywr-schema/tests/mutual-exclusivity2.json @@ -4,7 +4,7 @@ "description": "Test mutual exclusivities work with compound node types", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/mutual-exclusivity3.json b/pywr-schema/tests/mutual-exclusivity3.json index 41526757..1ef42f54 100644 --- a/pywr-schema/tests/mutual-exclusivity3.json +++ b/pywr-schema/tests/mutual-exclusivity3.json @@ -4,7 +4,7 @@ "description": "Test mutual exclusivities work with piecewise link node types", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/mutual-exclusivity4.json b/pywr-schema/tests/mutual-exclusivity4.json index c93f2be0..da64ef52 100644 --- a/pywr-schema/tests/mutual-exclusivity4.json +++ b/pywr-schema/tests/mutual-exclusivity4.json @@ -4,7 +4,7 @@ "description": "Test mutual exclusivities work with simple link minimum flows", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/piecewise_link1.json b/pywr-schema/tests/piecewise_link1.json index 23b918ac..013c25e5 100644 --- a/pywr-schema/tests/piecewise_link1.json +++ b/pywr-schema/tests/piecewise_link1.json @@ -4,7 +4,7 @@ "description": "A test of PiecewiseLinkNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/piecewise_storage1.json b/pywr-schema/tests/piecewise_storage1.json index 01c8e303..26350f1a 100644 --- a/pywr-schema/tests/piecewise_storage1.json +++ b/pywr-schema/tests/piecewise_storage1.json @@ -4,7 +4,7 @@ "description": "A test of PiecewiseStorageNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-03-31", "timestep": { diff --git a/pywr-schema/tests/piecewise_storage2.json b/pywr-schema/tests/piecewise_storage2.json index 787db489..ac345ae8 100644 --- a/pywr-schema/tests/piecewise_storage2.json +++ b/pywr-schema/tests/piecewise_storage2.json @@ -4,7 +4,7 @@ "description": "A test of PiecewiseStorageNode with a monthly profile control curve.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-08-31", "timestep": { diff --git a/pywr-schema/tests/piecewise_storage3.json b/pywr-schema/tests/piecewise_storage3.json index aabe86db..80cb7b7f 100644 --- a/pywr-schema/tests/piecewise_storage3.json +++ b/pywr-schema/tests/piecewise_storage3.json @@ -4,7 +4,7 @@ "description": "A test of PiecewiseStorageNode with a monthly profile control curve.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-08-31", "timestep": { diff --git a/pywr-schema/tests/piecewise_storage4.json b/pywr-schema/tests/piecewise_storage4.json index 182b8869..7f0b14f6 100644 --- a/pywr-schema/tests/piecewise_storage4.json +++ b/pywr-schema/tests/piecewise_storage4.json @@ -4,7 +4,7 @@ "description": "A test of PiecewiseStorageNode with a monthly profile control curve.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-08-31", "timestep": { diff --git a/pywr-schema/tests/python-agg-func1.json b/pywr-schema/tests/python-agg-func1.json index a923f894..ac7738eb 100644 --- a/pywr-schema/tests/python-agg-func1.json +++ b/pywr-schema/tests/python-agg-func1.json @@ -4,7 +4,7 @@ "description": "A simple example showing how to use a Python function to aggregate data.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/python-parameter1.json b/pywr-schema/tests/python-parameter1.json index 14029369..ad522c50 100644 --- a/pywr-schema/tests/python-parameter1.json +++ b/pywr-schema/tests/python-parameter1.json @@ -4,7 +4,7 @@ "description": "An example of using custom Python parameters.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-15", "timestep": { diff --git a/pywr-schema/tests/python-parameter2.json b/pywr-schema/tests/python-parameter2.json index b2c95ce0..5a9c8614 100644 --- a/pywr-schema/tests/python-parameter2.json +++ b/pywr-schema/tests/python-parameter2.json @@ -4,7 +4,7 @@ "description": "An example of using custom Python function that returns multiple values.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-15", "timestep": { diff --git a/pywr-schema/tests/reservoir-failure-levels1.json b/pywr-schema/tests/reservoir-failure-levels1.json index d2740644..0e160730 100644 --- a/pywr-schema/tests/reservoir-failure-levels1.json +++ b/pywr-schema/tests/reservoir-failure-levels1.json @@ -4,7 +4,7 @@ "description": "A test of the ReservoirNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2017-12-31", "timestep": { diff --git a/pywr-schema/tests/reservoir_with_river1.json b/pywr-schema/tests/reservoir_with_river1.json index f01c6c71..1ff8a304 100644 --- a/pywr-schema/tests/reservoir_with_river1.json +++ b/pywr-schema/tests/reservoir_with_river1.json @@ -4,7 +4,7 @@ "description": "A test of the ReservoirNode with a spill to a termination (output) node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-06", "timestep": { diff --git a/pywr-schema/tests/reservoir_with_river2.json b/pywr-schema/tests/reservoir_with_river2.json index e7dcc94f..6ff54276 100644 --- a/pywr-schema/tests/reservoir_with_river2.json +++ b/pywr-schema/tests/reservoir_with_river2.json @@ -4,7 +4,7 @@ "description": "A test of the ReservoirNode with a spill to a termination (output) node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-06", "timestep": { diff --git a/pywr-schema/tests/reservoir_with_spill1.json b/pywr-schema/tests/reservoir_with_spill1.json index d497c838..237be7d1 100644 --- a/pywr-schema/tests/reservoir_with_spill1.json +++ b/pywr-schema/tests/reservoir_with_spill1.json @@ -4,7 +4,7 @@ "description": "A test of the ReservoirNode with a spill to an internal output node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-06", "timestep": { diff --git a/pywr-schema/tests/river_gauge1.json b/pywr-schema/tests/river_gauge1.json index 2cba9062..2cd78ffe 100644 --- a/pywr-schema/tests/river_gauge1.json +++ b/pywr-schema/tests/river_gauge1.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/river_loss1.json b/pywr-schema/tests/river_loss1.json index 7ee690a6..9fc2c123 100644 --- a/pywr-schema/tests/river_loss1.json +++ b/pywr-schema/tests/river_loss1.json @@ -4,7 +4,7 @@ "description": "Test River node", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/river_split_with_gauge1.json b/pywr-schema/tests/river_split_with_gauge1.json index c061afda..b3c7c75d 100644 --- a/pywr-schema/tests/river_split_with_gauge1.json +++ b/pywr-schema/tests/river_split_with_gauge1.json @@ -4,7 +4,7 @@ "description": "A river split with a proportion above an MRF taken off.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/river_split_with_gauge2.json b/pywr-schema/tests/river_split_with_gauge2.json index 7d9da425..8d647901 100644 --- a/pywr-schema/tests/river_split_with_gauge2.json +++ b/pywr-schema/tests/river_split_with_gauge2.json @@ -4,7 +4,7 @@ "description": "A river split with a proportion above an MRF taken off. The split is given a user defined slot name.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/seasonal-vs1.json b/pywr-schema/tests/seasonal-vs1.json index d5f7066e..ad7bdfb6 100644 --- a/pywr-schema/tests/seasonal-vs1.json +++ b/pywr-schema/tests/seasonal-vs1.json @@ -4,7 +4,7 @@ "description": "An example of a seasonal licence that resets to full on 15th Jan and 20th Feb each year.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-02-28", "timestep": { diff --git a/pywr-schema/tests/seasonal-vs2.json b/pywr-schema/tests/seasonal-vs2.json index 274f9b85..dc744df2 100644 --- a/pywr-schema/tests/seasonal-vs2.json +++ b/pywr-schema/tests/seasonal-vs2.json @@ -4,7 +4,7 @@ "description": "An example of a seasonal licence that resets to full on 15th Dec and 20th Jan each year.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2014-12-01", "end": "2015-01-31", "timestep": { diff --git a/pywr-schema/tests/simple1.json b/pywr-schema/tests/simple1.json index e9042ad0..0b9188c0 100644 --- a/pywr-schema/tests/simple1.json +++ b/pywr-schema/tests/simple1.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/storage_max_volumes.json b/pywr-schema/tests/storage_max_volumes.json index 7815692f..421e4bcd 100644 --- a/pywr-schema/tests/storage_max_volumes.json +++ b/pywr-schema/tests/storage_max_volumes.json @@ -4,7 +4,7 @@ "description": "Several examples of storage nodes with different max volumes.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/tbl-formats1.json b/pywr-schema/tests/tbl-formats1.json index e706c67d..112661c3 100644 --- a/pywr-schema/tests/tbl-formats1.json +++ b/pywr-schema/tests/tbl-formats1.json @@ -4,7 +4,7 @@ "description": "Examples of using data tables to define values in a model.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-02", "timestep": { diff --git a/pywr-schema/tests/test_invalid.rs b/pywr-schema/tests/test_invalid.rs index 01db175e..e3780f8c 100644 --- a/pywr-schema/tests/test_invalid.rs +++ b/pywr-schema/tests/test_invalid.rs @@ -49,7 +49,7 @@ fn deserialise_test_model(model_path: &Path) -> ModelSchema { fn build_test_model(schema: &ModelSchema) -> ModelSchemaBuildError { let temp_dir = TempDir::new().unwrap(); let data_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("invalid"); - match schema.build_model(Some(&data_dir), Some(temp_dir.path())) { + match schema.create_model_builder(Some(&data_dir), Some(temp_dir.path())) { Ok(_) => panic!("Expected an error, but model built successfully!"), Err(e) => e, } diff --git a/pywr-schema/tests/test_models.rs b/pywr-schema/tests/test_models.rs index 48bf95a7..c17e9690 100644 --- a/pywr-schema/tests/test_models.rs +++ b/pywr-schema/tests/test_models.rs @@ -87,7 +87,7 @@ model_tests! { test_seasonal_vs2: ("seasonal-vs2.json", vec![("seasonal-vs2-expected.csv", ResultsShape::Long)], vec!["ipm-simd", "ipm-ocl"], vec![]), test_thirty_day_licence: ("30-day-licence.json", vec![], vec!["ipm-simd", "ipm-ocl"], vec![]), test_wtw1: ("wtw1.json", vec![("wtw1-expected.csv", ResultsShape::Long)], vec!["ipm-simd", "ipm-ocl"], vec![]), - test_wtw2: ("wtw2.json", vec![("wtw2-expected.csv", ResultsShape::Long)], vec!["ipm-simd", "ipm-ocl"], vec![]), + test_wtw2: ("wtw2.json", vec![("wtw2-expected.csv", ResultsShape::Long)], vec![], vec![]), test_local_parameter1: ("local-parameter1.json", vec![("local-parameter1-expected.csv", ResultsShape::Long)], vec![], vec![]), test_python_agg_func1: ("python-agg-func1.json", vec![("python-agg-func1-expected.csv", ResultsShape::Long)], vec![], vec![]), test_python_parameter2: ("python-parameter2.json", vec![("python-parameter2-expected.csv", ResultsShape::Long)], vec![], vec![]), @@ -139,7 +139,9 @@ fn run_test_model( ) { let temp_dir = TempDir::new().unwrap(); let data_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); - let model = schema.build_model(Some(&data_dir), Some(temp_dir.path())).unwrap(); + let builder = schema + .create_model_builder(Some(&data_dir), Some(temp_dir.path())) + .unwrap(); // After model run there should be an output file. let expected_outputs: Vec> = result_paths .iter() @@ -155,6 +157,8 @@ fn run_test_model( }) .collect(); + let model = builder.build().unwrap(); + // Test all solvers run_all_solvers(&model, solvers_without_features, solvers_to_skip, &expected_outputs); } diff --git a/pywr-schema/tests/timeseries.json b/pywr-schema/tests/timeseries.json index 4a7a8562..7357a43e 100644 --- a/pywr-schema/tests/timeseries.json +++ b/pywr-schema/tests/timeseries.json @@ -2,7 +2,7 @@ "metadata": { "title": "Simple timeseries" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { diff --git a/pywr-schema/tests/timeseries2-hourly.json b/pywr-schema/tests/timeseries2-hourly.json index 99c55498..ad671e46 100644 --- a/pywr-schema/tests/timeseries2-hourly.json +++ b/pywr-schema/tests/timeseries2-hourly.json @@ -10,7 +10,7 @@ } ] }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-02", "timestep": { diff --git a/pywr-schema/tests/timeseries2.json b/pywr-schema/tests/timeseries2.json index 0e75a018..0490dfff 100644 --- a/pywr-schema/tests/timeseries2.json +++ b/pywr-schema/tests/timeseries2.json @@ -10,7 +10,7 @@ } ] }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-10", "timestep": { diff --git a/pywr-schema/tests/timeseries3.json b/pywr-schema/tests/timeseries3.json index 99409903..0650d04b 100644 --- a/pywr-schema/tests/timeseries3.json +++ b/pywr-schema/tests/timeseries3.json @@ -15,7 +15,7 @@ } ] }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-10", "timestep": { diff --git a/pywr-schema/tests/timeseries4.json b/pywr-schema/tests/timeseries4.json index 3d6a5f14..a622e51b 100644 --- a/pywr-schema/tests/timeseries4.json +++ b/pywr-schema/tests/timeseries4.json @@ -39,7 +39,7 @@ ] ] }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-10", "timestep": { diff --git a/pywr-schema/tests/timeseries5.json b/pywr-schema/tests/timeseries5.json index b7afd4c4..09427004 100644 --- a/pywr-schema/tests/timeseries5.json +++ b/pywr-schema/tests/timeseries5.json @@ -39,7 +39,7 @@ } ] }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-10", "timestep": { diff --git a/pywr-schema/tests/timeseries_pandas.json b/pywr-schema/tests/timeseries_pandas.json index 5d07aa57..9c15e595 100644 --- a/pywr-schema/tests/timeseries_pandas.json +++ b/pywr-schema/tests/timeseries_pandas.json @@ -2,7 +2,7 @@ "metadata": { "title": "Simple timeseries" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { diff --git a/pywr-schema/tests/turbine1.json b/pywr-schema/tests/turbine1.json index 335c4ecf..5ffab806 100644 --- a/pywr-schema/tests/turbine1.json +++ b/pywr-schema/tests/turbine1.json @@ -4,7 +4,7 @@ "description": "A simple example of using the Turbine node.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/v1/breaklink-converted.json b/pywr-schema/tests/v1/breaklink-converted.json index 4c85f475..a76d49c2 100644 --- a/pywr-schema/tests/v1/breaklink-converted.json +++ b/pywr-schema/tests/v1/breaklink-converted.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/v1/inline-parameter-converted.json b/pywr-schema/tests/v1/inline-parameter-converted.json index ca9ffec6..d0564eaf 100644 --- a/pywr-schema/tests/v1/inline-parameter-converted.json +++ b/pywr-schema/tests/v1/inline-parameter-converted.json @@ -150,7 +150,7 @@ } ] }, - "timestepper": { + "time": { "end": "2015-12-31", "start": "2015-01-01", "timestep": { diff --git a/pywr-schema/tests/v1/river_split_with_gauge1-converted.json b/pywr-schema/tests/v1/river_split_with_gauge1-converted.json index 03a94cf0..47228bd5 100644 --- a/pywr-schema/tests/v1/river_split_with_gauge1-converted.json +++ b/pywr-schema/tests/v1/river_split_with_gauge1-converted.json @@ -4,7 +4,7 @@ "description": "Example of an abstraction with an MRF of form y=mx+c", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/v1/scenarios-converted.json b/pywr-schema/tests/v1/scenarios-converted.json index 0ced1ff1..6b2ca7ed 100644 --- a/pywr-schema/tests/v1/scenarios-converted.json +++ b/pywr-schema/tests/v1/scenarios-converted.json @@ -4,7 +4,7 @@ "description": "A very simple example.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-schema/tests/v1/timeseries-converted.json b/pywr-schema/tests/v1/timeseries-converted.json index ab3bef00..cde18e44 100644 --- a/pywr-schema/tests/v1/timeseries-converted.json +++ b/pywr-schema/tests/v1/timeseries-converted.json @@ -2,7 +2,7 @@ "metadata": { "title": "Simple timeseries" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-01-31", "timestep": { diff --git a/pywr-schema/tests/vs-with-piecewise-link.json b/pywr-schema/tests/vs-with-piecewise-link.json index bca65c49..b69ae7aa 100644 --- a/pywr-schema/tests/vs-with-piecewise-link.json +++ b/pywr-schema/tests/vs-with-piecewise-link.json @@ -4,7 +4,7 @@ "description": "Ensure that the correct flow is removed from the virtual storage node when using a PiecewiseLinkNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/vs-with-wtw1.json b/pywr-schema/tests/vs-with-wtw1.json index 819695e6..6e76b56f 100644 --- a/pywr-schema/tests/vs-with-wtw1.json +++ b/pywr-schema/tests/vs-with-wtw1.json @@ -4,7 +4,7 @@ "description": "Ensure that the correct flow is removed from the virtual storage node when using a PiecewiseLinkNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/vs-with-wtw2.json b/pywr-schema/tests/vs-with-wtw2.json index 49d20e37..558d62b5 100644 --- a/pywr-schema/tests/vs-with-wtw2.json +++ b/pywr-schema/tests/vs-with-wtw2.json @@ -4,7 +4,7 @@ "description": "Ensure that the correct flow is removed from the virtual storage node when using a PiecewiseLinkNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/vs-with-wtw3.json b/pywr-schema/tests/vs-with-wtw3.json index 55df45a5..da9991cf 100644 --- a/pywr-schema/tests/vs-with-wtw3.json +++ b/pywr-schema/tests/vs-with-wtw3.json @@ -4,7 +4,7 @@ "description": "Ensure that the correct flow is removed from the virtual storage node when using a PiecewiseLinkNode.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/wtw1.json b/pywr-schema/tests/wtw1.json index c225e277..c5e2bc3a 100644 --- a/pywr-schema/tests/wtw1.json +++ b/pywr-schema/tests/wtw1.json @@ -4,7 +4,7 @@ "description": "Test WTW work", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { diff --git a/pywr-schema/tests/wtw2.json b/pywr-schema/tests/wtw2.json index 89cdba52..8f2ebb37 100644 --- a/pywr-schema/tests/wtw2.json +++ b/pywr-schema/tests/wtw2.json @@ -4,7 +4,7 @@ "description": "Test WTW work", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-01-03", "timestep": { From 923e0e94a5db40f180f2e15b633987013888ce7c Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Thu, 18 Jun 2026 13:32:48 +0100 Subject: [PATCH 03/12] docs: Update dev guide to include parameter builder. --- pywr-book/src/developers-guide/adding-a-parameter.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pywr-book/src/developers-guide/adding-a-parameter.md b/pywr-book/src/developers-guide/adding-a-parameter.md index c8bcc3c3..d02d7809 100644 --- a/pywr-book/src/developers-guide/adding-a-parameter.md +++ b/pywr-book/src/developers-guide/adding-a-parameter.md @@ -41,11 +41,15 @@ The `ParameterMeta` struct is used to store the metadata for all parameters and {{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:parameter}} ``` -To allow the parameter to be used in the model it is helpful to add a `new` function that creates a new instance of the -parameter. This will be used by the schema to create the parameter when it is loaded from a model file. +To allow the parameter to be used in the model a "builder" is required. This struct must implement the +`ParameterBuilder` trait. This builder be used by the schema to create the parameter when it is loaded from a model +file. Builders will typically have the same fields as the parameter itself, but will use "unresolved" types +(e.g. `UnresolvedMetricF64` or `UnresolvedNode`). This allows the builder to be created without first resolving the +dependencies of the parameter. The `build` function is then used to resolve the dependencies and create the parameter +used in the model. ```rust,ignore -{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:impl-new}} +{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:impl-builder}} ``` Finally, the minimum implementation of the `Parameter` and one of the three types of parameter compute traits should be From c0790546e9937f9077b5abea2de735fd49f3483d Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Thu, 18 Jun 2026 13:46:36 +0100 Subject: [PATCH 04/12] fix(python): Update Python test models to new schema. --- pywr-python/tests/models/aggregated-node1/model.json | 2 +- pywr-python/tests/models/multi1/model.json | 2 +- pywr-python/tests/models/piecewise-link1/model.json | 2 +- pywr-python/tests/models/simple-custom-parameter/model.json | 2 +- pywr-python/tests/models/simple-storage-timeseries/model.json | 2 +- pywr-python/tests/models/simple-timeseries/model.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pywr-python/tests/models/aggregated-node1/model.json b/pywr-python/tests/models/aggregated-node1/model.json index 886db0a7..4486500e 100644 --- a/pywr-python/tests/models/aggregated-node1/model.json +++ b/pywr-python/tests/models/aggregated-node1/model.json @@ -2,7 +2,7 @@ "metadata": { "title": "Test of aggregated node." }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { diff --git a/pywr-python/tests/models/multi1/model.json b/pywr-python/tests/models/multi1/model.json index 6fb6e8a8..210903bc 100644 --- a/pywr-python/tests/models/multi1/model.json +++ b/pywr-python/tests/models/multi1/model.json @@ -4,7 +4,7 @@ "description": "A simple multi-model that passes data from sub-model1 to sub-model2.", "minimum_version": "0.1" }, - "timestepper": { + "time": { "start": "2015-01-01", "end": "2015-12-31", "timestep": { diff --git a/pywr-python/tests/models/piecewise-link1/model.json b/pywr-python/tests/models/piecewise-link1/model.json index 5b1c81f4..dcc09fa4 100644 --- a/pywr-python/tests/models/piecewise-link1/model.json +++ b/pywr-python/tests/models/piecewise-link1/model.json @@ -2,7 +2,7 @@ "metadata": { "title": "Test of piecewise link." }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { diff --git a/pywr-python/tests/models/simple-custom-parameter/model.json b/pywr-python/tests/models/simple-custom-parameter/model.json index 11c2f39c..0f730b22 100644 --- a/pywr-python/tests/models/simple-custom-parameter/model.json +++ b/pywr-python/tests/models/simple-custom-parameter/model.json @@ -2,7 +2,7 @@ "metadata": { "title": "Simple timeseries" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { diff --git a/pywr-python/tests/models/simple-storage-timeseries/model.json b/pywr-python/tests/models/simple-storage-timeseries/model.json index 96ee4197..cdd763fd 100644 --- a/pywr-python/tests/models/simple-storage-timeseries/model.json +++ b/pywr-python/tests/models/simple-storage-timeseries/model.json @@ -2,7 +2,7 @@ "metadata": { "title": "Simple storage timeseries" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { diff --git a/pywr-python/tests/models/simple-timeseries/model.json b/pywr-python/tests/models/simple-timeseries/model.json index 269aaacc..95757559 100644 --- a/pywr-python/tests/models/simple-timeseries/model.json +++ b/pywr-python/tests/models/simple-timeseries/model.json @@ -2,7 +2,7 @@ "metadata": { "title": "Simple timeseries" }, - "timestepper": { + "time": { "start": "2021-01-01", "end": "2021-12-31", "timestep": { From d90e709f94f6b1a804c09c2b15b434724f3b8629 Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Thu, 18 Jun 2026 21:19:53 +0100 Subject: [PATCH 05/12] fix: Fix some schema build errors with core only feature. --- pywr-schema/src/model.rs | 4 +++- pywr-schema/src/outputs/csv.rs | 2 +- pywr-schema/src/outputs/hdf.rs | 6 +++--- pywr-schema/src/outputs/memory.rs | 2 +- pywr-schema/src/outputs/mod.rs | 6 +++--- pywr-schema/src/parameters/python.rs | 6 +++--- pywr-schema/src/parameters/tables.rs | 6 +++--- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index 47de50f9..ba40cb90 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -17,10 +17,12 @@ use chrono::{NaiveDate, NaiveDateTime}; use pyo3::Python; #[cfg(feature = "pyo3")] use pyo3::{Bound, PyErr, PyResult, exceptions::PyRuntimeError, pyclass, pymethods, types::PyType}; +#[cfg(all(feature = "core", feature = "pyo3"))] +use pywr_core::models::Model; #[cfg(feature = "core")] use pywr_core::{ models::{ - Model, ModelBuilder, ModelDomainBuilder, MultiNetworkEntryBuilder, MultiNetworkModelBuilder, + ModelBuilder, ModelDomainBuilder, MultiNetworkEntryBuilder, MultiNetworkModelBuilder, MultiNetworkModelBuilderError, MultiNetworkTransferBuilder, }, timestep::TimestepDuration, diff --git a/pywr-schema/src/outputs/csv.rs b/pywr-schema/src/outputs/csv.rs index 665045ce..bb491c02 100644 --- a/pywr-schema/src/outputs/csv.rs +++ b/pywr-schema/src/outputs/csv.rs @@ -54,7 +54,7 @@ pub struct CsvOutput { #[cfg(feature = "core")] impl CsvOutput { - pub fn add_to_model( + pub fn add_to_network( &self, network: &mut pywr_core::network::NetworkBuilder, output_path: Option<&Path>, diff --git a/pywr-schema/src/outputs/hdf.rs b/pywr-schema/src/outputs/hdf.rs index 4b95f397..209d86f0 100644 --- a/pywr-schema/src/outputs/hdf.rs +++ b/pywr-schema/src/outputs/hdf.rs @@ -18,7 +18,7 @@ pub struct Hdf5Output { #[cfg(all(feature = "core", feature = "hdf5"))] impl Hdf5Output { - pub fn add_to_model( + pub fn add_to_network( &self, network: &mut pywr_core::network::NetworkBuilder, output_path: Option<&Path>, @@ -38,9 +38,9 @@ impl Hdf5Output { #[cfg(all(feature = "core", not(feature = "hdf5")))] impl Hdf5Output { - pub fn add_to_model( + pub fn add_to_network( &self, - _network: &mut pywr_core::network::Network, + _network: &mut pywr_core::network::NetworkBuilder, _output_path: Option<&Path>, ) -> Result<(), SchemaError> { Err(SchemaError::FeatureNotEnabled("hdf5".to_string())) diff --git a/pywr-schema/src/outputs/memory.rs b/pywr-schema/src/outputs/memory.rs index 664d999c..afe76a55 100644 --- a/pywr-schema/src/outputs/memory.rs +++ b/pywr-schema/src/outputs/memory.rs @@ -55,7 +55,7 @@ pub struct MemoryOutput { #[cfg(feature = "core")] impl MemoryOutput { - pub fn add_to_model( + pub fn add_to_network( &self, network: &mut pywr_core::network::NetworkBuilder, data_path: Option<&Path>, diff --git a/pywr-schema/src/outputs/mod.rs b/pywr-schema/src/outputs/mod.rs index ac420b16..f61509b4 100644 --- a/pywr-schema/src/outputs/mod.rs +++ b/pywr-schema/src/outputs/mod.rs @@ -34,9 +34,9 @@ impl Output { output_path: Option<&Path>, ) -> Result<(), SchemaError> { match self { - Self::CSV(o) => o.add_to_model(network, output_path), - Self::HDF5(o) => o.add_to_model(network, output_path), - Self::Memory(o) => o.add_to_model(network, data_path), + Self::CSV(o) => o.add_to_network(network, output_path), + Self::HDF5(o) => o.add_to_network(network, output_path), + Self::Memory(o) => o.add_to_network(network, data_path), } } diff --git a/pywr-schema/src/parameters/python.rs b/pywr-schema/src/parameters/python.rs index c9fe3bad..72e3f9a6 100644 --- a/pywr-schema/src/parameters/python.rs +++ b/pywr-schema/src/parameters/python.rs @@ -192,12 +192,12 @@ impl PythonParameter { #[cfg(all(feature = "core", not(feature = "pyo3")))] impl PythonParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - _network: &mut pywr_core::network::Network, + _network: &mut pywr_core::network::NetworkBuilder, _args: &LoadArgs, _parent: Option<&str>, - ) -> Result { + ) -> Result<(), SchemaError> { Err(SchemaError::FeatureNotEnabled("pyo3".to_string())) } } diff --git a/pywr-schema/src/parameters/tables.rs b/pywr-schema/src/parameters/tables.rs index 0aa5ce9b..ceff4ee0 100644 --- a/pywr-schema/src/parameters/tables.rs +++ b/pywr-schema/src/parameters/tables.rs @@ -95,12 +95,12 @@ impl TablesArrayParameter { #[cfg(all(feature = "core", not(feature = "hdf5")))] impl TablesArrayParameter { - pub fn add_to_model( + pub fn add_to_network( &self, - _network: &mut pywr_core::network::Network, + _network: &mut pywr_core::network::NetworkBuilder, _args: &LoadArgs, _parent: Option<&str>, - ) -> Result, SchemaError> { + ) -> Result<(), SchemaError> { Err(SchemaError::FeatureNotEnabled("hdf5".to_string())) } } From e5cbe591e5b085b9761a68a8eff367b1b2bed47c Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Fri, 3 Jul 2026 20:57:59 +0100 Subject: [PATCH 06/12] Improve parameter error handling. Distinguish between missing parameters and circular reference errors. Derive Debug for most structs to allow use of unwrap and expect on build methods. --- .../listings/adding-a-parameter/src/main.rs | 2 + pywr-core/src/aggregated_node.rs | 13 +- pywr-core/src/aggregated_storage_node.rs | 1 + pywr-core/src/metric.rs | 1 + pywr-core/src/network.rs | 130 ++++++++++++++++- pywr-core/src/node.rs | 2 + pywr-core/src/parameters/aggregated.rs | 5 +- pywr-core/src/parameters/aggregated_index.rs | 5 +- pywr-core/src/parameters/array.rs | 10 +- pywr-core/src/parameters/asymmetric.rs | 2 + pywr-core/src/parameters/constant.rs | 2 + pywr-core/src/parameters/constant_scenario.rs | 2 + .../parameters/control_curves/apportion.rs | 2 + .../src/parameters/control_curves/index.rs | 2 + .../parameters/control_curves/interpolated.rs | 2 + .../parameters/control_curves/piecewise.rs | 2 + .../src/parameters/control_curves/simple.rs | 2 + .../control_curves/volume_between.rs | 5 +- pywr-core/src/parameters/deficit.rs | 2 + pywr-core/src/parameters/delay.rs | 7 +- pywr-core/src/parameters/difference.rs | 5 +- pywr-core/src/parameters/discount_factor.rs | 2 + pywr-core/src/parameters/division.rs | 2 + pywr-core/src/parameters/hydropower.rs | 2 + pywr-core/src/parameters/indexed_array.rs | 2 + pywr-core/src/parameters/interpolated.rs | 2 + pywr-core/src/parameters/max.rs | 2 + pywr-core/src/parameters/min.rs | 2 + pywr-core/src/parameters/mod.rs | 131 ++++++++++++------ pywr-core/src/parameters/multi_threshold.rs | 2 + pywr-core/src/parameters/muskingum.rs | 3 + pywr-core/src/parameters/negative.rs | 2 + pywr-core/src/parameters/negativemax.rs | 2 + pywr-core/src/parameters/negativemin.rs | 2 + pywr-core/src/parameters/offset.rs | 2 + pywr-core/src/parameters/polynomial.rs | 2 + pywr-core/src/parameters/profiles/daily.rs | 2 + pywr-core/src/parameters/profiles/diurnal.rs | 2 + pywr-core/src/parameters/profiles/monthly.rs | 4 +- pywr-core/src/parameters/profiles/rbf.rs | 3 + .../parameters/profiles/uniform_drawdown.rs | 2 + pywr-core/src/parameters/profiles/weekly.rs | 4 + pywr-core/src/parameters/py.rs | 6 + pywr-core/src/parameters/rolling.rs | 11 +- pywr-core/src/parameters/threshold.rs | 3 + pywr-core/src/parameters/vector.rs | 2 + pywr-core/src/recorders/memory.rs | 6 +- pywr-core/src/recorders/metric_set.rs | 3 +- pywr-core/src/recorders/mod.rs | 33 ++++- pywr-core/src/virtual_storage.rs | 8 +- 50 files changed, 380 insertions(+), 73 deletions(-) diff --git a/pywr-book/listings/adding-a-parameter/src/main.rs b/pywr-book/listings/adding-a-parameter/src/main.rs index f2a4f4aa..d35884e1 100644 --- a/pywr-book/listings/adding-a-parameter/src/main.rs +++ b/pywr-book/listings/adding-a-parameter/src/main.rs @@ -11,6 +11,7 @@ use pywr_core::state::State; use pywr_core::timestep::Timestep; // ANCHOR: parameter +#[derive(Debug)] pub struct MaxParameter { meta: ParameterMeta, metric: MetricF64, @@ -48,6 +49,7 @@ impl GeneralParameter for MaxParameter { } // ANCHOR: impl-builder +#[derive(Debug)] pub struct MaxParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/aggregated_node.rs b/pywr-core/src/aggregated_node.rs index 54f60004..5be52c91 100644 --- a/pywr-core/src/aggregated_node.rs +++ b/pywr-core/src/aggregated_node.rs @@ -1,9 +1,11 @@ #![warn(clippy::pedantic)] + use crate::NodeIndex; use crate::metric::{ConstantMetricF64Error, MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; use crate::network::{AggregatedNodeIndex, Network, ResolutionMaps}; use crate::node::{FlowConstraints, NodeMeta, UnresolvedNode}; use crate::state::{ConstParameterValues, State}; +use std::fmt::Debug; use thiserror::Error; /// Factors relating node flows in an aggregated node. @@ -38,7 +40,7 @@ impl Factors { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct ProportionalFactorsBuilder { factors: Vec, } @@ -66,7 +68,7 @@ impl RelationshipBuilder for ProportionalFactorsBuilder { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct RatioFactorsBuilder { factors: Vec, } @@ -94,7 +96,7 @@ impl RelationshipBuilder for RatioFactorsBuilder { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct CoefficientFactorsBuilder { factors: Vec, rhs: Option, @@ -158,7 +160,7 @@ impl Exclusivity { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ExclusivityBuilder { min_active: u64, max_active: u64, @@ -212,7 +214,7 @@ pub enum RelationshipBuildError { source: MetricF64ResolutionError, }, } -pub trait RelationshipBuilder { +pub trait RelationshipBuilder: Debug { /// Try to construct a [`Relationship`]. /// /// # Errors @@ -547,6 +549,7 @@ pub enum AggregatedNodeBuilderError { RelationshipBuildError(#[from] RelationshipBuildError), } +#[derive(Debug)] pub struct AggregatedNodeBuilder { name: UnresolvedNode, min_flow: Option, diff --git a/pywr-core/src/aggregated_storage_node.rs b/pywr-core/src/aggregated_storage_node.rs index 1a7a8d77..0a8bbcca 100644 --- a/pywr-core/src/aggregated_storage_node.rs +++ b/pywr-core/src/aggregated_storage_node.rs @@ -52,6 +52,7 @@ pub enum AggregatedStorageNodeBuilderError { NodeIndexNotFound { node: UnresolvedNode }, } +#[derive(Debug)] pub struct AggregatedStorageNodeBuilder { name: UnresolvedNode, nodes: Vec, diff --git a/pywr-core/src/metric.rs b/pywr-core/src/metric.rs index 604cff72..46f62c84 100644 --- a/pywr-core/src/metric.rs +++ b/pywr-core/src/metric.rs @@ -946,6 +946,7 @@ pub enum MetricU64ResolutionError { InterNetworkTransferNotFound { transfer: String }, } +#[derive(Debug)] pub enum UnresolvedMetricU64 { ParameterValue { name: ParameterName, diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 6fcef754..5fb3b33b 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -542,7 +542,7 @@ impl NetworkResult { /// to represent a discrete system. A network can be simulated using a model and a solver. The /// network is translated into a linear program using the [`Solver`] trait. /// -#[derive(Default)] +#[derive(Debug, Default)] pub struct Network { nodes: Vec, edges: Vec, @@ -1711,6 +1711,7 @@ impl Display for MetricSetIndex { /// A helper struct for building a network. This struct contains look-ups and references /// for resolving names and other unresolved references during the build process. +#[derive(Debug)] pub struct ResolutionMaps { pub nodes: HashMap, /// The edges incoming to each node. @@ -1820,8 +1821,6 @@ pub enum NetworkBuildError { #[source] source: Box, }, - #[error("Could not load all parameters due to circulate reference(s).")] - CircularParameterReference, #[error("Parameter collection build error: {0}")] ParameterCollectionBuildError(#[from] Box), } @@ -1829,7 +1828,7 @@ pub enum NetworkBuildError { /// A builder for [`Network`]. /// /// This is the only way to construct a [`Network`] instance. -#[derive(Default)] +#[derive(Default, Debug)] pub struct NetworkBuilder { nodes: Vec, virtual_storage_nodes: Vec, @@ -2227,7 +2226,7 @@ impl NetworkBuilder { #[cfg(test)] mod tests { use super::*; - use crate::metric::UnresolvedMetricF64; + use crate::metric::{MetricF64ResolutionError, UnresolvedMetricF64}; use crate::models::ModelBuilder; use crate::parameters::{ActivationFunction, ControlCurveInterpolatedParameterBuilder}; use crate::recorders::AssertionF64RecorderBuilder; @@ -2342,6 +2341,127 @@ mod tests { builder.build(&domain, &HashMap::new()).unwrap(); } + #[test] + /// Test the error response when a parameter is missing for a node's attribute. + fn test_missing_parameter_for_node_attr() { + let mut builder = NetworkBuilder::default(); + + let mut input_node_builder = NodeBuilder::input("input"); + // Add the reference to the constant parameter we have not yet added. + input_node_builder.max_flow(UnresolvedMetricF64::new_parameter_before("this-is-missing")); + builder.node(input_node_builder); + + let output_node_builder = NodeBuilder::output("output"); + builder.node(output_node_builder); + + builder.connect("input", "output"); + + let input_max_flow_builder = parameters::ConstantParameterBuilder::new("my-constant".into(), 10.0); + builder.parameters().f64(Box::new(input_max_flow_builder)); + + let domain = default_domain(); + + let build_err = builder + .build(&domain, &HashMap::new()) + .expect_err("Builder should error."); + + if let NetworkBuildError::NodeBuilderError { + name, source: node_err, .. + } = &build_err + && let NodeBuilderError::ResolveMetricF64Error { + attr, + source: metric_err, + } = node_err.deref() + && let MetricF64ResolutionError::ParameterNotFound { parameter } = metric_err + { + assert_eq!(name.to_string(), "input"); + assert_eq!(attr, "max_flow"); + assert_eq!(parameter.name(), "this-is-missing"); + } else { + panic!("Incorrect error returned, expect ParameterNotFound: {build_err:?}"); + } + } + + #[test] + /// Test a parameter with a reference to a missing parameter. + fn test_missing_parameter() { + let mut builder = NetworkBuilder::default(); + + let input_node_builder = NodeBuilder::input("input"); + builder.node(input_node_builder); + + let output_node_builder = NodeBuilder::output("output"); + builder.node(output_node_builder); + + builder.connect("input", "output"); + + let broken_parameter = parameters::MaxParameterBuilder::new( + "my-max".into(), + UnresolvedMetricF64::new_parameter_before("this-is-missing"), + 0.0, + ); + builder.parameters().f64(Box::new(broken_parameter)); + + let domain = default_domain(); + + let build_err = builder + .build(&domain, &HashMap::new()) + .expect_err("Builder should error."); + + if let NetworkBuildError::ParameterCollectionBuildError(coll_err) = &build_err + && let ParameterCollectionBuilderError::ParameterNotFound { name } = coll_err.deref() + { + assert_eq!(name.to_string(), "this-is-missing"); + } else { + panic!("Incorrect error returned, expected ParameterNotFound: {build_err:?}"); + } + } + + #[test] + /// Test the error return when there is a circular reference. + fn test_circular_parameter() { + let mut builder = NetworkBuilder::default(); + + let input_node_builder = NodeBuilder::input("input"); + builder.node(input_node_builder); + + let output_node_builder = NodeBuilder::output("output"); + builder.node(output_node_builder); + + builder.connect("input", "output"); + + let max_param = parameters::MaxParameterBuilder::new( + "my-max".into(), + UnresolvedMetricF64::new_parameter_before("my-other-max"), + 0.0, + ); + builder.parameters().f64(Box::new(max_param)); + + let max_param = parameters::MaxParameterBuilder::new( + "my-other-max".into(), + UnresolvedMetricF64::new_parameter_before("my-max"), + 0.0, + ); + builder.parameters().f64(Box::new(max_param)); + + let domain = default_domain(); + + let build_err = builder + .build(&domain, &HashMap::new()) + .expect_err("Builder should error."); + + if let NetworkBuildError::ParameterCollectionBuildError(coll_err) = &build_err + && let ParameterCollectionBuilderError::CircularParameterReference { names } = coll_err.deref() + { + // This is fine! + assert_eq!(names.len(), 2); + assert_eq!(names[0].name(), "my-max"); + assert_eq!(names[1].name(), "my-other-max"); + } else { + panic!("Incorrect error returned, expected CircularParameterReference: {build_err:?}"); + } + } + #[test] fn test_step() { const NUM_SCENARIOS: usize = 2; diff --git a/pywr-core/src/node.rs b/pywr-core/src/node.rs index e0c51f7b..aea3be3d 100644 --- a/pywr-core/src/node.rs +++ b/pywr-core/src/node.rs @@ -68,6 +68,7 @@ pub enum NodeBuilderError { UnexpectedIncomingEdges { node_type: NodeType, num: usize }, } +#[derive(Debug)] pub struct NodeBuilder { name: UnresolvedNode, node_type: NodeType, @@ -1100,6 +1101,7 @@ impl StorageInitialVolume { } } +#[derive(Debug)] pub enum UnresolvedStorageInitialVolume { Absolute(f64), Proportional(f64), diff --git a/pywr-core/src/parameters/aggregated.rs b/pywr-core/src/parameters/aggregated.rs index eba5fb70..2ad54ddf 100644 --- a/pywr-core/src/parameters/aggregated.rs +++ b/pywr-core/src/parameters/aggregated.rs @@ -11,7 +11,9 @@ use crate::resolve_metric_f64_vec; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, SimpleParameterValues, State}; use crate::timestep::Timestep; +use std::fmt::Debug; +#[derive(Debug)] pub struct AggregatedParameter { meta: ParameterMeta, metrics: Vec, @@ -20,7 +22,7 @@ pub struct AggregatedParameter { impl Parameter for AggregatedParameter where - M: Send + Sync, + M: Send + Sync + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -134,6 +136,7 @@ impl ConstParameter for AggregatedParameter { } } +#[derive(Debug)] pub struct AggregatedParameterBuilder { meta: ParameterMeta, agg_func: AggFuncF64, diff --git a/pywr-core/src/parameters/aggregated_index.rs b/pywr-core/src/parameters/aggregated_index.rs index 18290c02..c7e1b058 100644 --- a/pywr-core/src/parameters/aggregated_index.rs +++ b/pywr-core/src/parameters/aggregated_index.rs @@ -13,7 +13,9 @@ use crate::resolve_metric_u64_vec; use crate::scenario::ScenarioIndex; use crate::state::{ConstParameterValues, SimpleParameterValues, State}; use crate::timestep::Timestep; +use std::fmt::Debug; +#[derive(Debug)] pub struct AggregatedIndexParameter { meta: ParameterMeta, metrics: Vec, @@ -22,7 +24,7 @@ pub struct AggregatedIndexParameter { impl Parameter for AggregatedIndexParameter where - M: Send + Sync, + M: Send + Sync + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -136,6 +138,7 @@ impl ConstParameter for AggregatedIndexParameter { } } +#[derive(Debug)] pub struct AggregatedIndexParameterBuilder { meta: ParameterMeta, metrics: Vec, diff --git a/pywr-core/src/parameters/array.rs b/pywr-core/src/parameters/array.rs index 511042a0..d630b500 100644 --- a/pywr-core/src/parameters/array.rs +++ b/pywr-core/src/parameters/array.rs @@ -8,7 +8,9 @@ use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::{Timestep, TimestepIndex}; use ndarray::{Array1, Array2, Axis, s}; +use std::fmt::Debug; +#[derive(Debug)] pub struct Array1Parameter { meta: ParameterMeta, array: Array1, @@ -31,7 +33,7 @@ impl Array1Parameter { } impl Parameter for Array1Parameter where - T: Send + Sync + Clone, + T: Send + Sync + Clone + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -93,7 +95,7 @@ impl SimpleParameter for Array1Parameter { self } } - +#[derive(Debug)] pub struct Array1ParameterBuilder { meta: ParameterMeta, array: Array1, @@ -149,6 +151,7 @@ impl ParameterBuilder for Array1ParameterBuilder { } } +#[derive(Debug)] pub struct Array2Parameter { meta: ParameterMeta, array: Array2, @@ -173,7 +176,7 @@ impl Array2Parameter { impl Parameter for Array2Parameter where - T: Send + Sync + Clone, + T: Send + Sync + Clone + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -266,6 +269,7 @@ impl SimpleParameter for Array2Parameter { } } +#[derive(Debug)] pub struct Array2ParameterBuilder { meta: ParameterMeta, array: Array2, diff --git a/pywr-core/src/parameters/asymmetric.rs b/pywr-core/src/parameters/asymmetric.rs index 1a6c41ae..204260b2 100644 --- a/pywr-core/src/parameters/asymmetric.rs +++ b/pywr-core/src/parameters/asymmetric.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct AsymmetricSwitchIndexParameter { meta: ParameterMeta, on_parameter: MetricU64, @@ -68,6 +69,7 @@ impl GeneralParameter for AsymmetricSwitchIndexParameter { } } +#[derive(Debug)] pub struct AsymmetricSwitchIndexParameterBuilder { meta: ParameterMeta, on_parameter: UnresolvedMetricU64, diff --git a/pywr-core/src/parameters/constant.rs b/pywr-core/src/parameters/constant.rs index 3c06b91f..2cb9ca42 100644 --- a/pywr-core/src/parameters/constant.rs +++ b/pywr-core/src/parameters/constant.rs @@ -9,6 +9,7 @@ use crate::scenario::ScenarioIndex; use crate::state::ConstParameterValues; use crate::timestep::Timestep; +#[derive(Debug)] pub struct ConstantParameter { meta: ParameterMeta, value: f64, @@ -116,6 +117,7 @@ impl VariableParameter for ConstantParameter { } } +#[derive(Debug)] pub struct ConstantParameterBuilder { meta: ParameterMeta, value: f64, diff --git a/pywr-core/src/parameters/constant_scenario.rs b/pywr-core/src/parameters/constant_scenario.rs index 443589fd..56d541ed 100644 --- a/pywr-core/src/parameters/constant_scenario.rs +++ b/pywr-core/src/parameters/constant_scenario.rs @@ -7,6 +7,7 @@ use crate::parameters::{ use crate::scenario::ScenarioIndex; use crate::state::ConstParameterValues; +#[derive(Debug)] pub struct ConstantScenarioParameter { meta: ParameterMeta, values: Vec, @@ -38,6 +39,7 @@ impl ConstParameter for ConstantScenarioParameter { } } +#[derive(Debug)] pub struct ConstantScenarioParameterBuilder { meta: ParameterMeta, values: Vec, diff --git a/pywr-core/src/parameters/control_curves/apportion.rs b/pywr-core/src/parameters/control_curves/apportion.rs index c43a0a6a..09651cc4 100644 --- a/pywr-core/src/parameters/control_curves/apportion.rs +++ b/pywr-core/src/parameters/control_curves/apportion.rs @@ -19,6 +19,7 @@ use std::collections::HashMap; /// is equal to `(1.0 - control_curve) * metric` and the lower amount is equal to /// `control_curve * metric`. /// +#[derive(Debug)] pub struct ApportionParameter { meta: ParameterMeta, metric: MetricF64, @@ -62,6 +63,7 @@ impl GeneralParameter for ApportionParameter { } } +#[derive(Debug)] pub struct ApportionParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/control_curves/index.rs b/pywr-core/src/parameters/control_curves/index.rs index 972d233d..a9f5fcb1 100644 --- a/pywr-core/src/parameters/control_curves/index.rs +++ b/pywr-core/src/parameters/control_curves/index.rs @@ -10,6 +10,7 @@ use crate::state::State; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_f64_vec}; +#[derive(Debug)] pub struct ControlCurveIndexParameter { meta: ParameterMeta, metric: MetricF64, @@ -50,6 +51,7 @@ impl GeneralParameter for ControlCurveIndexParameter { } } +#[derive(Debug)] pub struct ControlCurveIndexParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/control_curves/interpolated.rs b/pywr-core/src/parameters/control_curves/interpolated.rs index 5c7c4c1c..030fb5c8 100644 --- a/pywr-core/src/parameters/control_curves/interpolated.rs +++ b/pywr-core/src/parameters/control_curves/interpolated.rs @@ -11,6 +11,7 @@ use crate::state::State; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_f64_vec}; +#[derive(Debug)] pub struct ControlCurveInterpolatedParameter { meta: ParameterMeta, metric: MetricF64, @@ -67,6 +68,7 @@ impl GeneralParameter for ControlCurveInterpolatedParameter { } } +#[derive(Debug)] pub struct ControlCurveInterpolatedParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/control_curves/piecewise.rs b/pywr-core/src/parameters/control_curves/piecewise.rs index f960c007..c372b292 100644 --- a/pywr-core/src/parameters/control_curves/piecewise.rs +++ b/pywr-core/src/parameters/control_curves/piecewise.rs @@ -11,6 +11,7 @@ use crate::state::State; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_f64_vec}; +#[derive(Debug)] pub struct PiecewiseInterpolatedParameter { meta: ParameterMeta, metric: MetricF64, @@ -72,6 +73,7 @@ impl GeneralParameter for PiecewiseInterpolatedParameter { } } +#[derive(Debug)] pub struct PiecewiseInterpolatedParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/control_curves/simple.rs b/pywr-core/src/parameters/control_curves/simple.rs index 72050e3d..369ec044 100644 --- a/pywr-core/src/parameters/control_curves/simple.rs +++ b/pywr-core/src/parameters/control_curves/simple.rs @@ -10,6 +10,7 @@ use crate::state::State; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_f64_vec}; +#[derive(Debug)] pub struct ControlCurveParameter { meta: ParameterMeta, metric: MetricF64, @@ -70,6 +71,7 @@ impl GeneralParameter for ControlCurveParameter { } } +#[derive(Debug)] pub struct ControlCurveParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/control_curves/volume_between.rs b/pywr-core/src/parameters/control_curves/volume_between.rs index 427b640a..781915d1 100644 --- a/pywr-core/src/parameters/control_curves/volume_between.rs +++ b/pywr-core/src/parameters/control_curves/volume_between.rs @@ -9,8 +9,10 @@ use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; +use std::fmt::Debug; /// A parameter that returns the volume that is the proportion between two control curves +#[derive(Debug)] pub struct VolumeBetweenControlCurvesParameter { meta: ParameterMeta, total: M, @@ -20,7 +22,7 @@ pub struct VolumeBetweenControlCurvesParameter { impl Parameter for VolumeBetweenControlCurvesParameter where - M: Send + Sync, + M: Send + Sync + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -51,6 +53,7 @@ impl SimpleParameter for VolumeBetweenControlCurvesParameter { meta: ParameterMeta, total: M, diff --git a/pywr-core/src/parameters/deficit.rs b/pywr-core/src/parameters/deficit.rs index 98dcd5b8..527487c5 100644 --- a/pywr-core/src/parameters/deficit.rs +++ b/pywr-core/src/parameters/deficit.rs @@ -13,6 +13,7 @@ use crate::timestep::Timestep; /// /// Typically used to represent the deficit between actual inflow and requested max flow at /// a node. +#[derive(Debug)] pub struct DeficitParameter { meta: ParameterMeta, flow: MetricF64, @@ -49,6 +50,7 @@ impl GeneralParameter for DeficitParameter { } } +#[derive(Debug)] pub struct DeficitParameterBuilder { meta: ParameterMeta, flow: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/delay.rs b/pywr-core/src/parameters/delay.rs index 425f5e75..279581fe 100644 --- a/pywr-core/src/parameters/delay.rs +++ b/pywr-core/src/parameters/delay.rs @@ -13,7 +13,9 @@ use crate::state::{SimpleParameterValues, State}; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_u64}; use std::collections::VecDeque; +use std::fmt::Debug; +#[derive(Debug)] pub struct DelayParameter { meta: ParameterMeta, metric: M, @@ -55,8 +57,8 @@ where impl Parameter for DelayParameter where - M: Send + Sync, - T: Send + Sync + Copy + 'static, + M: Send + Sync + Debug, + T: Send + Sync + Copy + Debug + 'static, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -279,6 +281,7 @@ impl SimpleParameter for DelayParameter { } } +#[derive(Debug)] pub struct DelayParameterBuilder { meta: ParameterMeta, metric: M, diff --git a/pywr-core/src/parameters/difference.rs b/pywr-core/src/parameters/difference.rs index 1a9ff0f1..f98fbf95 100644 --- a/pywr-core/src/parameters/difference.rs +++ b/pywr-core/src/parameters/difference.rs @@ -10,6 +10,7 @@ use crate::resolve_metric_f64; use crate::scenario::ScenarioIndex; use crate::state::{SimpleParameterValues, State}; use crate::timestep::Timestep; +use std::fmt::Debug; /// A parameter that computes the difference between two metrics, with optional minimum and maximum bounds. /// @@ -18,6 +19,7 @@ use crate::timestep::Timestep; /// /// If `min` is provided, the result is clamped to be at least `min`. /// If `max` is provided, the result is clamped to be at most `max`. +#[derive(Debug)] pub struct DifferenceParameter { meta: ParameterMeta, a: M, @@ -28,7 +30,7 @@ pub struct DifferenceParameter { impl Parameter for DifferenceParameter where - M: Send + Sync, + M: Send + Sync + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -116,6 +118,7 @@ fn difference(a: f64, b: f64, min: Option, max: Option) -> f64 { result } +#[derive(Debug)] pub struct DifferenceParameterBuilder { meta: ParameterMeta, a: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/discount_factor.rs b/pywr-core/src/parameters/discount_factor.rs index 1edd4f1f..3b78a8aa 100644 --- a/pywr-core/src/parameters/discount_factor.rs +++ b/pywr-core/src/parameters/discount_factor.rs @@ -11,6 +11,7 @@ use crate::state::State; use crate::timestep::Timestep; use chrono::Datelike; +#[derive(Debug)] pub struct DiscountFactorParameter { meta: ParameterMeta, discount_rate: MetricF64, @@ -46,6 +47,7 @@ impl GeneralParameter for DiscountFactorParameter { } } +#[derive(Debug)] pub struct DiscountFactorParameterBuilder { meta: ParameterMeta, discount_rate: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/division.rs b/pywr-core/src/parameters/division.rs index 001c85f1..e628921f 100644 --- a/pywr-core/src/parameters/division.rs +++ b/pywr-core/src/parameters/division.rs @@ -8,6 +8,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct DivisionParameter { meta: ParameterMeta, numerator: MetricF64, @@ -46,6 +47,7 @@ impl GeneralParameter for DivisionParameter { } } +#[derive(Debug)] pub struct DivisionParameterBuilder { meta: ParameterMeta, numerator: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/hydropower.rs b/pywr-core/src/parameters/hydropower.rs index b75ed7ba..9b8aa492 100644 --- a/pywr-core/src/parameters/hydropower.rs +++ b/pywr-core/src/parameters/hydropower.rs @@ -25,6 +25,7 @@ pub struct HydropowerTargetData { pub energy_unit_conversion: Option, } +#[derive(Debug)] pub struct HydropowerTargetParameter { meta: ParameterMeta, actual_flow: Option, @@ -145,6 +146,7 @@ impl GeneralParameter for HydropowerTargetParameter { } } +#[derive(Debug)] pub struct HydropowerTargetParameterBuilder { meta: ParameterMeta, actual_flow: Option, diff --git a/pywr-core/src/parameters/indexed_array.rs b/pywr-core/src/parameters/indexed_array.rs index 5e45bc06..9ae3ae80 100644 --- a/pywr-core/src/parameters/indexed_array.rs +++ b/pywr-core/src/parameters/indexed_array.rs @@ -10,6 +10,7 @@ use crate::state::State; use crate::timestep::Timestep; use crate::{resolve_metric_f64_vec, resolve_metric_u64}; +#[derive(Debug)] pub struct IndexedArrayParameter { meta: ParameterMeta, index_parameter: MetricU64, @@ -53,6 +54,7 @@ impl GeneralParameter for IndexedArrayParameter { } } +#[derive(Debug)] pub struct IndexedArrayParameterBuilder { meta: ParameterMeta, index_parameter: UnresolvedMetricU64, diff --git a/pywr-core/src/parameters/interpolated.rs b/pywr-core/src/parameters/interpolated.rs index 6027641f..b76c1f28 100644 --- a/pywr-core/src/parameters/interpolated.rs +++ b/pywr-core/src/parameters/interpolated.rs @@ -12,6 +12,7 @@ use crate::state::State; use crate::timestep::Timestep; /// A parameter that interpolates a value to a function with given discrete data points. +#[derive(Debug)] pub struct InterpolatedParameter { meta: ParameterMeta, x: MetricF64, @@ -60,6 +61,7 @@ impl GeneralParameter for InterpolatedParameter { } } +#[derive(Debug)] pub struct InterpolatedParameterBuilder { meta: ParameterMeta, x: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/max.rs b/pywr-core/src/parameters/max.rs index c4e8c9a0..c6132702 100644 --- a/pywr-core/src/parameters/max.rs +++ b/pywr-core/src/parameters/max.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct MaxParameter { meta: ParameterMeta, metric: MetricF64, @@ -44,6 +45,7 @@ impl GeneralParameter for MaxParameter { } } +#[derive(Debug)] pub struct MaxParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/min.rs b/pywr-core/src/parameters/min.rs index b20d0695..eb388dc0 100644 --- a/pywr-core/src/parameters/min.rs +++ b/pywr-core/src/parameters/min.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct MinParameter { meta: ParameterMeta, metric: MetricF64, @@ -42,6 +43,7 @@ impl GeneralParameter for MinParameter { } } +#[derive(Debug)] pub struct MinParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 649e23bb..2be0b0c1 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -89,7 +89,7 @@ pub use profiles::{ pub use py::{ParameterInfo, PyClassParameter, PyClassParameterBuilder, PyFuncParameter, PyFuncParameterBuilder}; pub use rolling::{RollingParameter, RollingParameterBuilder}; use std::fmt; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::ops::Deref; @@ -663,7 +663,7 @@ pub fn downcast_variable_config_ref(variable_config: &dyn VariableCo /// A trait that defines a component that produces a value each time-step. /// /// The trait is generic over the type of the value produced. -pub trait Parameter: Send + Sync { +pub trait Parameter: Send + Sync + Debug { fn meta(&self) -> &ParameterMeta; fn name(&self) -> &ParameterName { &self.meta().name @@ -788,7 +788,10 @@ pub enum BuiltParameter { pub enum MaybeBuiltParameter { Built(BuiltParameter), - Retry(Box>), + Retry { + builder: Box>, + parameter_not_found: ParameterName, + }, } impl From> for MaybeBuiltParameter { @@ -797,7 +800,7 @@ impl From> for MaybeBuiltParameter { } } -pub trait ParameterBuilder { +pub trait ParameterBuilder: Debug { /// The name of the parameter fn name(&self) -> &ParameterName; /// Construct a parameter from the builder. @@ -827,8 +830,11 @@ macro_rules! resolve_metric_f64 { match $unresolved.resolve($maps) { Ok(m) => m, Err(err) => { - return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { attr: ($attr).to_string(), @@ -858,8 +864,11 @@ macro_rules! resolve_metric_u64 { match $unresolved.resolve($maps) { Ok(m) => m, Err(err) => { - return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricU64Error { attr: ($attr).to_string(), @@ -886,8 +895,11 @@ macro_rules! resolve_optional_metric_f64 { Some(u) => match u.resolve($maps) { Ok(m) => Some(m), Err(err) => { - return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { attr: ($attr).to_string(), @@ -922,8 +934,11 @@ macro_rules! resolve_metric_f64_vec { match m.resolve($maps) { Ok(m) => resolved.push(m), Err(err) => { - return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { attr: ($attr).to_string(), @@ -953,8 +968,11 @@ macro_rules! resolve_metric_u64_vec { match m.resolve($maps) { Ok(m) => resolved.push(m), Err(err) => { - return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricU64Error { attr: ($attr).to_string(), @@ -986,8 +1004,11 @@ macro_rules! resolve_metric_f64_hashmap { resolved.insert(k.clone(), m); } Err(err) => { - return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricF64Error { attr: ($attr).to_string(), @@ -1019,8 +1040,11 @@ macro_rules! resolve_metric_u64_hashmap { resolved.insert(k.clone(), m); } Err(err) => { - return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { .. } = err { - Ok($crate::parameters::MaybeBuiltParameter::Retry($self)) + return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { parameter } = err { + Ok($crate::parameters::MaybeBuiltParameter::Retry { + builder: $self, + parameter_not_found: parameter, + }) } else { Err($crate::parameters::ParameterBuildError::ResolveMetricU64Error { attr: ($attr).to_string(), @@ -1077,7 +1101,7 @@ pub trait ConstParameter: Parameter { fn as_parameter(&self) -> &dyn Parameter; } -#[derive(Hash, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum GeneralParameterType { Parameter(GeneralParameterIndex), Index(GeneralParameterIndex), @@ -1102,6 +1126,7 @@ impl From> for GeneralParameterType { } } +#[derive(Debug)] pub enum SimpleParameterType { Parameter(SimpleParameterIndex), Index(SimpleParameterIndex), @@ -1126,6 +1151,7 @@ impl From> for SimpleParameterType { } } +#[derive(Debug)] pub enum ConstParameterType { Parameter(ConstParameterIndex), Index(ConstParameterIndex), @@ -1480,6 +1506,7 @@ pub enum ParameterCollectionGeneralCalculationError { } /// A collection of parameters that return different types. +#[derive(Debug)] pub struct ParameterCollection { constant_f64: Vec>>, constant_u64: Vec>>, @@ -2509,13 +2536,15 @@ pub enum ParameterCollectionBuilderError { #[source] source: Box, }, - #[error("Circulate parameters found: {names:?}")] + #[error("Circular (or self) parameter references found: {names:?}")] CircularParameterReference { names: Vec }, + #[error("Parameter not found: {name}.")] + ParameterNotFound { name: ParameterName }, } /// A builder for [`ParameterCollection`] that allows adding parameters without worrying about the /// internal structure of the collection. -#[derive(Default)] +#[derive(Default, Debug)] pub struct ParameterCollectionBuilder { pub f64: Vec>>, pub u64: Vec>>, @@ -2588,8 +2617,11 @@ impl ParameterCollectionBuilder { resolution_maps.parameters_f64.insert(name, idx); } - MaybeBuiltParameter::Retry(builder) => { - failed_f64.push(builder); + MaybeBuiltParameter::Retry { + builder, + parameter_not_found, + } => { + failed_f64.push((builder, parameter_not_found)); } } } @@ -2622,8 +2654,11 @@ impl ParameterCollectionBuilder { resolution_maps.parameters_u64.insert(name, idx); } - MaybeBuiltParameter::Retry(builder) => { - failed_u64.push(builder); + MaybeBuiltParameter::Retry { + builder, + parameter_not_found, + } => { + failed_u64.push((builder, parameter_not_found)); } } } @@ -2656,8 +2691,11 @@ impl ParameterCollectionBuilder { resolution_maps.parameters_multi.insert(name, idx); } - MaybeBuiltParameter::Retry(builder) => { - failed_multi.push(builder); + MaybeBuiltParameter::Retry { + builder, + parameter_not_found, + } => { + failed_multi.push((builder, parameter_not_found)); } } } @@ -2670,27 +2708,36 @@ impl ParameterCollectionBuilder { } } - self.f64 = failed_f64; - self.u64 = failed_u64; - self.multi = failed_multi; - - let new_total = self.len(); + let new_total = failed_f64.len() + failed_u64.len() + failed_multi.len(); if num_unbuilt == new_total { - let failed_names = self - .f64 - .iter() - .map(|b| b.name().clone()) + let (failed_names, missing_names): (Vec<_>, Vec<_>) = failed_f64 + .into_iter() + .map(|(b, pn)| (b.name().clone(), pn)) .chain( - self.u64 - .iter() - .map(|b| b.name().clone()) - .chain(self.multi.iter().map(|b| b.name().clone())), + failed_u64 + .into_iter() + .map(|(b, pn)| (b.name().clone(), pn)) + .chain(failed_multi.into_iter().map(|(b, pn)| (b.name().clone(), pn))), ) - .collect::>(); + .unzip(); + // If any of the missing names are not in the failed names, then we have legitimate + // missing parameter (or typo). + for missing in missing_names { + if !failed_names.contains(&missing) { + return Err(ParameterCollectionBuilderError::ParameterNotFound { name: missing }); + } + } + // Otherwise all the missing names are other failed parameters and this is a circular + // or self reference. return Err(ParameterCollectionBuilderError::CircularParameterReference { names: failed_names }); } else { + // Keep the builders for the next iteration, but we no longer need the missing parameter names. + self.f64 = failed_f64.into_iter().map(|(b, _)| b).collect(); + self.u64 = failed_u64.into_iter().map(|(b, _)| b).collect(); + self.multi = failed_multi.into_iter().map(|(b, _)| b).collect(); + num_unbuilt = new_total; } } @@ -2712,6 +2759,7 @@ mod tests { use crate::state::{ConstParameterValues, MultiValue}; use crate::test_utils::default_domain; + #[derive(Debug)] struct TestParameterBuilder { meta: ParameterMeta, } @@ -2753,6 +2801,7 @@ mod tests { } /// Parameter for testing purposes + #[derive(Debug)] struct TestParameter { meta: ParameterMeta, } diff --git a/pywr-core/src/parameters/multi_threshold.rs b/pywr-core/src/parameters/multi_threshold.rs index e6245a10..dfb35742 100644 --- a/pywr-core/src/parameters/multi_threshold.rs +++ b/pywr-core/src/parameters/multi_threshold.rs @@ -10,6 +10,7 @@ use crate::state::State; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_f64_vec}; +#[derive(Debug)] pub struct MultiThresholdParameter { meta: ParameterMeta, metric: MetricF64, @@ -80,6 +81,7 @@ impl GeneralParameter for MultiThresholdParameter { } } +#[derive(Debug)] pub struct MultiThresholdParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/muskingum.rs b/pywr-core/src/parameters/muskingum.rs index e2c3007e..c0431da2 100644 --- a/pywr-core/src/parameters/muskingum.rs +++ b/pywr-core/src/parameters/muskingum.rs @@ -10,6 +10,7 @@ use crate::state::{MultiValue, State}; use crate::timestep::Timestep; use std::collections::HashMap; +#[derive(Debug)] pub enum MuskingumInitialCondition { SteadyState, Specified { inflow: f64, outflow: f64 }, @@ -32,6 +33,7 @@ pub enum MuskingumInitialCondition { /// - `SteadyState`: Assumes that the inflow and outflow are equal at the first time-step. /// - `Specified`: Allows the user to specify the initial inflow and outflow values. /// +#[derive(Debug)] pub struct MuskingumParameter { meta: ParameterMeta, inflow: MetricF64, @@ -122,6 +124,7 @@ fn steady_state_outflow_factor(weight: f64, travel_time: f64) -> f64 { 1.0 - (2.0 * travel_time * (1.0 - weight) - 1.0) / (2.0 * travel_time * (1.0 - weight) + 1.0) } +#[derive(Debug)] pub struct MuskingumParameterBuilder { meta: ParameterMeta, inflow: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/negative.rs b/pywr-core/src/parameters/negative.rs index afe793f4..f27e6162 100644 --- a/pywr-core/src/parameters/negative.rs +++ b/pywr-core/src/parameters/negative.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct NegativeParameter { meta: ParameterMeta, metric: MetricF64, @@ -43,6 +44,7 @@ impl GeneralParameter for NegativeParameter { } } +#[derive(Debug)] pub struct NegativeParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/negativemax.rs b/pywr-core/src/parameters/negativemax.rs index 9b200486..3995d2be 100644 --- a/pywr-core/src/parameters/negativemax.rs +++ b/pywr-core/src/parameters/negativemax.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct NegativeMaxParameter { meta: ParameterMeta, metric: MetricF64, @@ -42,6 +43,7 @@ impl GeneralParameter for NegativeMaxParameter { } } +#[derive(Debug)] pub struct NegativeMaxParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/negativemin.rs b/pywr-core/src/parameters/negativemin.rs index 68453fec..7227f02f 100644 --- a/pywr-core/src/parameters/negativemin.rs +++ b/pywr-core/src/parameters/negativemin.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct NegativeMinParameter { meta: ParameterMeta, metric: MetricF64, @@ -42,6 +43,7 @@ impl GeneralParameter for NegativeMinParameter { } } +#[derive(Debug)] pub struct NegativeMinParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/offset.rs b/pywr-core/src/parameters/offset.rs index c68cc743..6ee4a026 100644 --- a/pywr-core/src/parameters/offset.rs +++ b/pywr-core/src/parameters/offset.rs @@ -11,6 +11,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct OffsetParameter { meta: ParameterMeta, metric: MetricF64, @@ -114,6 +115,7 @@ impl VariableParameter for OffsetParameter { } } +#[derive(Debug)] pub struct OffsetParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/polynomial.rs b/pywr-core/src/parameters/polynomial.rs index e58d06af..5b7270ee 100644 --- a/pywr-core/src/parameters/polynomial.rs +++ b/pywr-core/src/parameters/polynomial.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct Polynomial1DParameter { meta: ParameterMeta, metric: MetricF64, @@ -53,6 +54,7 @@ impl GeneralParameter for Polynomial1DParameter { } } +#[derive(Debug)] pub struct Polynomial1DParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/profiles/daily.rs b/pywr-core/src/parameters/profiles/daily.rs index 618df3e4..dae9b0c8 100644 --- a/pywr-core/src/parameters/profiles/daily.rs +++ b/pywr-core/src/parameters/profiles/daily.rs @@ -8,6 +8,7 @@ use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; +#[derive(Debug)] pub struct DailyProfileParameter { meta: ParameterMeta, values: [f64; 366], @@ -38,6 +39,7 @@ impl SimpleParameter for DailyProfileParameter { } } +#[derive(Debug)] pub struct DailyProfileParameterBuilder { meta: ParameterMeta, values: [f64; 366], diff --git a/pywr-core/src/parameters/profiles/diurnal.rs b/pywr-core/src/parameters/profiles/diurnal.rs index 90f1769b..f362f27b 100644 --- a/pywr-core/src/parameters/profiles/diurnal.rs +++ b/pywr-core/src/parameters/profiles/diurnal.rs @@ -12,6 +12,7 @@ use chrono::Timelike; /// A parameter that defines a profile over a 24-hour period. /// /// The values array should contain 24 values, one for each hour of the day. +#[derive(Debug)] pub struct DiurnalProfileParameter { meta: ParameterMeta, values: [f64; 24], @@ -42,6 +43,7 @@ impl SimpleParameter for DiurnalProfileParameter { } } +#[derive(Debug)] pub struct DiurnalProfileParameterBuilder { meta: ParameterMeta, values: [f64; 24], diff --git a/pywr-core/src/parameters/profiles/monthly.rs b/pywr-core/src/parameters/profiles/monthly.rs index 8594fcd8..50d29bdc 100644 --- a/pywr-core/src/parameters/profiles/monthly.rs +++ b/pywr-core/src/parameters/profiles/monthly.rs @@ -9,18 +9,20 @@ use crate::state::SimpleParameterValues; use crate::timestep::Timestep; use chrono::{Datelike, NaiveDateTime, Timelike}; -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub enum MonthlyInterpDay { First, Last, } +#[derive(Debug)] pub struct MonthlyProfileParameter { meta: ParameterMeta, values: [f64; 12], interp_day: Option, } +#[derive(Debug)] pub struct MonthlyProfileParameterBuilder { meta: ParameterMeta, values: [f64; 12], diff --git a/pywr-core/src/parameters/profiles/rbf.rs b/pywr-core/src/parameters/profiles/rbf.rs index 5588b531..a0218ca7 100644 --- a/pywr-core/src/parameters/profiles/rbf.rs +++ b/pywr-core/src/parameters/profiles/rbf.rs @@ -40,6 +40,7 @@ impl RbfProfileVariableConfig { /// A parameter that interpolates between a set of points using a radial basis function to /// create a daily profile. +#[derive(Debug)] pub struct RbfProfileParameter { meta: ParameterMeta, points: Vec<(u32, f64)>, @@ -291,6 +292,7 @@ impl VariableParameter for RbfProfileParameter { } /// Radial basis functions for interpolation. +#[derive(Debug)] pub enum RadialBasisFunction { Linear, Cubic, @@ -377,6 +379,7 @@ fn interpolate_rbf_profile(points: &[(u32, f64)], function: &RadialBasisFunction profile.try_into().unwrap() } +#[derive(Debug)] pub struct RbfProfileParameterBuilder { meta: ParameterMeta, points: Vec<(u32, f64)>, diff --git a/pywr-core/src/parameters/profiles/uniform_drawdown.rs b/pywr-core/src/parameters/profiles/uniform_drawdown.rs index 12438c51..6c6a379d 100644 --- a/pywr-core/src/parameters/profiles/uniform_drawdown.rs +++ b/pywr-core/src/parameters/profiles/uniform_drawdown.rs @@ -13,6 +13,7 @@ fn is_leap_year(year: i32) -> bool { (year % 4 == 0) & ((year % 100 != 0) | (year % 400 == 0)) } +#[derive(Debug)] pub struct UniformDrawdownProfileParameter { meta: ParameterMeta, residual_days: u8, @@ -75,6 +76,7 @@ impl SimpleParameter for UniformDrawdownProfileParameter { } } +#[derive(Debug)] pub struct UniformDrawdownProfileParameterBuilder { meta: ParameterMeta, residual_days: u8, diff --git a/pywr-core/src/parameters/profiles/weekly.rs b/pywr-core/src/parameters/profiles/weekly.rs index be1f4972..b736a1e1 100644 --- a/pywr-core/src/parameters/profiles/weekly.rs +++ b/pywr-core/src/parameters/profiles/weekly.rs @@ -9,12 +9,14 @@ use crate::state::SimpleParameterValues; use crate::timestep::Timestep; use thiserror::Error; +#[derive(Debug)] pub enum WeeklyInterpDay { First, Last, } // A weekly profile can be 52 or 53 week long +#[derive(Debug)] pub enum WeeklyProfileValues { FiftyTwo([f64; 52]), FiftyThree([f64; 53]), @@ -157,6 +159,7 @@ impl TryFrom<&[f64]> for WeeklyProfileValues { } /// Weekly profile parameter. This supports a profile with either 52 or 53 weeks, with or without interpolation. +#[derive(Debug)] pub struct WeeklyProfileParameter { meta: ParameterMeta, values: WeeklyProfileValues, @@ -188,6 +191,7 @@ impl SimpleParameter for WeeklyProfileParameter { } } +#[derive(Debug)] pub struct WeeklyProfileParameterBuilder { meta: ParameterMeta, values: WeeklyProfileValues, diff --git a/pywr-core/src/parameters/py.rs b/pywr-core/src/parameters/py.rs index 7139621d..abff40a8 100644 --- a/pywr-core/src/parameters/py.rs +++ b/pywr-core/src/parameters/py.rs @@ -53,6 +53,7 @@ impl ParameterInfo { } } +#[derive(Debug)] struct PyCommon { meta: ParameterMeta, args: Py, @@ -103,6 +104,7 @@ impl PyCommon { } } +#[derive(Debug)] struct PyCommonBuilder { meta: ParameterMeta, args: Py, @@ -137,6 +139,7 @@ impl PyCommonBuilder { /// /// This parameter allows you to define a Python class that implements a `before` and/or `after` methods, /// which will be called to compute the parameter values. +#[derive(Debug)] pub struct PyClassParameter { /// This is the user's class that implements the parameter logic. class: Py, @@ -378,6 +381,7 @@ impl GeneralParameter for PyClassParameter { } } +#[derive(Debug)] pub struct PyClassParameterBuilder { class: Py, common: PyCommonBuilder, @@ -493,6 +497,7 @@ impl ParameterBuilder for PyClassParameterBuilder { /// /// This parameter allows you to define a Python function which takes a `ParameterInfo` object as its first argument, /// and then the user defined `args` and `kwargs` as additional arguments. +#[derive(Debug)] pub struct PyFuncParameter { /// This is the user's class that implements the parameter logic. function: Py, @@ -678,6 +683,7 @@ impl GeneralParameter for PyFuncParameter { } } +#[derive(Debug)] pub struct PyFuncParameterBuilder { function: Py, common: PyCommonBuilder, diff --git a/pywr-core/src/parameters/rolling.rs b/pywr-core/src/parameters/rolling.rs index 1048f9c5..8f9947a4 100644 --- a/pywr-core/src/parameters/rolling.rs +++ b/pywr-core/src/parameters/rolling.rs @@ -15,6 +15,7 @@ use crate::state::{SimpleParameterValues, State}; use crate::timestep::Timestep; use crate::{resolve_metric_f64, resolve_metric_u64}; use std::collections::VecDeque; +use std::fmt::Debug; /// A rolling parameter that computes an aggregated value over a specified window of metric /// values. @@ -24,6 +25,7 @@ use std::collections::VecDeque; /// previous metric values are included in the calculation. If an `initial_value` is provided, /// it will be used as the return value until `min_values` number of metric values have been /// processed. +#[derive(Debug)] pub struct RollingParameter { meta: ParameterMeta, metric: M, @@ -65,8 +67,8 @@ impl TryInto> for &RollingPar impl Parameter for RollingParameter where - M: Send + Sync, - AF: Send + Sync, + M: Send + Sync + Debug, + AF: Send + Sync + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -85,8 +87,8 @@ where impl Parameter for RollingParameter where - M: Send + Sync, - AF: Send + Sync, + M: Send + Sync + Debug, + AF: Send + Sync + Debug, { fn meta(&self) -> &ParameterMeta { &self.meta @@ -321,6 +323,7 @@ impl SimpleParameter for RollingParameter } } +#[derive(Debug)] pub struct RollingParameterBuilder { meta: ParameterMeta, metric: M, diff --git a/pywr-core/src/parameters/threshold.rs b/pywr-core/src/parameters/threshold.rs index 79fc9a67..67dacfb3 100644 --- a/pywr-core/src/parameters/threshold.rs +++ b/pywr-core/src/parameters/threshold.rs @@ -10,6 +10,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub enum Predicate { LessThan, GreaterThan, @@ -31,6 +32,7 @@ impl Predicate { } } +#[derive(Debug)] pub struct ThresholdParameter { meta: ParameterMeta, metric: MetricF64, @@ -93,6 +95,7 @@ impl GeneralParameter for ThresholdParameter { } } +#[derive(Debug)] pub struct ThresholdParameterBuilder { meta: ParameterMeta, metric: UnresolvedMetricF64, diff --git a/pywr-core/src/parameters/vector.rs b/pywr-core/src/parameters/vector.rs index 00b2611b..69693151 100644 --- a/pywr-core/src/parameters/vector.rs +++ b/pywr-core/src/parameters/vector.rs @@ -8,6 +8,7 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; +#[derive(Debug)] pub struct VectorParameter { meta: ParameterMeta, values: Vec, @@ -46,6 +47,7 @@ impl GeneralParameter for VectorParameter { } } +#[derive(Debug)] pub struct VectorParameterBuilder { meta: ParameterMeta, values: Vec, diff --git a/pywr-core/src/recorders/memory.rs b/pywr-core/src/recorders/memory.rs index ca11776d..32868cfd 100644 --- a/pywr-core/src/recorders/memory.rs +++ b/pywr-core/src/recorders/memory.rs @@ -27,7 +27,7 @@ pub enum AggregationError { AggFuncError(#[from] AggFuncError), } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Aggregation { scenario: Option, time: Option, @@ -281,7 +281,7 @@ impl RecorderFinalResult for MemoryRecorderResult { } } -#[derive(Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone)] pub enum AggregationOrder { #[default] MetricTimeScenario, @@ -296,6 +296,7 @@ pub enum AggregationOrder { /// /// Users should be aware that this recorder can consume a large amount of memory if the number of /// scenarios, time steps, and metrics is large. +#[derive(Debug)] pub struct MemoryRecorder { meta: RecorderMeta, metric_set_idx: MetricSetIndex, @@ -388,6 +389,7 @@ impl Recorder for MemoryRecorder { } } +#[derive(Debug)] pub struct MemoryRecorderBuilder { meta: RecorderMeta, metric_set: String, diff --git a/pywr-core/src/recorders/metric_set.rs b/pywr-core/src/recorders/metric_set.rs index 8e70ea39..6182d8aa 100644 --- a/pywr-core/src/recorders/metric_set.rs +++ b/pywr-core/src/recorders/metric_set.rs @@ -42,7 +42,7 @@ impl OutputMetric { } } -#[derive(Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct UnresolvedOutputMetric { name: String, attribute: String, @@ -210,6 +210,7 @@ pub enum MetricSetBuilderError { }, } +#[derive(Debug)] pub struct MetricSetBuilder { name: String, aggregator: Option, diff --git a/pywr-core/src/recorders/mod.rs b/pywr-core/src/recorders/mod.rs index 45a2737a..07b097b8 100644 --- a/pywr-core/src/recorders/mod.rs +++ b/pywr-core/src/recorders/mod.rs @@ -33,6 +33,7 @@ use ndarray::Array2; use ndarray::prelude::*; use polars::prelude::PolarsError; use std::any::Any; +use std::fmt::Debug; use thiserror::Error; /// Meta data common to all parameters. @@ -175,7 +176,7 @@ pub trait RecorderFinalResult: Any + Send + Sync { } } -pub trait Recorder: Send + Sync { +pub trait Recorder: Send + Sync + Debug { fn meta(&self) -> &RecorderMeta; fn name(&self) -> &str { self.meta().name.as_str() @@ -234,10 +235,11 @@ pub enum RecorderBuilderError { MetricSetNotFound { name: String }, } -pub trait RecorderBuilder { +pub trait RecorderBuilder: Debug { fn name(&self) -> &str; fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, RecorderBuilderError>; } +#[derive(Debug)] pub struct Array2Recorder { meta: RecorderMeta, metric: MetricF64, @@ -281,6 +283,7 @@ impl Recorder for Array2Recorder { } } +#[derive(Debug)] pub struct Array2RecorderBuilder { meta: RecorderMeta, metric: UnresolvedMetricF64, @@ -317,6 +320,7 @@ impl RecorderBuilder for Array2RecorderBuilder { } } +#[derive(Debug)] pub struct AssertionF64Recorder { meta: RecorderMeta, expected_values: Array2, @@ -380,6 +384,7 @@ expected: `{:?}`"#, } } +#[derive(Debug)] pub struct AssertionF64RecorderBuilder { meta: RecorderMeta, expected_values: Array2, @@ -435,6 +440,7 @@ impl RecorderBuilder for AssertionF64RecorderBuilder { } } +#[derive(Debug)] pub struct AssertionU64Recorder { meta: RecorderMeta, expected_values: Array2, @@ -490,6 +496,7 @@ expected: `{:?}`"#, } } +#[derive(Debug)] pub struct AssertionU64RecorderBuilder { meta: RecorderMeta, expected_values: Array2, @@ -537,6 +544,17 @@ pub struct AssertionFnRecorder { epsilon: f64, } +impl Debug for AssertionFnRecorder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AssertionFnRecorder") + .field("meta", &self.meta) + .field("metric", &self.metric) + .field("ulps", &self.ulps) + .field("epsilon", &self.epsilon) + .finish() + } +} + impl Recorder for AssertionFnRecorder where F: Send + Sync + Fn(&Timestep, &ScenarioIndex) -> f64, @@ -587,6 +605,17 @@ pub struct AssertionFnRecorderBuilder { epsilon: f64, } +impl Debug for AssertionFnRecorderBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AssertionFnRecorderBuilder") + .field("meta", &self.meta) + .field("metric", &self.metric) + .field("ulps", &self.ulps) + .field("epsilon", &self.epsilon) + .finish() + } +} + impl AssertionFnRecorderBuilder where F: Fn(&Timestep, &ScenarioIndex) -> f64, diff --git a/pywr-core/src/virtual_storage.rs b/pywr-core/src/virtual_storage.rs index 27606ca6..ffdae26a 100644 --- a/pywr-core/src/virtual_storage.rs +++ b/pywr-core/src/virtual_storage.rs @@ -32,6 +32,7 @@ pub enum VirtualStorageNodeBuilderError { } /// Builder for creating a [`VirtualStorageNode`] node. +#[derive(Debug)] pub struct VirtualStorageNodeBuilder { name: UnresolvedNode, nodes: Vec, @@ -285,7 +286,7 @@ impl VirtualStorageNodeBuilder { } /// Defines when the virtual storage volume should be reset. -#[derive(Clone)] +#[derive(Debug, Clone)] pub enum VirtualStorageReset { Never, DayOfYear { day: u32, month: Month }, @@ -293,14 +294,14 @@ pub enum VirtualStorageReset { } /// When resetting the virtual storage volume, this enum defines how much volume to set. -#[derive(Clone)] +#[derive(Debug, Clone)] pub enum VirtualStorageResetVolume { Initial, Max, } /// Active periods for a virtual storage node. -#[derive(Clone)] +#[derive(Debug, Clone)] pub enum VirtualStorageActivePeriod { Always, Period { @@ -363,6 +364,7 @@ pub enum VirtualStorageError { /// for the choices. In addition, a rolling window can be provided as a number of time-steps. /// Volume is recovered into the virtual storage after this number of time-steps once per time-step /// with the oldest value added back to the volume. +#[derive(Debug)] pub struct VirtualStorageNode { meta: NodeMeta, nodes: Vec, From 623ce8bd375dff4e5234680f7933b92aa60efc9c Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Fri, 3 Jul 2026 21:02:37 +0100 Subject: [PATCH 07/12] Remove debug println! --- pywr-core/src/node.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/pywr-core/src/node.rs b/pywr-core/src/node.rs index aea3be3d..7d3001d1 100644 --- a/pywr-core/src/node.rs +++ b/pywr-core/src/node.rs @@ -225,7 +225,6 @@ impl NodeBuilder { &self, resolution_maps: &ResolutionMaps, ) -> Result { - println!("Min volume: {:#?}", &self.min_volume); let min_volume = self .min_volume .as_ref() From d2ab7f15ef79c2175e7c60e737bc6cd6bedf70f3 Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Fri, 3 Jul 2026 21:51:54 +0100 Subject: [PATCH 08/12] Handle error in uniform drawdown builder instead of panic. --- pywr-core/src/network.rs | 36 +++++++++++++++---- pywr-core/src/parameters/mod.rs | 2 ++ .../parameters/profiles/uniform_drawdown.rs | 30 ++++++++++------ pywr-schema/src/parameters/profiles.rs | 4 +-- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 5fb3b33b..93d2d87d 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -444,8 +444,6 @@ pub enum NetworkError { #[source] source: Box, }, - #[error("Cannot connect a node to itself: `{name}`")] - NodeConnectToSelf { name: String, sub_name: Option }, #[error("Error in parameter collection: `{0}`")] ParameterCollectionError(#[from] ParameterCollectionError), #[error("Metric set `{0}` already exists")] @@ -1555,10 +1553,6 @@ impl Network { } } -/// Errors returned by [`NetworkBuilder`] -#[derive(Debug, Error)] -pub enum NetworkBuilderError {} - #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct UnresolvedEdge { from: UnresolvedNode, @@ -1763,6 +1757,7 @@ impl ResolutionMaps { type NodeEdgeMap = HashMap>; +/// Errors returned by [`NetworkBuilder`] #[derive(Debug, Error)] pub enum NetworkBuildError { #[error("Duplicate node names found: {name}")] @@ -1791,6 +1786,8 @@ pub enum NetworkBuildError { #[source] source: Box, }, + #[error("Cannot connect a node to itself: `{name}`")] + NodeConnectToSelf { name: UnresolvedNode }, #[error("Error building aggregated node `{name}`: {source}")] AggregatedNodeBuilderError { name: UnresolvedNode, @@ -1950,6 +1947,11 @@ impl NetworkBuilder { edge: Box::new(edge.clone()), })?; + // Self connections are forbidden. + if from_node_index == to_node_index { + return Err(NetworkBuildError::NodeConnectToSelf { name: edge.to.clone() }); + } + outgoing.entry(*from_node_index).or_default().push(ei); incoming.entry(*to_node_index).or_default().push(ei); } @@ -2319,6 +2321,28 @@ mod tests { ); } + #[test] + /// Test connecting to yourself is forbidden. + fn test_self_connection() { + let mut builder = NetworkBuilder::default(); + + builder + .node(NodeBuilder::input("my-input")) + .node(NodeBuilder::link("my-link")) + .node(NodeBuilder::output("my-output")); + + builder.connect("my-input", "my-link"); + builder.connect("my-link", "my-link"); + builder.connect("my-link", "my-output"); + + let domain = default_domain(); + let result = builder.build(&domain, &HashMap::new()); + assert!( + matches!(result, Err(NetworkBuildError::NodeConnectToSelf { name}) if name.to_string() == "my-link" + ) + ); + } + #[test] /// Test adding a constant parameter to a network. fn test_constant_parameter() { diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 2be0b0c1..8b8780d5 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -778,6 +778,8 @@ pub enum ParameterBuildError { #[source] source: MetricF64Error, }, + #[error("Could not compute day of the year; invalid date: day: {day}, month: {day}")] + InvalidDayOfYear { day: u32, month: u32 }, } pub enum BuiltParameter { diff --git a/pywr-core/src/parameters/profiles/uniform_drawdown.rs b/pywr-core/src/parameters/profiles/uniform_drawdown.rs index 6c6a379d..57114271 100644 --- a/pywr-core/src/parameters/profiles/uniform_drawdown.rs +++ b/pywr-core/src/parameters/profiles/uniform_drawdown.rs @@ -80,22 +80,24 @@ impl SimpleParameter for UniformDrawdownProfileParameter { pub struct UniformDrawdownProfileParameterBuilder { meta: ParameterMeta, residual_days: u8, - reset_doy: u16, + reset_day: u32, + reset_month: u32, } impl UniformDrawdownProfileParameterBuilder { - pub fn new(name: ParameterName, reset_day: u32, reset_month: u32, residual_days: u8) -> Self { - // Calculate the reset day of year in a known leap year. - let reset_doy = NaiveDate::from_ymd_opt(2016, reset_month, reset_day) - .expect("Invalid reset day") - .ordinal() as u16; - + pub fn new(name: ParameterName, reset_day: u32, reset_month: u32) -> Self { Self { meta: ParameterMeta::new(name), - residual_days, - reset_doy, + residual_days: 0, + reset_month, + reset_day, } } + + pub fn residual_days(&mut self, residual_days: u8) -> &mut Self { + self.residual_days = residual_days; + self + } } impl ParameterBuilder for UniformDrawdownProfileParameterBuilder { @@ -107,10 +109,18 @@ impl ParameterBuilder for UniformDrawdownProfileParameterBuilder { self: Box, _resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { + // Calculate the reset day of year in a known leap year. + let reset_doy = NaiveDate::from_ymd_opt(2016, self.reset_month, self.reset_day) + .ok_or(ParameterBuildError::InvalidDayOfYear { + day: self.reset_day, + month: self.reset_month, + })? + .ordinal() as u16; + let p = UniformDrawdownProfileParameter { meta: self.meta, residual_days: self.residual_days, - reset_doy: self.reset_doy, + reset_doy, }; Ok(MaybeBuiltParameter::Built(BuiltParameter::Simple(Box::new(p)))) diff --git a/pywr-schema/src/parameters/profiles.rs b/pywr-schema/src/parameters/profiles.rs index 729b0ba0..2f53915b 100644 --- a/pywr-schema/src/parameters/profiles.rs +++ b/pywr-schema/src/parameters/profiles.rs @@ -199,12 +199,12 @@ impl UniformDrawdownProfileParameter { None => 0, }; - let p = pywr_core::parameters::UniformDrawdownProfileParameterBuilder::new( + let mut p = pywr_core::parameters::UniformDrawdownProfileParameterBuilder::new( ParameterName::new(&self.meta.name, parent), reset_day, reset_month, - residual_days, ); + p.residual_days(residual_days); network.parameters().f64(Box::new(p)); From c62b6dd8faff614a141ecb392ba3bbef3ff277a7 Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Wed, 8 Jul 2026 20:57:01 +0100 Subject: [PATCH 09/12] Handle multi-model self transfer errors and duplicate transfer names. --- pywr-core/src/models/multi.rs | 69 ++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index 90097c47..f00f1a33 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -2,8 +2,8 @@ use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, Unresol use crate::models::{ModelDomain, ModelDomainBuilder, ModelDomainBuilderError}; use crate::network::{ Network, NetworkBuildError, NetworkBuilder, NetworkFinaliseError, NetworkRecorderSaveError, - NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, NetworkStepError, NetworkTimings, - RunDuration, + NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, + NetworkStepError, NetworkTimings, RunDuration, }; use crate::recorders::RecorderInternalState; use crate::scenario::ScenarioIndex; @@ -47,11 +47,14 @@ enum OtherNetworkIndex { } impl OtherNetworkIndex { - fn new(from_idx: usize, to_idx: usize) -> Self { + /// Create an [`OtherNetworkIndex`] from two indices. + /// + /// If `from_idx` is equal to `to_idx` then `None` is returned, as a model cannot reference itself. + fn from_indices(from_idx: usize, to_idx: usize) -> Option { match from_idx.cmp(&to_idx) { - Ordering::Equal => panic!("Cannot create OtherNetworkIndex to self."), - Ordering::Less => Self::Before(NonZeroUsize::new(to_idx - from_idx).unwrap()), - Ordering::Greater => Self::After(NonZeroUsize::new(from_idx - to_idx).unwrap()), + Ordering::Equal => None, + Ordering::Less => Some(Self::Before(NonZeroUsize::new(to_idx - from_idx).unwrap())), + Ordering::Greater => Some(Self::After(NonZeroUsize::new(from_idx - to_idx).unwrap())), } } } @@ -366,23 +369,6 @@ impl MultiNetworkModel { self.networks.iter().position(|n| n.name == name) } - /// Add a transfer of data from one network to another. - pub fn add_inter_network_transfer( - &mut self, - from_network_idx: usize, - from_metric: MetricF64, - to_network_idx: usize, - initial_value: Option, - ) { - let parameter = MultiNetworkTransfer { - from_model_idx: OtherNetworkIndex::new(from_network_idx, to_network_idx), - from_metric, - initial_value, - }; - - self.networks[to_network_idx].transfers.push(parameter); - } - pub fn setup( &self, settings: &S::Settings, @@ -969,6 +955,10 @@ pub enum MultiNetworkModelBuilderError { }, #[error("Duplicate network name `{name}` found.")] DuplicateNetworkName { name: String }, + #[error("Duplicate transfer name `{transfer}` found in network `{network}`.")] + DuplicateTransferName { transfer: String, network: String }, + #[error("Transfer `{name}` in network `{network}` cannot transfer from itself.")] + TransferToSelf { name: String, network: String }, } #[cfg(feature = "pyo3")] @@ -1010,16 +1000,20 @@ impl MultiNetworkModelBuilder { for (i, entry_builder) in self.networks.into_iter().enumerate() { let name = entry_builder.name; - let transfers_map: HashMap<_, _> = entry_builder - .transfers - .iter() - .enumerate() - .map(|(i, t)| { - let idx = MultiNetworkTransferIndex(i); - let name = t.name.clone(); - (name, idx) - }) - .collect(); + // Build a map of transfer names to their indices for this network + let mut transfers_map = HashMap::with_capacity(entry_builder.transfers.len()); + for (i, t) in entry_builder.transfers.iter().enumerate() { + let idx = MultiNetworkTransferIndex(i); + let transfer_name = t.name.clone(); + if transfers_map.contains_key(&transfer_name) { + return Err(MultiNetworkModelBuilderError::DuplicateTransferName { + transfer: transfer_name, + network: name, + }); + } + + transfers_map.insert(transfer_name, idx); + } let (network, resolution_map) = entry_builder.network.build(&domain, &transfers_map).map_err(|source| { MultiNetworkModelBuilderError::NetworkBuilderError { @@ -1059,8 +1053,15 @@ impl MultiNetworkModelBuilder { } })?; + let from_model_idx = OtherNetworkIndex::from_indices(idx, entries.len()).ok_or_else(|| { + MultiNetworkModelBuilderError::TransferToSelf { + name: t.name.clone(), + network: name.clone(), + } + })?; + Ok(MultiNetworkTransfer { - from_model_idx: OtherNetworkIndex::new(idx, entries.len()), + from_model_idx, from_metric: metric, initial_value: t.initial_value, }) From 42078d1a3c026fb02b3085bb5f5a0d46e7f9aac2 Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Wed, 8 Jul 2026 21:02:49 +0100 Subject: [PATCH 10/12] Remove dead code. --- pywr-core/src/network.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 0faf1038..834f1c5d 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -1023,11 +1023,6 @@ impl Network { .compute_general(timestep, scenario_index, self, state, internal_states, p_timings) .map_err(|source| NetworkStepError::GeneralParameterCalculationError(Box::new(source)))?; - // if let Some(timings) = timings.as_deref_mut() { - // // Update the component timings - // timings.add_component_calculation_timing(c_idx, start.elapsed()); - // } - Ok(()) } From 634b28f815fbd1d45827b9a0f09d72d2226e2352 Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Thu, 9 Jul 2026 11:24:27 +0100 Subject: [PATCH 11/12] Refactor model builders to accept built domain. Removes double building of the domain. --- pywr-core/src/aggregated_node.rs | 10 +++++----- pywr-core/src/models/multi.rs | 31 +++++++++++++++---------------- pywr-core/src/models/simple.rs | 16 ++++++++-------- pywr-core/src/network.rs | 6 +++--- pywr-core/src/test_utils.rs | 10 ++++++---- pywr-core/src/virtual_storage.rs | 4 ++-- pywr-schema/src/model.rs | 17 +++++++++-------- 7 files changed, 48 insertions(+), 46 deletions(-) diff --git a/pywr-core/src/aggregated_node.rs b/pywr-core/src/aggregated_node.rs index 5be52c91..6732311d 100644 --- a/pywr-core/src/aggregated_node.rs +++ b/pywr-core/src/aggregated_node.rs @@ -1092,7 +1092,7 @@ mod tests { use crate::parameters::{MonthlyProfileParameterBuilder, ParameterName}; use crate::recorders::AssertionF64RecorderBuilder; use crate::state::ParameterReturnValue; - use crate::test_utils::{default_domain_builder, run_all_solvers}; + use crate::test_utils::{default_domain, run_all_solvers}; use ndarray::Array2; /// Test the factors forcing a simple ratio of flow @@ -1164,7 +1164,7 @@ mod tests { ); builder.recorder(Box::new(recorder)); - let domain = default_domain_builder(); + let domain = default_domain(); let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["ipm-simd", "ipm-ocl"], &[], &[]); @@ -1248,7 +1248,7 @@ mod tests { ); builder.recorder(Box::new(recorder)); - let domain = default_domain_builder(); + let domain = default_domain(); let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["cbc", "ipm-simd", "ipm-ocl"], &[], &[]); @@ -1326,7 +1326,7 @@ mod tests { ); builder.recorder(Box::new(recorder)); - let domain = default_domain_builder(); + let domain = default_domain(); let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["clp", "ipm-simd", "ipm-ocl"], &[], &[]); @@ -1435,7 +1435,7 @@ mod tests { ); builder.recorder(Box::new(recorder)); - let domain = default_domain_builder(); + let domain = default_domain(); let model = ModelBuilder::new(domain, builder).build().unwrap(); run_all_solvers(&model, &["clp", "ipm-ocl", "ipm-simd"], &[], &[]); diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index f00f1a33..e2553fe5 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -1,5 +1,5 @@ use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; -use crate::models::{ModelDomain, ModelDomainBuilder, ModelDomainBuilderError}; +use crate::models::ModelDomain; use crate::network::{ Network, NetworkBuildError, NetworkBuilder, NetworkFinaliseError, NetworkRecorderSaveError, NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, @@ -937,8 +937,6 @@ impl MultiNetworkModel { #[derive(Debug, Error)] pub enum MultiNetworkModelBuilderError { - #[error("Error building model domain: {0}")] - ModelDomainBuilderError(#[from] ModelDomainBuilderError), #[error("Error building network `{name}`: {source}")] NetworkBuilderError { name: String, @@ -969,12 +967,12 @@ impl From for PyErr { } pub struct MultiNetworkModelBuilder { - domain: ModelDomainBuilder, + domain: ModelDomain, networks: Vec, } impl MultiNetworkModelBuilder { - pub fn new(domain: ModelDomainBuilder) -> Self { + pub fn new(domain: ModelDomain) -> Self { Self { domain, networks: Vec::new(), @@ -992,8 +990,6 @@ impl MultiNetworkModelBuilder { } pub fn build(self) -> Result { - let domain = self.domain.build()?; - let mut networks = Vec::with_capacity(self.networks.len()); let mut network_map = HashMap::with_capacity(self.networks.len()); let mut resolution_maps = Vec::with_capacity(self.networks.len()); @@ -1015,12 +1011,14 @@ impl MultiNetworkModelBuilder { transfers_map.insert(transfer_name, idx); } - let (network, resolution_map) = entry_builder.network.build(&domain, &transfers_map).map_err(|source| { - MultiNetworkModelBuilderError::NetworkBuilderError { - name: name.clone(), - source: Box::new(source), - } - })?; + let (network, resolution_map) = + entry_builder + .network + .build(&self.domain, &transfers_map) + .map_err(|source| MultiNetworkModelBuilderError::NetworkBuilderError { + name: name.clone(), + source: Box::new(source), + })?; networks.push((name.clone(), network, entry_builder.transfers)); resolution_maps.push(resolution_map); @@ -1078,7 +1076,7 @@ impl MultiNetworkModelBuilder { } Ok(MultiNetworkModel { - domain, + domain: self.domain, networks: entries, }) } @@ -1171,8 +1169,9 @@ mod tests { let mut domain_builder = ModelDomainBuilder::new(time_builder); domain_builder.scenario(scenario_builder); + let domain = domain_builder.build().unwrap(); - let mut builder = MultiNetworkModelBuilder::new(domain_builder); + let mut builder = MultiNetworkModelBuilder::new(domain); let mut network1 = NetworkBuilder::default(); simple_network(&mut network1, "test-scenario", 2); @@ -1201,7 +1200,7 @@ mod tests { fn test_duplicate_network_names() { let time_builder = default_time_domain_builder(); - let mut builder = MultiNetworkModelBuilder::new(ModelDomainBuilder::new(time_builder)); + let mut builder = MultiNetworkModelBuilder::new(ModelDomainBuilder::new(time_builder).build().unwrap()); builder.network(MultiNetworkEntryBuilder::new("network1", NetworkBuilder::default())); builder.network(MultiNetworkEntryBuilder::new("network1", NetworkBuilder::default())); diff --git a/pywr-core/src/models/simple.rs b/pywr-core/src/models/simple.rs index d4b2468e..ceda253c 100644 --- a/pywr-core/src/models/simple.rs +++ b/pywr-core/src/models/simple.rs @@ -1,4 +1,4 @@ -use crate::models::{ModelDomain, ModelDomainBuilder, ModelDomainBuilderError}; +use crate::models::ModelDomain; use crate::network::{ Network, NetworkBuildError, NetworkBuilder, NetworkFinaliseError, NetworkRecorderSaveError, NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, @@ -603,8 +603,6 @@ impl Model { #[derive(Debug, Error)] pub enum ModelBuilderError { - #[error("Error building model domain: {0}")] - ModelDomainBuilderError(#[from] ModelDomainBuilderError), #[error("Error building network: {0}")] NetworkBuildError(#[from] NetworkBuildError), } @@ -617,12 +615,12 @@ impl From for PyErr { } pub struct ModelBuilder { - domain: ModelDomainBuilder, + domain: ModelDomain, network: NetworkBuilder, } impl ModelBuilder { - pub fn new(domain: ModelDomainBuilder, network: NetworkBuilder) -> Self { + pub fn new(domain: ModelDomain, network: NetworkBuilder) -> Self { Self { domain, network } } @@ -632,9 +630,11 @@ impl ModelBuilder { } pub fn build(self) -> Result { - let domain = self.domain.build()?; - let (network, _) = self.network.build(&domain, &HashMap::new())?; - Ok(Model { domain, network }) + let (network, _) = self.network.build(&self.domain, &HashMap::new())?; + Ok(Model { + domain: self.domain, + network, + }) } } diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 834f1c5d..8109af57 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -2229,8 +2229,7 @@ mod tests { use crate::recorders::AssertionF64RecorderBuilder; use crate::solvers::{ClpSolver, ClpSolverSettings}; use crate::test_utils::{ - default_domain, default_domain_builder, run_all_solvers, simple_model, simple_storage_model, - simple_storage_network, + default_domain, run_all_solvers, simple_model, simple_storage_model, simple_storage_network, }; use float_cmp::assert_approx_eq; use ndarray::{Array, Array2}; @@ -2575,7 +2574,8 @@ mod tests { ); network.recorder(Box::new(recorder)); - let model = ModelBuilder::new(default_domain_builder(), network).build().unwrap(); + let domain = default_domain(); + let model = ModelBuilder::new(domain, network).build().unwrap(); // Test all solvers run_all_solvers(&model, &[], &[], &[]); diff --git a/pywr-core/src/test_utils.rs b/pywr-core/src/test_utils.rs index df2baeed..4c5b4875 100644 --- a/pywr-core/src/test_utils.rs +++ b/pywr-core/src/test_utils.rs @@ -114,8 +114,9 @@ pub fn simple_model(num_scenarios: usize, time_builder: Option NetworkBuilder { pub fn simple_storage_model() -> ModelBuilder { let network = simple_storage_network(); - ModelBuilder::new(default_domain_builder(), network) + ModelBuilder::new(default_domain(), network) } /// Add the given parameter to the given model along with an assertion recorder that asserts @@ -677,9 +678,10 @@ pub fn make_random_model_builder( let mut domain_builder = ModelDomainBuilder::new(time_builder); domain_builder.scenario(scenario_builder); + let domain = domain_builder.build().unwrap(); // Quickly build the domain to determine its shape so we can setup the correct input data. - let (num_timesteps, num_inflow_scenarios) = domain_builder.clone().build().unwrap().shape(); + let (num_timesteps, num_inflow_scenarios) = domain.shape(); let mut network_builder = NetworkBuilder::default(); for i in 0..num_systems { @@ -696,7 +698,7 @@ pub fn make_random_model_builder( make_simple_connections(&mut network_builder, num_systems, density, rng); - ModelBuilder::new(domain_builder, network_builder) + ModelBuilder::new(domain, network_builder) } #[cfg(all(test, feature = "ipm-simd"))] diff --git a/pywr-core/src/virtual_storage.rs b/pywr-core/src/virtual_storage.rs index ffdae26a..d61158fc 100644 --- a/pywr-core/src/virtual_storage.rs +++ b/pywr-core/src/virtual_storage.rs @@ -512,7 +512,7 @@ mod tests { use crate::parameters::ControlCurveInterpolatedParameterBuilder; use crate::recorders::{AssertionF64RecorderBuilder, AssertionFnRecorderBuilder}; use crate::scenario::ScenarioIndex; - use crate::test_utils::{default_domain_builder, run_all_solvers, simple_model}; + use crate::test_utils::{default_domain, run_all_solvers, simple_model}; use crate::timestep::{TimeDomainBuilder, Timestep, TimestepDuration}; use crate::virtual_storage::{ VirtualStorageActivePeriod, VirtualStorageNodeBuilder, VirtualStorageReset, months_since_last_reset, @@ -651,7 +651,7 @@ mod tests { ); network_builder.recorder(Box::new(recorder)); - let domain = default_domain_builder(); + let domain = default_domain(); let model = ModelBuilder::new(domain, network_builder).build().unwrap(); // Test all solvers run_all_solvers(&model, &["ipm-ocl", "ipm-simd"], &[], &[]); diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index 12a7d572..1bfb6e30 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -22,7 +22,7 @@ use pywr_core::models::Model; #[cfg(feature = "core")] use pywr_core::{ models::{ - ModelBuilder, ModelDomainBuilder, MultiNetworkEntryBuilder, MultiNetworkModelBuilder, + ModelBuilder, ModelDomainBuilder, ModelDomainBuilderError, MultiNetworkEntryBuilder, MultiNetworkModelBuilder, MultiNetworkModelBuilderError, MultiNetworkTransferBuilder, }, timestep::TimestepDuration, @@ -417,6 +417,8 @@ pub enum ModelSchemaBuildError { #[source] source: Box, }, + #[error("Error building model domain: {0}")] + CoreModelDomainBuilderError(#[from] ModelDomainBuilderError), } #[cfg(all(feature = "core", feature = "pyo3"))] @@ -543,8 +545,7 @@ impl ModelSchema { let mut domain_builder = ModelDomainBuilder::new(time_domain_builder); domain_builder.scenario(scenario_builder); - // TODO handle this error - let domain = domain_builder.clone().build().unwrap(); + let domain = domain_builder.build()?; let mut network_builder = pywr_core::network::NetworkBuilder::default(); @@ -554,7 +555,7 @@ impl ModelSchema { source: Box::new(source), })?; - let model_builder = ModelBuilder::new(domain_builder, network_builder); + let model_builder = ModelBuilder::new(domain, network_builder); Ok(model_builder) } @@ -667,8 +668,8 @@ pub struct MultiNetworkEntry { pub enum MultiNetworkModelSchemaBuildError { #[error("Failed to construct scenario builder: {0}")] ScenarioBuilderError(#[from] pywr_core::scenario::ScenarioDomainBuilderError), - #[error("Failed to construct model domain: {0}")] - CoreModelDomainError(#[from] pywr_core::models::ModelDomainError), + #[error("Error building model domain: {0}")] + CoreModelDomainBuilderError(#[from] ModelDomainBuilderError), #[error("Failed to construct the network `{name}`: {source}")] NetworkBuildError { name: String, @@ -836,7 +837,7 @@ impl MultiNetworkModelSchema { let mut domain_builder = ModelDomainBuilder::new(time_builder); domain_builder.scenario(scenario_builder); - let domain = domain_builder.clone().build().unwrap(); + let domain = domain_builder.build()?; let mut network_entry_builders = Vec::with_capacity(self.networks.len()); let mut network_builder_map = HashMap::with_capacity(self.networks.len()); @@ -950,7 +951,7 @@ impl MultiNetworkModelSchema { } // Now construct the model from the loaded components - let mut model_builder = MultiNetworkModelBuilder::new(domain_builder); + let mut model_builder = MultiNetworkModelBuilder::new(domain); for (network_entry, (network_builder, t_builders)) in self .networks From c793548af07bf6954a038993a08f12e5a1b61bef Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Thu, 9 Jul 2026 11:39:42 +0100 Subject: [PATCH 12/12] Remove panic in timing code and handle errors. --- pywr-core/src/models/multi.rs | 23 +++++++++++++++++++---- pywr-core/src/models/simple.rs | 20 ++++++++++++++++---- pywr-core/src/network.rs | 15 +++++++++++---- pywr-core/src/parameters/mod.rs | 22 ++++++++++++++++++---- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index e2553fe5..08664f45 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -5,6 +5,7 @@ use crate::network::{ NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, NetworkStepError, NetworkTimings, RunDuration, }; +use crate::parameters::ParameterCollectionIdMismatchError; use crate::recorders::RecorderInternalState; use crate::scenario::ScenarioIndex; #[cfg(all(feature = "cbc", feature = "pyo3"))] @@ -213,6 +214,11 @@ pub enum MultiNetworkModelFinaliseError { #[source] source: Box, }, + #[error("Timing data from a different network: {source}")] + TimingMismatchError { + #[source] + source: ParameterCollectionIdMismatchError, + }, } #[derive(Debug, Error)] @@ -258,7 +264,10 @@ impl MultiNetworkModelTimings { } /// Print summary statistics of the model run. - fn print_summary_statistics(&self, entries: &[MultiNetworkEntry]) { + fn print_summary_statistics( + &self, + entries: &[MultiNetworkEntry], + ) -> Result<(), ParameterCollectionIdMismatchError> { info!("Run timing statistics:"); let total_duration = self.run_duration.total_duration().as_secs_f64(); info!("{: <24} | {: <10}", "Metric", "Value"); @@ -269,8 +278,10 @@ impl MultiNetworkModelTimings { .get(&entry.name) .expect("Network timings not found for network."); info!("Network: {}", entry.name); - timing.print_table(total_duration, &entry.network); + timing.print_table(total_duration, &entry.network)?; } + + Ok(()) } } @@ -689,7 +700,9 @@ impl MultiNetworkModel { .collect::, _>>()?; timings.finish(); - timings.print_summary_statistics(&self.networks); + timings + .print_summary_statistics(&self.networks) + .map_err(|source| MultiNetworkModelFinaliseError::TimingMismatchError { source })?; Ok(MultiNetworkModelResult { network_results, @@ -730,7 +743,9 @@ impl MultiNetworkModel { .collect::, _>>()?; timings.finish(); - timings.print_summary_statistics(&self.networks); + timings + .print_summary_statistics(&self.networks) + .map_err(|source| MultiNetworkModelFinaliseError::TimingMismatchError { source })?; Ok(MultiNetworkModelResult { network_results, diff --git a/pywr-core/src/models/simple.rs b/pywr-core/src/models/simple.rs index ceda253c..216528a9 100644 --- a/pywr-core/src/models/simple.rs +++ b/pywr-core/src/models/simple.rs @@ -4,6 +4,7 @@ use crate::network::{ NetworkRecorderSetupError, NetworkResult, NetworkSetupError, NetworkSolverSetupError, NetworkState, NetworkStepError, NetworkTimings, RunDuration, }; +use crate::parameters::ParameterCollectionIdMismatchError; use crate::recorders::RecorderInternalState; #[cfg(all(feature = "cbc", feature = "pyo3"))] use crate::solvers::{CbcSolver, build_cbc_settings_py}; @@ -80,6 +81,11 @@ pub enum ModelStepError { pub enum ModelFinaliseError { #[error("Error finalising network: {0}")] NetworkFinaliseError(#[from] NetworkFinaliseError), + #[error("Timing data from a different network: {source}")] + TimingMismatchError { + #[source] + source: ParameterCollectionIdMismatchError, + }, } #[derive(Debug, Error)] @@ -120,12 +126,14 @@ impl ModelTimings { } /// Print summary statistics of the model run. - pub fn print_summary_statistics(&self, network: &Network) { + pub fn print_summary_statistics(&self, network: &Network) -> Result<(), ParameterCollectionIdMismatchError> { info!("Run timing statistics:"); let total_duration = self.run_duration.total_duration().as_secs_f64(); info!("{: <24} | {: <10}", "Metric", "Value"); self.run_duration.print_table(); - self.network_timings.print_table(total_duration, network); + self.network_timings.print_table(total_duration, network)?; + + Ok(()) } } @@ -428,7 +436,9 @@ impl Model { // End the global timer and print the run statistics timings.finish(); - timings.print_summary_statistics(&self.network); + timings + .print_summary_statistics(&self.network) + .map_err(|source| ModelFinaliseError::TimingMismatchError { source })?; Ok(ModelResult { network_result, @@ -458,7 +468,9 @@ impl Model { // End the global timer and print the run statistics timings.finish(); - timings.print_summary_statistics(&self.network); + timings + .print_summary_statistics(&self.network) + .map_err(|source| ModelFinaliseError::TimingMismatchError { source })?; Ok(ModelResult { network_result, diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 8109af57..17ea25d3 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -8,7 +8,7 @@ use crate::node::{Node, NodeBuilder, NodeBuilderError, NodeError, UnresolvedNode use crate::parameters::{ GeneralParameterIndex, GeneralParameterType, ParameterCollection, ParameterCollectionBuilder, ParameterCollectionBuilderError, ParameterCollectionConstCalculationError, ParameterCollectionError, - ParameterCollectionGeneralCalculationError, ParameterCollectionSetupError, + ParameterCollectionGeneralCalculationError, ParameterCollectionIdMismatchError, ParameterCollectionSetupError, ParameterCollectionSimpleCalculationError, ParameterIndex, ParameterName, ParameterStates, ParameterTiming, ParameterTimings, VariableConfig, }; @@ -151,10 +151,11 @@ impl ComponentTimings { &self, n: usize, collection: &ParameterCollection, - ) -> Option> { + ) -> Result>, ParameterCollectionIdMismatchError> { self.parameters .as_ref() .map(|p| p.slowest_parameters_named(n, collection)) + .transpose() } } @@ -186,7 +187,11 @@ impl NetworkTimings { } /// Print a summary of the timings to the log. - pub fn print_table(&self, total_duration: f64, network: &Network) { + pub fn print_table( + &self, + total_duration: f64, + network: &Network, + ) -> Result<(), ParameterCollectionIdMismatchError> { info!( "{: <24} | {: <10.5}s ({:5.2}%)", "Components calcs", @@ -242,7 +247,7 @@ impl NetworkTimings { 100.0 * not_counted / total_duration, ); - if let Some(slowest) = self.component_timings.slowest_components(10, &network.parameters) { + if let Some(slowest) = self.component_timings.slowest_components(10, &network.parameters)? { info!("Slowest components:"); info!( " {: <24} | {: <10} | {: <10} | {: <10} | {:5}", @@ -259,6 +264,8 @@ impl NetworkTimings { ); } } + + Ok(()) } } diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 8b8780d5..deb9234a 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -1343,6 +1343,14 @@ pub enum ParameterCollectionSimpleCalculationError { // Unique ID for each parameter collection. static PARAMETER_COLLECTION_ID: AtomicU64 = AtomicU64::new(0); +#[derive(Debug, Error)] +#[error("Parameter collection ID mismatch: expected {expected}, actual {actual}: {context}")] +pub struct ParameterCollectionIdMismatchError { + expected: u64, + actual: u64, + context: String, +} + #[derive(Default, Copy, Clone)] pub struct ParameterTiming { before: Duration, @@ -1454,19 +1462,25 @@ impl ParameterTimings { &self, n: usize, collection: &ParameterCollection, - ) -> Vec<(ParameterName, ParameterTiming)> { + ) -> Result, ParameterCollectionIdMismatchError> { if self.id != collection.id { - todo!("Return error with ID mismatch"); + return Err(ParameterCollectionIdMismatchError{ + expected: collection.id, + actual: self.id, + context: "ParameterTimings and ParameterCollection must have the same ID to ensure that the indices are correct.".to_string(), + }); } // SAFETY: The id of the timings must match the id of the collection to ensure that the // indices are correct. This is checked above and should be guaranteed by construction. - unsafe { + let slowest = unsafe { self.slowest_parameters(n) .into_iter() .map(|(idx, timing)| (collection.get_general_unchecked(idx).name().clone(), timing)) .collect() - } + }; + + Ok(slowest) } }