diff --git a/pywr-book/listings/adding-a-parameter/src/main.rs b/pywr-book/listings/adding-a-parameter/src/main.rs index 712974d6..a6a00acb 100644 --- a/pywr-book/listings/adding-a-parameter/src/main.rs +++ b/pywr-book/listings/adding-a-parameter/src/main.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -use pywr_core::metric::{MetricF64, UnresolvedMetricF64}; +use pywr_core::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use pywr_core::network::ResolutionMaps; use pywr_core::parameters::{ BuiltParameter, GeneralBeforeParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, @@ -71,7 +71,9 @@ impl ParameterBuilder for MaxParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = MaxParameter { meta: self.meta, diff --git a/pywr-core/src/aggregated_node.rs b/pywr-core/src/aggregated_node.rs index 6732311d..3b513041 100644 --- a/pywr-core/src/aggregated_node.rs +++ b/pywr-core/src/aggregated_node.rs @@ -1,7 +1,10 @@ #![warn(clippy::pedantic)] use crate::NodeIndex; -use crate::metric::{ConstantMetricF64Error, MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; +use crate::metric::{ + ConstantMetricF64Error, MetricConsumerPhase, MetricF64, MetricF64Error, MetricF64ResolutionError, + UnresolvedMetricF64, +}; use crate::network::{AggregatedNodeIndex, Network, ResolutionMaps}; use crate::node::{FlowConstraints, NodeMeta, UnresolvedNode}; use crate::state::{ConstParameterValues, State}; @@ -57,7 +60,7 @@ impl RelationshipBuilder for ProportionalFactorsBuilder { let factors = self .factors .iter() - .map(|f| f.resolve(resolution_maps)) + .map(|f| f.resolve(resolution_maps, MetricConsumerPhase::Before)) .collect::, _>>() .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { attr: "factors".to_string(), @@ -85,7 +88,7 @@ impl RelationshipBuilder for RatioFactorsBuilder { let factors = self .factors .iter() - .map(|f| f.resolve(resolution_maps)) + .map(|f| f.resolve(resolution_maps, MetricConsumerPhase::Before)) .collect::, _>>() .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { attr: "factors".to_string(), @@ -119,7 +122,7 @@ impl RelationshipBuilder for CoefficientFactorsBuilder { let factors = self .factors .iter() - .map(|f| f.resolve(resolution_maps)) + .map(|f| f.resolve(resolution_maps, MetricConsumerPhase::Before)) .collect::, _>>() .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { attr: "factors".to_string(), @@ -129,7 +132,7 @@ impl RelationshipBuilder for CoefficientFactorsBuilder { let rhs = self .rhs .as_ref() - .map(|r| r.resolve(resolution_maps)) + .map(|r| r.resolve(resolution_maps, MetricConsumerPhase::Before)) .transpose() .map_err(|source| RelationshipBuildError::ResolveMetricF64Error { attr: "rhs".to_string(), @@ -610,7 +613,7 @@ impl AggregatedNodeBuilder { .as_ref() .map(|min_flow| { min_flow - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| AggregatedNodeBuilderError::ResolveMetricF64Error { attr: "min_flow".to_string(), source, @@ -623,7 +626,7 @@ impl AggregatedNodeBuilder { .as_ref() .map(|max_flow| { max_flow - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| AggregatedNodeBuilderError::ResolveMetricF64Error { attr: "max_flow".to_string(), source, @@ -1089,9 +1092,8 @@ mod tests { use crate::models::ModelBuilder; use crate::network::NetworkBuilder; use crate::node::{NodeBuilder, UnresolvedNode}; - use crate::parameters::{MonthlyProfileParameterBuilder, ParameterName}; + use crate::parameters::{MonthlyProfileParameterBuilder, ParameterName, ParameterReturnValue}; use crate::recorders::AssertionF64RecorderBuilder; - use crate::state::ParameterReturnValue; use crate::test_utils::{default_domain, run_all_solvers}; use ndarray::Array2; diff --git a/pywr-core/src/metric.rs b/pywr-core/src/metric.rs index 78c8fd01..f0992209 100644 --- a/pywr-core/src/metric.rs +++ b/pywr-core/src/metric.rs @@ -5,10 +5,13 @@ use crate::network::{ VirtualStorageIndex, }; use crate::node::{NodeError, UnresolvedNode}; -use crate::parameters::{ConstParameterIndex, GeneralParameterIndex, ParameterName, SimpleParameterIndex}; +use crate::parameters::{ + ConstParameterIndex, GeneralAfterValueIndex, GeneralBeforeValueIndex, ParameterIndex, ParameterName, + ParameterReturnValue, SimpleParameterIndex, +}; use crate::state::{ - ConstParameterValues, ConstParameterValuesError, MultiValue, NetworkStateError, ParameterReturnValue, - SimpleParameterValues, SimpleParameterValuesError, State, StateError, + ConstParameterValues, ConstParameterValuesError, MultiValue, NetworkStateError, SimpleParameterValues, + SimpleParameterValuesError, State, StateError, }; use num::Zero; use thiserror::Error; @@ -63,16 +66,13 @@ pub enum SimpleMetricF64Error { pub enum SimpleMetricF64 { ParameterValue { index: SimpleParameterIndex, - return_value: ParameterReturnValue, }, IndexParameterValue { index: SimpleParameterIndex, - return_value: ParameterReturnValue, }, MultiParameterValue { index: SimpleParameterIndex, key: String, - return_value: ParameterReturnValue, }, Constant(ConstantMetricF64), } @@ -80,15 +80,9 @@ pub enum SimpleMetricF64 { impl SimpleMetricF64 { pub fn get_value(&self, values: &SimpleParameterValues) -> Result { match self { - SimpleMetricF64::ParameterValue { index, return_value } => Ok(values.get_f64(*index, *return_value)?), - SimpleMetricF64::IndexParameterValue { index, return_value } => { - Ok(values.get_u64(*index, *return_value)? as f64) - } - SimpleMetricF64::MultiParameterValue { - index, - key, - return_value, - } => Ok(values.get_multi_f64(*index, key, *return_value)?), + SimpleMetricF64::ParameterValue { index } => Ok(values.get_f64(*index)?), + SimpleMetricF64::IndexParameterValue { index } => Ok(values.get_u64(*index)? as f64), + SimpleMetricF64::MultiParameterValue { index, key } => Ok(values.get_multi_f64(*index, key)?), SimpleMetricF64::Constant(m) => Ok(m.get_value(values.get_constant_values())?), } } @@ -135,6 +129,8 @@ pub enum MetricF64Error { SimpleMetricError(#[from] SimpleMetricF64Error), #[error("Cannot simplify metric to a simple metric")] CannotSimplifyMetric, + #[error("General parameter with has no key: {key}")] + GeneralMultiValueParameterKeyNotFound { key: String }, } #[derive(Clone, Debug, PartialEq)] @@ -154,18 +150,17 @@ pub enum MetricF64 { indices: Vec, name: String, }, - ParameterValue { - index: GeneralParameterIndex, - return_value: ParameterReturnValue, - }, - IndexParameterValue { - index: GeneralParameterIndex, - return_value: ParameterReturnValue, + ParameterBeforeF64(GeneralBeforeValueIndex), + ParameterAfterF64(GeneralAfterValueIndex), + ParameterBeforeU64(GeneralBeforeValueIndex), + ParameterAfterU64(GeneralAfterValueIndex), + ParameterBeforeMulti { + index: GeneralBeforeValueIndex, + key: String, }, - MultiParameterValue { - index: GeneralParameterIndex, + ParameterAfterMulti { + index: GeneralAfterValueIndex, key: String, - return_value: ParameterReturnValue, }, VirtualStorageVolume(VirtualStorageIndex), VirtualStorageProportionalVolume(VirtualStorageIndex), @@ -241,17 +236,24 @@ impl MetricF64 { .sum::>()?; Ok(flow) } - MetricF64::ParameterValue { index, return_value } => { - Ok(state.get_general_parameter_value(*index, *return_value)?) + MetricF64::ParameterBeforeF64(idx) => Ok(state.get_general_parameter_f64_before(*idx)?), + MetricF64::ParameterAfterF64(idx) => Ok(state.get_general_parameter_f64_after(*idx)?), + MetricF64::ParameterBeforeU64(idx) => Ok(state.get_general_parameter_u64_before(*idx)? as f64), + MetricF64::ParameterAfterU64(idx) => Ok(state.get_general_parameter_u64_after(*idx)? as f64), + MetricF64::ParameterBeforeMulti { index, key } => { + let mv = state.get_general_parameter_multi_before(*index)?; + let value = mv + .get_value(key) + .ok_or_else(|| MetricF64Error::GeneralMultiValueParameterKeyNotFound { key: key.clone() })?; + Ok(*value) } - MetricF64::IndexParameterValue { index, return_value } => { - Ok(state.get_general_parameter_index(*index, *return_value)? as f64) + MetricF64::ParameterAfterMulti { index, key } => { + let mv = state.get_general_parameter_multi_after(*index)?; + let value = mv + .get_value(key) + .ok_or_else(|| MetricF64Error::GeneralMultiValueParameterKeyNotFound { key: key.clone() })?; + Ok(*value) } - MetricF64::MultiParameterValue { - 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)?) @@ -418,27 +420,13 @@ where } } -// impl TryFrom> for SimpleMetricF64 { -// type Error = MetricF64Error; -// fn try_from(idx: ParameterIndex) -> Result { -// match idx { -// ParameterIndex::Simple(idx) => Ok(Self::ParameterValue(idx)), -// ParameterIndex::Const(idx) => Ok(Self::Constant(ConstantMetricF64::ParameterValue(idx))), -// ParameterIndex::General(_) => Err(MetricF64Error::CannotSimplifyMetric), -// } -// } -// } -// -// impl TryFrom> for SimpleMetricU64 { -// type Error = MetricU64Error; -// fn try_from(idx: ParameterIndex) -> Result { -// match idx { -// ParameterIndex::Simple(idx) => Ok(Self::IndexParameterValue(idx)), -// ParameterIndex::Const(idx) => Ok(Self::Constant(ConstantMetricU64::IndexParameterValue(idx))), -// ParameterIndex::General(_) => Err(MetricU64Error::CannotSimplifyMetric), -// } -// } -// } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum MetricConsumerPhase { + #[default] + Before, + After, + Both, +} #[derive(Debug, Error)] pub enum MetricF64ResolutionError { @@ -456,6 +444,14 @@ pub enum MetricF64ResolutionError { VirtualStorageNodeNotFound { node: UnresolvedNode }, #[error("Inter-network transfer not found when resolving F64 metric: {transfer}")] InterNetworkTransferNotFound { transfer: String }, + #[error( + "Parameter not registered in the correct phase when resolving F64 metric: {parameter}, consumer phase: {consumer_phase:?}, return value: {return_value:?}" + )] + ParameterNotRegisteredInCorrectPhase { + parameter: ParameterName, + consumer_phase: MetricConsumerPhase, + return_value: ParameterReturnValue, + }, } #[derive(Debug, Clone, PartialEq)] @@ -528,6 +524,15 @@ impl UnresolvedMetricF64 { } } + /// Create a new [`Self::ParameterValue`] variant with the given parameter name and a return + /// value of [`ParameterReturnValue::AfterOrElseInitial`]. + pub fn new_parameter_after_else_initial>(name: N) -> Self { + Self::ParameterValue { + name: name.into(), + return_value: ParameterReturnValue::AfterOrElseInitial, + } + } + /// 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 { @@ -548,207 +553,231 @@ impl UnresolvedMetricF64 { matches!(self, UnresolvedMetricF64::Constant(v) if *v == 0.0) } - 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(), - } + /// Resolve the [`UnresolvedMetricF64`] into a [`MetricF64`] using the provided [`ResolutionMaps`]. + /// + /// The `consumer_phase` parameter is used to determine which phase the consumer will be using + /// the metric in. This is important for resolving parameter metrics, as the phase determines whether + /// the value of the parameter to use is available. If the consumer phase is + /// [`MetricConsumerPhase::Before`], the metric will must be available in the "before" phase. + /// If the consumer phase is [`MetricConsumerPhase::After`], the metric must be available in the + /// "after" phase. + /// An error will be returned if the consumer phase is not compatible with what the parameter + /// supports. + /// + /// If any data required to resolve the metric is missing, an error will be returned. + pub fn resolve( + &self, + maps: &ResolutionMaps, + consumer_phase: MetricConsumerPhase, + ) -> Result { + let m = match self { + UnresolvedMetricF64::NodeInFlow(unresolved) => { + let idx = 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::NodeInFlow(*idx) + } + UnresolvedMetricF64::NodeOutFlow(unresolved) => { + let idx = 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::NodeOutFlow(*idx) + } + UnresolvedMetricF64::NodeVolume(unresolved) => { + let idx = maps + .nodes + .get(unresolved) + .ok_or_else(|| MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), })?; - MetricF64::NodeVolume(*idx) - } - UnresolvedMetricF64::NodeProportionalVolume(unresolved) => { - let idx = resolution_maps.nodes.get(unresolved).ok_or_else(|| { - MetricF64ResolutionError::NodeNotFound { - node: unresolved.clone(), - } + MetricF64::NodeVolume(*idx) + } + UnresolvedMetricF64::NodeProportionalVolume(unresolved) => { + let idx = 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 { - node: unresolved.clone(), - } - })?; - 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 { - aggregated_node: unresolved.clone(), - } + MetricF64::NodeProportionalVolume(*idx) + } + UnresolvedMetricF64::NodeMaxVolume(unresolved) => { + let idx = maps + .nodes + .get(unresolved) + .ok_or_else(|| MetricF64ResolutionError::NodeNotFound { + 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::NodeMaxVolume(*idx) + } + UnresolvedMetricF64::NodeMaxFlow(unresolved) => { + let idx = maps + .nodes + .get(unresolved) + .ok_or_else(|| MetricF64ResolutionError::NodeNotFound { + 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::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 { - edge: unresolved.clone(), - } + MetricF64::NodeMaxFlow(*idx) + } + UnresolvedMetricF64::AggregatedNodeInFlow(unresolved) => { + let idx = maps.aggregated_nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::AggregatedNodeNotFound { + aggregated_node: unresolved.clone(), + } + })?; + MetricF64::AggregatedNodeInFlow(*idx) + } + UnresolvedMetricF64::AggregatedNodeOutFlow(unresolved) => { + let idx = maps.aggregated_nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::AggregatedNodeNotFound { + aggregated_node: unresolved.clone(), + } + })?; + MetricF64::AggregatedNodeOutFlow(*idx) + } + UnresolvedMetricF64::AggregatedStorageNodeVolume(unresolved) => { + let idx = maps.aggregated_storage_nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::AggregatedStorageNodeNotFound { + aggregated_node: unresolved.clone(), + } + })?; + MetricF64::AggregatedStorageNodeVolume(*idx) + } + UnresolvedMetricF64::AggregatedStorageNodeProportionalVolume(unresolved) => { + let idx = maps.aggregated_storage_nodes.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::AggregatedStorageNodeNotFound { + aggregated_node: unresolved.clone(), + } + })?; + MetricF64::AggregatedStorageNodeProportionalVolume(*idx) + } + UnresolvedMetricF64::EdgeFlow(unresolved) => { + let idx = maps + .edges + .get(unresolved) + .ok_or_else(|| MetricF64ResolutionError::EdgeNotFound { + edge: unresolved.clone(), })?; - MetricF64::EdgeFlow(*idx) - } - UnresolvedMetricF64::MultiEdgeFlow { edges, name } => { - let resolved = edges - .iter() - .map(|unresolved| { - resolution_maps.edges.get(unresolved).copied().ok_or_else(|| { - MetricF64ResolutionError::EdgeNotFound { - edge: unresolved.clone(), - } + MetricF64::EdgeFlow(*idx) + } + UnresolvedMetricF64::MultiEdgeFlow { edges, name } => { + let resolved = edges + .iter() + .map(|unresolved| { + maps.edges + .get(unresolved) + .copied() + .ok_or_else(|| MetricF64ResolutionError::EdgeNotFound { + edge: unresolved.clone(), }) - }) - .collect::, _>>()?; - MetricF64::MultiEdgeFlow { - indices: resolved, - name: name.clone(), - } + }) + .collect::, _>>()?; + MetricF64::MultiEdgeFlow { + indices: resolved, + name: name.clone(), } - UnresolvedMetricF64::ParameterValue { name, return_value } => { - 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::ParameterValue { name, return_value } => { + match maps.parameters_f64.get(name) { + Some(idx) => resolve_parameter_index_f64_to_metric_f64(name, *idx, *return_value, consumer_phase)?, + None => { + // Not found as a F64 parameter; try index parameter instead. + let idx = maps.parameters_u64.get(name).ok_or_else(|| { + MetricF64ResolutionError::ParameterNotFound { + parameter: name.clone(), + } + })?; + + resolve_parameter_index_u64_to_metric_f64(name, *idx, *return_value, consumer_phase)? } } - UnresolvedMetricF64::MultiParameterValue { - name, - key, - return_value, - } => { - let idx = resolution_maps.parameters_multi.get(name).ok_or_else(|| { - MetricF64ResolutionError::ParameterNotFound { + } + UnresolvedMetricF64::MultiParameterValue { + name, + key, + return_value, + } => { + let idx = + 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(), - } - })?; + resolve_parameter_index_multi_to_metric_f64(name, idx.clone(), key, *return_value, consumer_phase)? + } + UnresolvedMetricF64::VirtualStorageVolume(unresolved) => { + let idx = 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::VirtualStorageVolume(*idx) + } + UnresolvedMetricF64::VirtualStorageProportionalVolume(unresolved) => { + let idx = maps.virtual_storage_node.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::VirtualStorageNodeNotFound { + node: unresolved.clone(), + } + })?; - MetricF64::VirtualStorageProportionalVolume(*idx) - } - UnresolvedMetricF64::MultiNodeInFlow { name, nodes: indices } => { - let resolved = indices - .iter() - .map(|unresolved| { - resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { - MetricF64ResolutionError::NodeNotFound { - node: unresolved.clone(), - } + MetricF64::VirtualStorageProportionalVolume(*idx) + } + UnresolvedMetricF64::MultiNodeInFlow { name, nodes: indices } => { + let resolved = indices + .iter() + .map(|unresolved| { + maps.nodes + .get(unresolved) + .copied() + .ok_or_else(|| MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), }) - }) - .collect::, _>>()?; + }) + .collect::, _>>()?; - MetricF64::MultiNodeInFlow { - indices: resolved, - name: name.clone(), - } + MetricF64::MultiNodeInFlow { + indices: resolved, + name: name.clone(), } - UnresolvedMetricF64::MultiNodeOutFlow { name, nodes: indices } => { - let resolved = indices - .iter() - .map(|unresolved| { - resolution_maps.nodes.get(unresolved).copied().ok_or_else(|| { - MetricF64ResolutionError::NodeNotFound { - node: unresolved.clone(), - } + } + UnresolvedMetricF64::MultiNodeOutFlow { name, nodes: indices } => { + let resolved = indices + .iter() + .map(|unresolved| { + maps.nodes + .get(unresolved) + .copied() + .ok_or_else(|| MetricF64ResolutionError::NodeNotFound { + node: unresolved.clone(), }) - }) - .collect::, _>>()?; + }) + .collect::, _>>()?; - MetricF64::MultiNodeOutFlow { - indices: resolved, - name: name.clone(), - } + MetricF64::MultiNodeOutFlow { + indices: resolved, + name: name.clone(), } - UnresolvedMetricF64::InterNetworkTransfer(unresolved) => { - let idx = resolution_maps.inter_network_transfers.get(unresolved).ok_or_else(|| { - MetricF64ResolutionError::InterNetworkTransferNotFound { - transfer: unresolved.clone(), - } - })?; + } + UnresolvedMetricF64::InterNetworkTransfer(unresolved) => { + let idx = maps.inter_network_transfers.get(unresolved).ok_or_else(|| { + MetricF64ResolutionError::InterNetworkTransferNotFound { + transfer: unresolved.clone(), + } + })?; - MetricF64::InterNetworkTransfer(*idx) - } - UnresolvedMetricF64::Constant(value) => (*value).into(), - }; + MetricF64::InterNetworkTransfer(*idx) + } + UnresolvedMetricF64::Constant(value) => (*value).into(), + }; Ok(m) } @@ -760,6 +789,480 @@ impl From for UnresolvedMetricF64 { } } +/// Resolve a [`ParameterIndex`] to a [`MetricF64`] using the provided [`ParameterReturnValue`] +/// and [`MetricConsumerPhase`]. This function is used to determine if a parameter can be resolved to a metric +/// based on the phase in which the consumer is using the metric and the return value of the parameter. +/// +/// If the parameter cannot be resolved to a metric, an error is returned. +fn resolve_parameter_index_f64_to_metric_f64( + name: &ParameterName, + idx: ParameterIndex, + parameter_return_value: ParameterReturnValue, + consumer_phase: MetricConsumerPhase, +) -> Result { + match idx { + // Constant and simple can always be resolved to a metric, regardless of the consumer phase + // as long as the parameter return value is "before". + ParameterIndex::Const(idx) => match parameter_return_value { + ParameterReturnValue::Before => Ok(ConstantMetricF64::ParameterValue(idx).into()), + ParameterReturnValue::After | ParameterReturnValue::AfterOrElseInitial => { + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + }, + ParameterIndex::Simple(idx) => match parameter_return_value { + ParameterReturnValue::Before => Ok(SimpleMetricF64::ParameterValue { index: idx }.into()), + ParameterReturnValue::After | ParameterReturnValue::AfterOrElseInitial => { + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + }, + // General parameters must be validated against the consumer phase to determine if they can be resolved to a metric. + ParameterIndex::General(idx) => { + match (parameter_return_value, consumer_phase) { + (ParameterReturnValue::Before, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, and the parameter is + // providing a "before" value, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeF64(before_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, but the parameter is + // providing a "before" value. This is fine because the "before" value is still + // valid in the "after" phase, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeF64(before_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing a "before" value. This is fine because the "before" + // value is valid in both phases, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeF64(before_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::After, MetricConsumerPhase::Before) + | (ParameterReturnValue::After, MetricConsumerPhase::Both) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. This is not valid because the "after" value is not + // valid in the "before" phase, so we cannot resolve it to a metric. + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + (ParameterReturnValue::After, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterF64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. However, they have specified that using any + // initial value is acceptable, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterF64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterF64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing an "after" value. However, they have specified that using any + // initial value is acceptable in the "before" phase, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterF64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + } + } + } +} + +/// Resolve a [`ParameterIndex`] to a [`MetricF64`] using the provided [`ParameterReturnValue`] +/// and [`MetricConsumerPhase`]. This function is used to determine if a parameter can be resolved to a metric +/// based on the phase in which the consumer is using the metric and the return value of the parameter. +/// +/// If the parameter cannot be resolved to a metric, an error is returned. +fn resolve_parameter_index_u64_to_metric_f64( + name: &ParameterName, + idx: ParameterIndex, + parameter_return_value: ParameterReturnValue, + consumer_phase: MetricConsumerPhase, +) -> Result { + match idx { + // Constant and simple can always be resolved to a metric, regardless of the consumer phase + // as long as the parameter return value is "before". + ParameterIndex::Const(idx) => match parameter_return_value { + ParameterReturnValue::Before => Ok(ConstantMetricF64::IndexParameterValue(idx).into()), + ParameterReturnValue::After | ParameterReturnValue::AfterOrElseInitial => { + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + }, + ParameterIndex::Simple(idx) => match parameter_return_value { + ParameterReturnValue::Before => Ok(SimpleMetricF64::IndexParameterValue { index: idx }.into()), + ParameterReturnValue::After | ParameterReturnValue::AfterOrElseInitial => { + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + }, + // General parameters must be validated against the consumer phase to determine if they can be resolved to a metric. + ParameterIndex::General(idx) => { + match (parameter_return_value, consumer_phase) { + (ParameterReturnValue::Before, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, and the parameter is + // providing a "before" value, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeU64(before_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, but the parameter is + // providing a "before" value. This is fine because the "before" value is still + // valid in the "after" phase, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeU64(before_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing a "before" value. This is fine because the "before" + // value is valid in both phases, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeU64(before_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::After, MetricConsumerPhase::Before) + | (ParameterReturnValue::After, MetricConsumerPhase::Both) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. This is not valid because the "after" value is not + // valid in the "before" phase, so we cannot resolve it to a metric. + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + (ParameterReturnValue::After, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterU64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. However, they have specified that using any + // initial value is acceptable, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterU64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterU64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing an "after" value. However, they have specified that using any + // initial value is acceptable in the "before" phase, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterU64(after_idx)), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + } + } + } +} + +/// Resolve a [`ParameterIndex`] to a [`MetricF64`] using the provided [`ParameterReturnValue`] +/// and [`MetricConsumerPhase`]. This function is used to determine if a parameter can be resolved to a metric +/// based on the phase in which the consumer is using the metric and the return value of the parameter. +/// +/// If the parameter cannot be resolved to a metric, an error is returned. +fn resolve_parameter_index_multi_to_metric_f64( + name: &ParameterName, + idx: ParameterIndex, + key: &str, + parameter_return_value: ParameterReturnValue, + consumer_phase: MetricConsumerPhase, +) -> Result { + match idx { + // Constant and simple can always be resolved to a metric, regardless of the consumer phase + // as long as the parameter return value is "before". + ParameterIndex::Const(index) => match parameter_return_value { + ParameterReturnValue::Before => Ok(ConstantMetricF64::MultiParameterValue { + index, + key: key.to_string(), + } + .into()), + ParameterReturnValue::After | ParameterReturnValue::AfterOrElseInitial => { + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + }, + ParameterIndex::Simple(index) => match parameter_return_value { + ParameterReturnValue::Before => Ok(SimpleMetricF64::MultiParameterValue { + index, + key: key.to_string(), + } + .into()), + ParameterReturnValue::After | ParameterReturnValue::AfterOrElseInitial => { + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + }, + // General parameters must be validated against the consumer phase to determine if they can be resolved to a metric. + ParameterIndex::General(idx) => { + match (parameter_return_value, consumer_phase) { + (ParameterReturnValue::Before, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, and the parameter is + // providing a "before" value, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeMulti { + index: before_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, but the parameter is + // providing a "before" value. This is fine because the "before" value is still + // valid in the "after" phase, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeMulti { + index: before_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing a "before" value. This is fine because the "before" + // value is valid in both phases, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricF64::ParameterBeforeMulti { + index: before_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::After, MetricConsumerPhase::Before) + | (ParameterReturnValue::After, MetricConsumerPhase::Both) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. This is not valid because the "after" value is not + // valid in the "before" phase, so we cannot resolve it to a metric. + Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + (ParameterReturnValue::After, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. However, they have specified that using any + // initial value is acceptable, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing an "after" value. However, they have specified that using any + // initial value is acceptable in the "before" phase, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricF64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricF64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + } + } + } +} + #[derive(Debug, Error)] pub enum ConstantMetricU64Error { #[error("Simple parameter value error: {0}")] @@ -800,12 +1303,10 @@ pub enum SimpleMetricU64Error { pub enum SimpleMetricU64 { IndexParameterValue { index: SimpleParameterIndex, - return_value: ParameterReturnValue, }, MultiParameterValue { index: SimpleParameterIndex, key: String, - return_value: ParameterReturnValue, }, Constant(ConstantMetricU64), } @@ -813,12 +1314,8 @@ pub enum SimpleMetricU64 { impl SimpleMetricU64 { pub fn get_value(&self, values: &SimpleParameterValues) -> Result { match self { - SimpleMetricU64::IndexParameterValue { index, return_value } => Ok(values.get_u64(*index, *return_value)?), - SimpleMetricU64::MultiParameterValue { - index, - key, - return_value, - } => Ok(values.get_multi_u64(*index, key, *return_value)?), + SimpleMetricU64::IndexParameterValue { index } => Ok(values.get_u64(*index)?), + SimpleMetricU64::MultiParameterValue { index, key } => Ok(values.get_multi_u64(*index, key)?), SimpleMetricU64::Constant(m) => Ok(m.get_value(values.get_constant_values())?), } } @@ -839,34 +1336,45 @@ pub enum MetricU64Error { SimpleMetricError(#[from] SimpleMetricU64Error), #[error("Cannot simplify metric to a simple metric")] CannotSimplifyMetric, + #[error("General parameter with has no key: {key}")] + GeneralMultiValueParameterKeyNotFound { key: String }, } #[derive(Clone, Debug, PartialEq)] pub enum MetricU64 { - IndexParameterValue { - index: GeneralParameterIndex, - return_value: ParameterReturnValue, + ParameterBeforeU64(GeneralBeforeValueIndex), + ParameterAfterU64(GeneralAfterValueIndex), + ParameterBeforeMulti { + index: GeneralBeforeValueIndex, + key: String, }, - Simple(SimpleMetricU64), - MultiParameterValue { - index: GeneralParameterIndex, + ParameterAfterMulti { + index: GeneralAfterValueIndex, key: String, - return_value: ParameterReturnValue, }, + Simple(SimpleMetricU64), InterNetworkTransfer(MultiNetworkTransferIndex), } impl MetricU64 { pub fn get_value(&self, _network: &Network, state: &State) -> Result { match self { - Self::IndexParameterValue { index, return_value } => { - Ok(state.get_general_parameter_index(*index, *return_value)?) + Self::ParameterBeforeU64(idx) => Ok(state.get_general_parameter_u64_before(*idx)?), + Self::ParameterAfterU64(idx) => Ok(state.get_general_parameter_u64_after(*idx)?), + Self::ParameterBeforeMulti { index, key } => { + let mv = state.get_general_parameter_multi_before(*index)?; + let value = mv + .get_index(key) + .ok_or_else(|| MetricU64Error::GeneralMultiValueParameterKeyNotFound { key: key.clone() })?; + Ok(*value) + } + Self::ParameterAfterMulti { index, key } => { + let mv = state.get_general_parameter_multi_after(*index)?; + let value = mv + .get_index(key) + .ok_or_else(|| MetricU64Error::GeneralMultiValueParameterKeyNotFound { key: key.clone() })?; + Ok(*value) } - Self::MultiParameterValue { - 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"), } @@ -957,6 +1465,14 @@ pub enum MetricU64ResolutionError { ParameterNotFound { parameter: ParameterName }, #[error("Inter-network transfer not found when resolving U64 metric: {transfer}")] InterNetworkTransferNotFound { transfer: String }, + #[error( + "Parameter not registered in the correct phase when resolving U64 metric: {parameter}, consumer phase: {consumer_phase:?}, return value: {return_value:?}" + )] + ParameterNotRegisteredInCorrectPhase { + parameter: ParameterName, + consumer_phase: MetricConsumerPhase, + return_value: ParameterReturnValue, + }, } #[derive(Debug)] @@ -981,7 +1497,11 @@ impl UnresolvedMetricU64 { return_value: ParameterReturnValue::Before, } } - pub fn resolve(&self, resolution_maps: &ResolutionMaps) -> Result { + pub fn resolve( + &self, + resolution_maps: &ResolutionMaps, + consumer_phase: MetricConsumerPhase, + ) -> Result { let m = match self { UnresolvedMetricU64::ParameterValue { name, return_value } => { let idx = resolution_maps.parameters_u64.get(name).ok_or_else(|| { @@ -990,7 +1510,7 @@ impl UnresolvedMetricU64 { } })?; - idx.into_metric_u64(*return_value) + resolve_parameter_index_u64_to_metric_u64(name, *idx, *return_value, consumer_phase)? } UnresolvedMetricU64::MultiParameterValue { name, @@ -1003,7 +1523,7 @@ impl UnresolvedMetricU64 { } })?; - idx.clone().into_metric_u64(key, *return_value) + resolve_parameter_index_multi_to_metric_u64(name, idx.clone(), key, *return_value, consumer_phase)? } UnresolvedMetricU64::InterNetworkTransfer(unresolved) => { let idx = resolution_maps.inter_network_transfers.get(unresolved).ok_or_else(|| { @@ -1026,3 +1546,291 @@ impl From for UnresolvedMetricU64 { Self::Constant(v) } } + +/// Resolve a [`ParameterIndex`] to a [`MetricU64`] using the provided [`ParameterReturnValue`] +/// and [`MetricConsumerPhase`]. This function is used to determine if a parameter can be resolved to a metric +/// based on the phase in which the consumer is using the metric and the return value of the parameter. +/// +/// If the parameter cannot be resolved to a metric, an error is returned. +fn resolve_parameter_index_u64_to_metric_u64( + name: &ParameterName, + idx: ParameterIndex, + parameter_return_value: ParameterReturnValue, + consumer_phase: MetricConsumerPhase, +) -> Result { + match idx { + // Constant and simple can always be resolved to a metric. + ParameterIndex::Const(idx) => Ok(ConstantMetricU64::IndexParameterValue(idx).into()), + ParameterIndex::Simple(idx) => Ok(SimpleMetricU64::IndexParameterValue { index: idx }.into()), + // General parameters must be validated against the consumer phase to determine if they can be resolved to a metric. + ParameterIndex::General(idx) => { + match (parameter_return_value, consumer_phase) { + (ParameterReturnValue::Before, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, and the parameter is + // providing a "before" value, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricU64::ParameterBeforeU64(before_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, but the parameter is + // providing a "before" value. This is fine because the "before" value is still + // valid in the "after" phase, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricU64::ParameterBeforeU64(before_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing a "before" value. This is fine because the "before" + // value is valid in both phases, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricU64::ParameterBeforeU64(before_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::After, MetricConsumerPhase::Before) + | (ParameterReturnValue::After, MetricConsumerPhase::Both) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. This is not valid because the "after" value is not + // valid in the "before" phase, so we cannot resolve it to a metric. + Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + (ParameterReturnValue::After, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterU64(after_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. However, they have specified that using any + // initial value is acceptable, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterU64(after_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterU64(after_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing an "after" value. However, they have specified that using any + // initial value is acceptable in the "before" phase, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterU64(after_idx)), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + } + } + } +} + +/// Resolve a [`ParameterIndex`] to a [`MetricU64`] using the provided [`ParameterReturnValue`] +/// and [`MetricConsumerPhase`]. This function is used to determine if a parameter can be resolved to a metric +/// based on the phase in which the consumer is using the metric and the return value of the parameter. +/// +/// If the parameter cannot be resolved to a metric, an error is returned. +fn resolve_parameter_index_multi_to_metric_u64( + name: &ParameterName, + idx: ParameterIndex, + key: &str, + parameter_return_value: ParameterReturnValue, + consumer_phase: MetricConsumerPhase, +) -> Result { + match idx { + // Constant and simple can always be resolved to a metric. + ParameterIndex::Const(index) => Ok(ConstantMetricU64::MultiParameterValue { + index, + key: key.to_string(), + } + .into()), + ParameterIndex::Simple(index) => Ok(SimpleMetricU64::MultiParameterValue { + index, + key: key.to_string(), + } + .into()), + // General parameters must be validated against the consumer phase to determine if they can be resolved to a metric. + ParameterIndex::General(idx) => { + match (parameter_return_value, consumer_phase) { + (ParameterReturnValue::Before, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, and the parameter is + // providing a "before" value, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricU64::ParameterBeforeMulti { + index: before_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, but the parameter is + // providing a "before" value. This is fine because the "before" value is still + // valid in the "after" phase, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricU64::ParameterBeforeMulti { + index: before_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::Before, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing a "before" value. This is fine because the "before" + // value is valid in both phases, so we can resolve it to a metric provided the + // parameter index contains a "before" index. + match idx.before { + Some(before_idx) => Ok(MetricU64::ParameterBeforeMulti { + index: before_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::After, MetricConsumerPhase::Before) + | (ParameterReturnValue::After, MetricConsumerPhase::Both) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. This is not valid because the "after" value is not + // valid in the "before" phase, so we cannot resolve it to a metric. + Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }) + } + (ParameterReturnValue::After, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Before) => { + // The consumer is using the metric in the "before" phase, but the parameter is + // providing an "after" value. However, they have specified that using any + // initial value is acceptable, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::After) => { + // The consumer is using the metric in the "after" phase, and the parameter is + // providing an "after" value, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + (ParameterReturnValue::AfterOrElseInitial, MetricConsumerPhase::Both) => { + // The consumer is using the metric in both "before" and "after" phases, and the + // parameter is providing an "after" value. However, they have specified that using any + // initial value is acceptable in the "before" phase, so we can resolve it to a metric provided the + // parameter index contains an "after" index. + match idx.after { + Some(after_idx) => Ok(MetricU64::ParameterAfterMulti { + index: after_idx, + key: key.to_string(), + }), + None => Err(MetricU64ResolutionError::ParameterNotRegisteredInCorrectPhase { + parameter: name.clone(), + consumer_phase, + return_value: parameter_return_value, + }), + } + } + } + } + } +} diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index 08664f45..b1b89431 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; use crate::models::ModelDomain; use crate::network::{ Network, NetworkBuildError, NetworkBuilder, NetworkFinaliseError, NetworkRecorderSaveError, @@ -1059,12 +1059,16 @@ impl MultiNetworkModelBuilder { let r_map = &resolution_maps[idx]; - let metric = t.from_metric.resolve(r_map).map_err(|source| { - MultiNetworkModelBuilderError::ResolveMetricF64ForTransferError { - name: t.name.clone(), - source, - } - })?; + // The consumer phase is after the "from" model has been stepped, so we resolve the metric in the "after" phase. + let metric = t + .from_metric + .resolve(r_map, MetricConsumerPhase::After) + .map_err( + |source| MultiNetworkModelBuilderError::ResolveMetricF64ForTransferError { + name: t.name.clone(), + source, + }, + )?; let from_model_idx = OtherNetworkIndex::from_indices(idx, entries.len()).ok_or_else(|| { MultiNetworkModelBuilderError::TransferToSelf { diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 0c4ebede..b8ead57e 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -1036,8 +1036,8 @@ impl Network { /// Undertake "after" for network components after solve. /// /// This method iterates through the network components (nodes, parameters, etc) to perform - /// pre-solve calculations. For nodes this can be adjustments to storage volume (e.g. to - /// set initial volume). For parameters this involves computing the current value for the + /// post-solve calculations. For nodes this can be adjustments to storage volume (e.g. to + /// set initial volume). For parameters this involves computing the "after" value for /// the timestep. The `state` object is progressively updated with these values during this /// method. fn after( @@ -2227,18 +2227,26 @@ impl NetworkBuilder { #[cfg(test)] mod tests { use super::*; + use crate::agg_funcs::AggFuncF64; use crate::metric::{MetricF64ResolutionError, UnresolvedMetricF64}; use crate::models::ModelBuilder; - use crate::parameters::{ActivationFunction, ControlCurveInterpolatedParameterBuilder}; + use crate::parameters::test_utils::{TestParameterBuilder, test_parameter_state}; + use crate::parameters::{ + ActivationFunction, AggregatedParameterBuilder, Array1ParameterBuilder, ConstantParameterBuilder, + ControlCurveInterpolatedParameterBuilder, MaxParameterBuilder, ParameterIndex, ParameterName, + }; use crate::recorders::AssertionF64RecorderBuilder; + use crate::scenario::{ScenarioDomainBuilder, ScenarioGroupBuilder}; use crate::solvers::{ClpSolver, ClpSolverSettings}; use crate::test_utils::{ - default_domain, 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}; use std::default::Default; use std::ops::Deref; + use std::sync::{Arc, Mutex}; #[test] fn test_simple_network() { @@ -2484,6 +2492,271 @@ mod tests { } } + fn reverse_parameter_chain_network() -> NetworkBuilder { + let mut builder = NetworkBuilder::default(); + let mut input = NodeBuilder::input("input"); + input + .min_flow(UnresolvedMetricF64::new_parameter_before("a")) + .max_flow(UnresolvedMetricF64::new_parameter_before("a")); + builder.node(input).node(NodeBuilder::output("output")); + builder.connect("input", "output"); + + builder.parameters().f64(Box::new(MaxParameterBuilder::new( + "a".into(), + UnresolvedMetricF64::new_parameter_before("b"), + 0.0, + ))); + let mut b = AggregatedParameterBuilder::before("b".into(), AggFuncF64::Sum); + b.metric(UnresolvedMetricF64::new_parameter_before("c")) + .metric(1.0.into()); + builder.parameters().f64(Box::new(b)); + builder + .parameters() + .f64(Box::new(ConstantParameterBuilder::new("c".into(), 5.0))); + builder + } + + #[test] + fn network_build_resolves_reverse_order_parameter_chain() { + let domain = default_domain(); + let (_, maps) = reverse_parameter_chain_network() + .build(&domain, &HashMap::new()) + .unwrap(); + assert!(matches!( + maps.parameters_f64.get(&"a".into()), + Some(ParameterIndex::General(_)) + )); + assert!(matches!( + maps.parameters_f64.get(&"b".into()), + Some(ParameterIndex::Const(_)) + )); + assert!(matches!( + maps.parameters_f64.get(&"c".into()), + Some(ParameterIndex::Const(_)) + )); + + let model = ModelBuilder::new(domain, reverse_parameter_chain_network()) + .build() + .unwrap(); + let mut state = model.setup::(&ClpSolverSettings::default()).unwrap(); + let mut timings = NetworkTimings::new_without_component_timings(); + model.step(&mut state, None, &mut timings).unwrap(); + + let scenario = &model.domain().scenarios().indices()[0]; + let output = model.network().get_node_by_name("output", None).unwrap(); + let flow = state + .network_state() + .state(scenario) + .get_network_state() + .get_node_in_flow(&output.index()) + .unwrap(); + assert_approx_eq!(f64, flow, 6.0); + } + + #[test] + fn network_lifecycle_runs_parameter_classes_in_expected_phases() { + let events = Arc::new(Mutex::new(Vec::new())); + let domain = default_domain(); + let mut builder = NetworkBuilder::default(); + builder + .parameters() + .f64(Box::new(ConstantParameterBuilder::new("lifecycle-const".into(), 10.0))) + .f64(Box::new(Array1ParameterBuilder::new( + "lifecycle-simple".into(), + Array::from_elem(domain.time().timesteps().len(), 20.0), + ))) + .f64(Box::new(TestParameterBuilder::network_lifecycle( + "lifecycle-general", + events.clone(), + ))); + let (network, maps) = builder.build(&domain, &HashMap::new()).unwrap(); + let const_index = match maps.parameters_f64[&"lifecycle-const".into()] { + ParameterIndex::Const(index) => index, + _ => panic!("expected a constant parameter"), + }; + let simple_index = match maps.parameters_f64[&"lifecycle-simple".into()] { + ParameterIndex::Simple(index) => index, + _ => panic!("expected a simple parameter"), + }; + let general = match maps.parameters_f64[&"lifecycle-general".into()] { + ParameterIndex::General(registration) => registration, + _ => panic!("expected a general parameter"), + }; + + let scenarios = domain.scenarios().indices(); + let scenario = &scenarios[0]; + let timesteps = domain.time().timesteps(); + let mut network_state = network.setup_network(timesteps, scenarios, 0).unwrap(); + assert_eq!(events.lock().unwrap().as_slice(), ["lifecycle-general:setup"]); + assert_eq!( + network_state + .state(scenario) + .get_const_parameter_values() + .get_f64(const_index) + .unwrap(), + 10.0 + ); + assert_eq!( + network_state + .state(scenario) + .get_simple_parameter_values() + .get_f64(simple_index) + .unwrap(), + 0.0 + ); + + let NetworkState { + states, + parameter_internal_states, + metric_set_internal_states, + } = &mut network_state; + network + .compute_components( + ×teps[0], + scenario, + &mut states[0], + &mut parameter_internal_states[0], + None, + ) + .unwrap(); + assert_eq!( + events.lock().unwrap().as_slice(), + ["lifecycle-general:setup", "lifecycle-general:before"] + ); + assert_eq!( + states[0].get_simple_parameter_values().get_f64(simple_index).unwrap(), + 20.0 + ); + assert_eq!( + states[0] + .get_general_parameter_f64_before(general.before.unwrap()) + .unwrap(), + 30.0 + ); + + network + .after( + ×teps[0], + scenario, + &mut states[0], + &mut parameter_internal_states[0], + &mut metric_set_internal_states[0], + None, + ) + .unwrap(); + assert_eq!( + events.lock().unwrap().as_slice(), + [ + "lifecycle-general:setup", + "lifecycle-general:before", + "lifecycle-general:after" + ] + ); + assert_eq!( + states[0] + .get_general_parameter_f64_after(general.after.unwrap()) + .unwrap(), + 40.0 + ); + } + + #[test] + fn parameter_internal_state_is_isolated_per_scenario() { + let scenario_group = ScenarioGroupBuilder::new("scenario", 2).build().unwrap(); + let scenarios = ScenarioDomainBuilder::default().with_group(scenario_group).unwrap(); + let mut domain_builder = default_domain_builder(); + domain_builder.scenario(scenarios); + let domain = domain_builder.build().unwrap(); + + let mut builder = NetworkBuilder::default(); + builder + .parameters() + .f64(Box::new(TestParameterBuilder::scenario_counter("scenario-counter"))); + let (network, maps) = builder.build(&domain, &HashMap::new()).unwrap(); + let registration = match maps.parameters_f64[&"scenario-counter".into()] { + ParameterIndex::General(registration) => registration, + _ => panic!("expected a general parameter"), + }; + let scenarios = domain.scenarios().indices(); + let timesteps = domain.time().timesteps(); + let mut network_state = network.setup_network(timesteps, scenarios, 0).unwrap(); + let NetworkState { + states, + parameter_internal_states, + .. + } = &mut network_state; + + for (timestep_number, timestep) in timesteps.iter().take(2).enumerate() { + for scenario in scenarios { + let scenario_id = scenario.simulation_id(); + network + .compute_components( + timestep, + scenario, + &mut states[scenario_id], + &mut parameter_internal_states[scenario_id], + None, + ) + .unwrap(); + assert_eq!( + states[scenario_id] + .get_general_parameter_f64_before(registration.before.unwrap()) + .unwrap(), + scenario_id as f64 * 100.0 + timestep_number as f64 + 1.0 + ); + } + } + + for scenario in scenarios { + let scenario_id = scenario.simulation_id(); + let state = parameter_internal_states[scenario_id] + .get_general_f64_state(registration.parameter) + .unwrap(); + let state = test_parameter_state(state).expect("expected test parameter state"); + assert_eq!(state.scenario_id(), scenario_id); + assert_eq!(state.calls(), 2); + } + } + + #[test] + fn general_after_parameter_reads_solved_network_state() { + let domain = default_domain(); + let mut builder = NetworkBuilder::default(); + let mut input = NodeBuilder::input("input"); + input.min_flow(7.0.into()).max_flow(7.0.into()); + builder.node(input).node(NodeBuilder::output("output")); + builder.connect("input", "output"); + let mut solved_flow = AggregatedParameterBuilder::after("solved-flow".into(), AggFuncF64::Sum); + solved_flow.metric(UnresolvedMetricF64::NodeOutFlow("input".into())); + builder.parameters().f64(Box::new(solved_flow)); + + let model = ModelBuilder::new(domain, builder).build().unwrap(); + let registration = match model + .network() + .get_parameter_index_by_name(&ParameterName::from("solved-flow")) + .unwrap() + { + ParameterIndex::General(registration) => registration, + _ => panic!("expected a general parameter"), + }; + let mut state = model.setup::(&ClpSolverSettings::default()).unwrap(); + let mut timings = NetworkTimings::new_without_component_timings(); + model.step(&mut state, None, &mut timings).unwrap(); + + let scenario = &model.domain().scenarios().indices()[0]; + let input = model.network().get_node_by_name("input", None).unwrap(); + let scenario_state = state.network_state().state(scenario); + let solved_node_flow = scenario_state + .get_network_state() + .get_node_out_flow(&input.index()) + .unwrap(); + let parameter_value = scenario_state + .get_general_parameter_f64_after(registration.after.unwrap()) + .unwrap(); + assert_approx_eq!(f64, solved_node_flow, 7.0); + assert_approx_eq!(f64, parameter_value, solved_node_flow); + } + #[test] fn test_step() { const NUM_SCENARIOS: usize = 2; @@ -2605,7 +2878,7 @@ mod tests { // 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 mut cc = ControlCurveInterpolatedParameterBuilder::new( + let mut cc = ControlCurveInterpolatedParameterBuilder::before( "interp".into(), UnresolvedMetricF64::NodeProportionalVolume("reservoir".into()), ); diff --git a/pywr-core/src/node.rs b/pywr-core/src/node.rs index 7d3001d1..55a72b9d 100644 --- a/pywr-core/src/node.rs +++ b/pywr-core/src/node.rs @@ -1,6 +1,6 @@ use crate::metric::{ - ConstantMetricF64Error, MetricF64, MetricF64Error, MetricF64ResolutionError, SimpleMetricF64, SimpleMetricF64Error, - UnresolvedMetricF64, + ConstantMetricF64Error, MetricConsumerPhase, MetricF64, MetricF64Error, MetricF64ResolutionError, SimpleMetricF64, + SimpleMetricF64Error, UnresolvedMetricF64, }; use crate::network::{EdgeIndex, Network, NodeIndex, ResolutionMaps, VirtualStorageIndex}; use crate::state::{ConstParameterValues, NetworkStateError, NodeState, SimpleParameterValues, State, StateError}; @@ -164,7 +164,7 @@ impl NodeBuilder { let local = match &self.cost { Some(cost) => cost - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "cost".to_string(), source, @@ -194,7 +194,7 @@ impl NodeBuilder { .as_ref() .map(|min_flow| { min_flow - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "min_flow".to_string(), source, @@ -207,7 +207,7 @@ impl NodeBuilder { .as_ref() .map(|max_flow| { max_flow - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "max_flow".to_string(), source, @@ -230,7 +230,7 @@ impl NodeBuilder { .as_ref() .map(|min_volume| { min_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "min_volume".to_string(), source, @@ -248,7 +248,7 @@ impl NodeBuilder { .as_ref() .map(|max_volume| { max_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "max_volume".to_string(), source, @@ -279,7 +279,7 @@ impl NodeBuilder { prior_max_volume, } => { let prior_max_volume = prior_max_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "prior_max_volume".to_string(), source, @@ -300,7 +300,7 @@ impl NodeBuilder { prior_max_volume, } => { let total_volume = total_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "total_volume".to_string(), source, @@ -311,7 +311,7 @@ impl NodeBuilder { source, })?; let prior_max_volume = prior_max_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| NodeBuilderError::ResolveMetricF64Error { attr: "prior_max_volume".to_string(), source, diff --git a/pywr-core/src/parameters/aggregated.rs b/pywr-core/src/parameters/aggregated.rs index dad49890..218b963b 100644 --- a/pywr-core/src/parameters/aggregated.rs +++ b/pywr-core/src/parameters/aggregated.rs @@ -5,8 +5,8 @@ use super::{ }; use crate::agg_funcs::AggFuncF64; use crate::metric::{ - ConstantMetricF64, MetricF64, SimpleMetricF64, UnresolvedMetricF64, try_into_constant_metrics_f64, - try_into_simple_metrics_f64, + ConstantMetricF64, MetricConsumerPhase, MetricF64, SimpleMetricF64, UnresolvedMetricF64, + try_into_constant_metrics_f64, try_into_simple_metrics_f64, }; use crate::network::ResolutionMaps; use crate::parameters::errors::{ConstCalculationError, GeneralCalculationError, SimpleCalculationError}; @@ -119,19 +119,43 @@ impl ConstParameter for AggregatedParameter { } } +/// Builder for creating an [`AggregatedParameter`] #[derive(Debug)] pub struct AggregatedParameterBuilder { meta: ParameterMeta, agg_func: AggFuncF64, metrics: Vec, + phase: MetricConsumerPhase, } impl AggregatedParameterBuilder { - pub fn new(name: ParameterName, agg_func: AggFuncF64) -> Self { + /// Create a new builder for [`AggregatedParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, agg_func: AggFuncF64) -> Self { Self { meta: ParameterMeta::new(name), metrics: Vec::new(), agg_func, + phase: MetricConsumerPhase::Before, + } + } + + /// Create a new builder for [`AggregatedParameter`] that is evaluated in the "after" phase. + pub fn after(name: ParameterName, agg_func: AggFuncF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metrics: Vec::new(), + agg_func, + phase: MetricConsumerPhase::After, + } + } + + /// Create a new builder for [`AggregatedParameter`] that is evaluated in both "before" and "after" phases. + pub fn both(name: ParameterName, agg_func: AggFuncF64) -> Self { + Self { + meta: ParameterMeta::new(name), + metrics: Vec::new(), + agg_func, + phase: MetricConsumerPhase::Both, } } @@ -150,39 +174,45 @@ impl ParameterBuilder for AggregatedParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metrics = resolve_metric_f64_vec!(self, &self.metrics, resolution_maps, "metrics"); + let metrics = resolve_metric_f64_vec!(self, &self.metrics, resolution_maps, self.phase, "metrics"); let meta = self.meta; let agg_func = self.agg_func; - // Try the narrowest dependency class first. - if let Some(metrics) = try_into_constant_metrics_f64(&metrics) { - return Ok( - BuiltParameter::Const(Box::new(AggregatedParameter:: { - meta, - metrics, - agg_func, - })) - .into(), - ); - } - - if let Some(metrics) = try_into_simple_metrics_f64(&metrics) { - return Ok(BuiltParameter::Simple(Box::new(AggregatedParameter:: { + let built = match self.phase { + MetricConsumerPhase::Before => { + if let Some(metrics) = try_into_constant_metrics_f64(&metrics) { + BuiltParameter::Const(Box::new(AggregatedParameter { + meta, + metrics, + agg_func, + })) + } else if let Some(metrics) = try_into_simple_metrics_f64(&metrics) { + BuiltParameter::Simple(Box::new(AggregatedParameter { + meta, + metrics, + agg_func, + })) + } else { + BuiltParameter::General(GeneralParameterEntry::before(AggregatedParameter { + meta, + metrics, + agg_func, + })) + } + } + MetricConsumerPhase::After => BuiltParameter::General(GeneralParameterEntry::after(AggregatedParameter { meta, metrics, agg_func, - })) - .into()); - } - - Ok( - BuiltParameter::General(GeneralParameterEntry::both(AggregatedParameter:: { + })), + MetricConsumerPhase::Both => BuiltParameter::General(GeneralParameterEntry::both(AggregatedParameter { meta, metrics, agg_func, - })) - .into(), - ) + })), + }; + + Ok(built.into()) } } diff --git a/pywr-core/src/parameters/aggregated_index.rs b/pywr-core/src/parameters/aggregated_index.rs index 84f57ff9..d00237a1 100644 --- a/pywr-core/src/parameters/aggregated_index.rs +++ b/pywr-core/src/parameters/aggregated_index.rs @@ -7,8 +7,8 @@ use super::{ }; use crate::agg_funcs::AggFuncU64; use crate::metric::{ - ConstantMetricU64, MetricU64, SimpleMetricU64, UnresolvedMetricU64, try_into_constant_metrics_u64, - try_into_simple_metrics_u64, + ConstantMetricU64, MetricConsumerPhase, MetricU64, SimpleMetricU64, UnresolvedMetricU64, + try_into_constant_metrics_u64, try_into_simple_metrics_u64, }; use crate::network::ResolutionMaps; use crate::parameters::errors::{ConstCalculationError, GeneralCalculationError, SimpleCalculationError}; @@ -126,14 +126,36 @@ pub struct AggregatedIndexParameterBuilder { meta: ParameterMeta, metrics: Vec, agg_func: AggFuncU64, + phase: MetricConsumerPhase, } impl AggregatedIndexParameterBuilder { - pub fn new(name: ParameterName, agg_func: AggFuncU64) -> Self { + /// Create a new builder for [`AggregatedIndexParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, agg_func: AggFuncU64) -> Self { Self { meta: ParameterMeta::new(name), metrics: Vec::new(), agg_func, + phase: MetricConsumerPhase::Before, + } + } + /// Create a new builder for [`AggregatedIndexParameter`] that is evaluated in the "after" phase. + pub fn after(name: ParameterName, agg_func: AggFuncU64) -> Self { + Self { + meta: ParameterMeta::new(name), + metrics: Vec::new(), + agg_func, + phase: MetricConsumerPhase::After, + } + } + + /// Create a new builder for [`AggregatedIndexParameter`] that is evaluated in both "before" and "after" phases. + pub fn both(name: ParameterName, agg_func: AggFuncU64) -> Self { + Self { + meta: ParameterMeta::new(name), + metrics: Vec::new(), + agg_func, + phase: MetricConsumerPhase::Both, } } @@ -152,41 +174,49 @@ impl ParameterBuilder for AggregatedIndexParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metrics = resolve_metric_u64_vec!(self, &self.metrics, resolution_maps, "metrics"); + let metrics = resolve_metric_u64_vec!(self, &self.metrics, resolution_maps, self.phase, "metrics"); let meta = self.meta; let agg_func = self.agg_func; - // Try the narrowest dependency class first. - if let Some(metrics) = try_into_constant_metrics_u64(&metrics) { - return Ok( - BuiltParameter::Const(Box::new(AggregatedIndexParameter:: { + let built = match self.phase { + MetricConsumerPhase::Before => { + if let Some(metrics) = try_into_constant_metrics_u64(&metrics) { + BuiltParameter::Const(Box::new(AggregatedIndexParameter:: { + meta, + metrics, + agg_func, + })) + } else if let Some(metrics) = try_into_simple_metrics_u64(&metrics) { + BuiltParameter::Simple(Box::new(AggregatedIndexParameter:: { + meta, + metrics, + agg_func, + })) + } else { + BuiltParameter::General(GeneralParameterEntry::both(AggregatedIndexParameter:: { + meta, + metrics, + agg_func, + })) + } + } + MetricConsumerPhase::After => { + BuiltParameter::General(GeneralParameterEntry::after(AggregatedIndexParameter:: { meta, metrics, agg_func, })) - .into(), - ); - } - - if let Some(metrics) = try_into_simple_metrics_u64(&metrics) { - return Ok( - BuiltParameter::Simple(Box::new(AggregatedIndexParameter:: { + } + MetricConsumerPhase::Both => { + BuiltParameter::General(GeneralParameterEntry::both(AggregatedIndexParameter:: { meta, metrics, agg_func, })) - .into(), - ); - } + } + }; - Ok( - BuiltParameter::General(GeneralParameterEntry::both(AggregatedIndexParameter:: { - meta, - metrics, - agg_func, - })) - .into(), - ) + Ok(built.into()) } } diff --git a/pywr-core/src/parameters/asymmetric.rs b/pywr-core/src/parameters/asymmetric.rs index 5311f22a..6dd70521 100644 --- a/pywr-core/src/parameters/asymmetric.rs +++ b/pywr-core/src/parameters/asymmetric.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricU64, UnresolvedMetricU64}; +use crate::metric::{MetricConsumerPhase, MetricU64, UnresolvedMetricU64}; use crate::network::ResolutionMaps; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ @@ -71,14 +71,17 @@ pub struct AsymmetricSwitchIndexParameterBuilder { meta: ParameterMeta, on_parameter: UnresolvedMetricU64, off_parameter: UnresolvedMetricU64, + phase: MetricConsumerPhase, } impl AsymmetricSwitchIndexParameterBuilder { - pub fn new(name: ParameterName, on_parameter: UnresolvedMetricU64, off_parameter: UnresolvedMetricU64) -> Self { + /// Create a new builder for [`AsymmetricSwitchIndexParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, on_parameter: UnresolvedMetricU64, off_parameter: UnresolvedMetricU64) -> Self { Self { meta: ParameterMeta::new(name), on_parameter, off_parameter, + phase: MetricConsumerPhase::Before, } } } @@ -92,8 +95,8 @@ impl ParameterBuilder for AsymmetricSwitchIndexParameterBuilder { 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 on_parameter = resolve_metric_u64!(self, self.on_parameter, resolution_maps, self.phase, "on_parameter"); + let off_parameter = resolve_metric_u64!(self, self.off_parameter, resolution_maps, self.phase, "off_parameter"); let p = AsymmetricSwitchIndexParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/control_curves/apportion.rs b/pywr-core/src/parameters/control_curves/apportion.rs index 161ac38e..f029ca42 100644 --- a/pywr-core/src/parameters/control_curves/apportion.rs +++ b/pywr-core/src/parameters/control_curves/apportion.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -70,7 +70,8 @@ pub struct ApportionParameterBuilder { } impl ApportionParameterBuilder { - pub fn new(name: ParameterName, metric: UnresolvedMetricF64, control_curve: UnresolvedMetricF64) -> Self { + /// Create a new builder for [`ApportionParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, metric: UnresolvedMetricF64, control_curve: UnresolvedMetricF64) -> Self { Self { meta: ParameterMeta::new(name), metric, @@ -88,8 +89,10 @@ impl ParameterBuilder for ApportionParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); + let control_curve = resolve_metric_f64!(self, self.control_curve, resolution_maps, phase, "control_curve"); let p = ApportionParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/control_curves/index.rs b/pywr-core/src/parameters/control_curves/index.rs index 46954098..f9a05494 100644 --- a/pywr-core/src/parameters/control_curves/index.rs +++ b/pywr-core/src/parameters/control_curves/index.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -57,7 +57,8 @@ pub struct ControlCurveIndexParameterBuilder { } impl ControlCurveIndexParameterBuilder { - pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + /// Create a new builder for [`ControlCurveIndexParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, metric: UnresolvedMetricF64) -> Self { Self { meta: ParameterMeta::new(name), metric, @@ -80,8 +81,11 @@ impl ParameterBuilder for ControlCurveIndexParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); + let control_curves = + resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, phase, "control_curves"); let p = ControlCurveIndexParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/control_curves/interpolated.rs b/pywr-core/src/parameters/control_curves/interpolated.rs index b8e5e09a..f3ee2086 100644 --- a/pywr-core/src/parameters/control_curves/interpolated.rs +++ b/pywr-core/src/parameters/control_curves/interpolated.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::interpolate; @@ -74,7 +74,8 @@ pub struct ControlCurveInterpolatedParameterBuilder { } impl ControlCurveInterpolatedParameterBuilder { - pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + /// Create a new builder for [`ControlCurveInterpolatedParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, metric: UnresolvedMetricF64) -> Self { Self { meta: ParameterMeta::new(name), metric, @@ -103,9 +104,12 @@ impl ParameterBuilder for ControlCurveInterpolatedParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); + let control_curves = + resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, phase, "control_curves"); + let values = resolve_metric_f64_vec!(self, &self.values, resolution_maps, phase, "values"); let p = ControlCurveInterpolatedParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/control_curves/piecewise.rs b/pywr-core/src/parameters/control_curves/piecewise.rs index 962146c2..b27f9dbc 100644 --- a/pywr-core/src/parameters/control_curves/piecewise.rs +++ b/pywr-core/src/parameters/control_curves/piecewise.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::interpolate; @@ -81,7 +81,8 @@ pub struct PiecewiseInterpolatedParameterBuilder { } impl PiecewiseInterpolatedParameterBuilder { - pub fn new(name: ParameterName, metric: UnresolvedMetricF64, maximum: f64, minimum: f64) -> Self { + /// Create a new builder for [`PiecewiseInterpolatedParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, metric: UnresolvedMetricF64, maximum: f64, minimum: f64) -> Self { Self { meta: ParameterMeta::new(name), metric, @@ -115,8 +116,11 @@ impl ParameterBuilder for PiecewiseInterpolatedParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); + let control_curves = + resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, phase, "control_curves"); let p = PiecewiseInterpolatedParameter { meta: self.meta, @@ -149,7 +153,7 @@ mod test { let volume = Array1ParameterBuilder::new("test-x".into(), Array1::linspace(1.0, 0.0, 21)); model_builder.network_builder().parameters().f64(Box::new(volume)); - let mut parameter = PiecewiseInterpolatedParameterBuilder::new( + let mut parameter = PiecewiseInterpolatedParameterBuilder::before( "test-parameter".into(), UnresolvedMetricF64::new_parameter_before("test-x"), // Interpolate with the parameter based values 1.0, diff --git a/pywr-core/src/parameters/control_curves/simple.rs b/pywr-core/src/parameters/control_curves/simple.rs index c0193e07..85dba589 100644 --- a/pywr-core/src/parameters/control_curves/simple.rs +++ b/pywr-core/src/parameters/control_curves/simple.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -77,7 +77,8 @@ pub struct ControlCurveParameterBuilder { } impl ControlCurveParameterBuilder { - pub fn new(name: ParameterName, metric: UnresolvedMetricF64) -> Self { + /// Create a new builder for [`ControlCurveParameter`] that is evaluated in the "before" phase. + pub fn before(name: ParameterName, metric: UnresolvedMetricF64) -> Self { Self { meta: ParameterMeta::new(name), metric, @@ -106,11 +107,14 @@ impl ParameterBuilder for ControlCurveParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); - let control_curves = resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); + let control_curves = + resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, phase, "control_curves"); - let values = resolve_metric_f64_vec!(self, &self.values, resolution_maps, "values"); + let values = resolve_metric_f64_vec!(self, &self.values, resolution_maps, phase, "values"); let p = ControlCurveParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/control_curves/volume_between.rs b/pywr-core/src/parameters/control_curves/volume_between.rs index 4175a133..944df69a 100644 --- a/pywr-core/src/parameters/control_curves/volume_between.rs +++ b/pywr-core/src/parameters/control_curves/volume_between.rs @@ -1,4 +1,4 @@ -use crate::metric::{SimpleMetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, SimpleMetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::SimpleCalculationError; use crate::parameters::{ @@ -91,7 +91,9 @@ impl ParameterBuilder for VolumeBetweenControlCurvesParameterBuilder, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let total = resolve_metric_f64!(self, self.total, resolution_maps, "total"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let total = resolve_metric_f64!(self, self.total, resolution_maps, phase, "total"); let total: SimpleMetricF64 = total .try_into() @@ -102,7 +104,7 @@ impl ParameterBuilder for VolumeBetweenControlCurvesParameterBuilder = match &self.upper { Some(upper) => { - let upper = resolve_metric_f64!(self, upper, resolution_maps, "upper"); + let upper = resolve_metric_f64!(self, upper, resolution_maps, phase, "upper"); let upper: SimpleMetricF64 = upper .try_into() @@ -118,7 +120,7 @@ impl ParameterBuilder for VolumeBetweenControlCurvesParameterBuilder = match &self.lower { Some(lower) => { - let lower = resolve_metric_f64!(self, lower, resolution_maps, "lower"); + let lower = resolve_metric_f64!(self, lower, resolution_maps, phase, "lower"); let lower = lower .try_into() .map_err(|source| ParameterBuildError::CouldNotSimplifyMetricF64 { diff --git a/pywr-core/src/parameters/deficit.rs b/pywr-core/src/parameters/deficit.rs index f8968c2f..8a775247 100644 --- a/pywr-core/src/parameters/deficit.rs +++ b/pywr-core/src/parameters/deficit.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::{ BuiltParameter, GeneralAfterParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, @@ -73,8 +73,10 @@ impl ParameterBuilder for DeficitParameterBuilder { 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"); + // Phase is hardcoded to "after" for this parameter, as it only implements the `GeneralAfterParameter` trait. + let phase = MetricConsumerPhase::After; + let flow = resolve_metric_f64!(self, self.flow, resolution_maps, phase, "flow"); + let max_flow = resolve_metric_f64!(self, self.max_flow, resolution_maps, phase, "max_flow"); let p = DeficitParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/delay.rs b/pywr-core/src/parameters/delay.rs index fe9435a1..8bc9bca5 100644 --- a/pywr-core/src/parameters/delay.rs +++ b/pywr-core/src/parameters/delay.rs @@ -1,6 +1,6 @@ use crate::metric::{ - MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64, UnresolvedMetricF64, - UnresolvedMetricU64, + MetricConsumerPhase, MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64, + UnresolvedMetricF64, UnresolvedMetricU64, }; use crate::network::ResolutionMaps; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError, SimpleCalculationError}; @@ -253,7 +253,10 @@ impl ParameterBuilder for DelayParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "after" for this parameter, as it only uses the metric + // values in the after phase to update internal state. + let phase = MetricConsumerPhase::After; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let simple_metric: Result = metric.clone().try_into(); if let Ok(simple_metric) = simple_metric { @@ -289,7 +292,10 @@ impl ParameterBuilder for DelayParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_u64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "both" for this parameter, as it only implements the + // `GeneralBeforeParameter` and `GeneralAfterParameterHook` traits. + let phase = MetricConsumerPhase::Both; + let metric = resolve_metric_u64!(self, self.metric, resolution_maps, phase, "metric"); let simple_metric: Result = metric.clone().try_into(); if let Ok(simple_metric) = simple_metric { diff --git a/pywr-core/src/parameters/difference.rs b/pywr-core/src/parameters/difference.rs index 27dc9b4b..643af22d 100644 --- a/pywr-core/src/parameters/difference.rs +++ b/pywr-core/src/parameters/difference.rs @@ -2,7 +2,7 @@ use super::{ BuiltParameter, GeneralBeforeParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterName, SimpleParameter, SimpleParameterContext, }; -use crate::metric::{MetricF64, SimpleMetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, SimpleMetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::{GeneralCalculationError, SimpleCalculationError}; use crate::parameters::{GeneralParameter, ParameterMeta, ParameterState}; @@ -142,14 +142,16 @@ impl ParameterBuilder for DifferenceParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let a = resolve_metric_f64!(self, self.a, resolution_maps, phase, "a"); + let b = resolve_metric_f64!(self, self.b, resolution_maps, phase, "b"); let min = match &self.min { - Some(min) => Some(resolve_metric_f64!(self, min, resolution_maps, "min")), + Some(min) => Some(resolve_metric_f64!(self, min, resolution_maps, phase, "min")), None => None, }; let max = match &self.max { - Some(max) => Some(resolve_metric_f64!(self, max, resolution_maps, "max")), + Some(max) => Some(resolve_metric_f64!(self, max, resolution_maps, phase, "max")), None => None, }; diff --git a/pywr-core/src/parameters/discount_factor.rs b/pywr-core/src/parameters/discount_factor.rs index fc682846..d0fd0ae8 100644 --- a/pywr-core/src/parameters/discount_factor.rs +++ b/pywr-core/src/parameters/discount_factor.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -69,7 +69,9 @@ impl ParameterBuilder for DiscountFactorParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let discount_rate = resolve_metric_f64!(self, self.discount_rate, resolution_maps, "discount_rate"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let discount_rate = resolve_metric_f64!(self, self.discount_rate, resolution_maps, phase, "discount_rate"); let p = DiscountFactorParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/division.rs b/pywr-core/src/parameters/division.rs index 74cca285..3a8b40f3 100644 --- a/pywr-core/src/parameters/division.rs +++ b/pywr-core/src/parameters/division.rs @@ -2,7 +2,7 @@ use super::{ BuiltParameter, GeneralBeforeParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterName, }; -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{GeneralParameter, ParameterMeta, ParameterState}; @@ -72,8 +72,10 @@ impl ParameterBuilder for DivisionParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let numerator = resolve_metric_f64!(self, self.numerator, resolution_maps, phase, "numerator"); + let denominator = resolve_metric_f64!(self, self.denominator, resolution_maps, phase, "denominator"); let p = DivisionParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/errors.rs b/pywr-core/src/parameters/errors.rs index ef9e2661..85dca642 100644 --- a/pywr-core/src/parameters/errors.rs +++ b/pywr-core/src/parameters/errors.rs @@ -17,6 +17,9 @@ pub enum ParameterSetupError { #[source] py_error: Box, }, + #[cfg(test)] + #[error("Test error: {0}")] + TestError(String), } /// Errors returned by parameter calculations. diff --git a/pywr-core/src/parameters/hydropower.rs b/pywr-core/src/parameters/hydropower.rs index 5ad8b065..a0dc4bac 100644 --- a/pywr-core/src/parameters/hydropower.rs +++ b/pywr-core/src/parameters/hydropower.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -191,12 +191,55 @@ impl ParameterBuilder for HydropowerTargetParameterBuilder { 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"); + // Determine which calculation phase(s) the parameter will be used in + // based on whether the target and actual flow are provided. + // If neither is provided, return an error. + let phase = match (self.target.is_some(), self.actual_flow.is_some()) { + (true, true) => MetricConsumerPhase::Both, + (true, false) => MetricConsumerPhase::Before, + (false, true) => MetricConsumerPhase::After, + (false, false) => { + return Err(ParameterBuildError::NoCalculationPhase { + detail: "HydropowerTargetParameter must have at least one of `target` or `actual_flow` defined." + .into(), + }); + } + }; + + // Actual flow and target are used to determine the phases calculated. + let actual_flow = resolve_optional_metric_f64!(self, &self.actual_flow, resolution_maps, phase, "actual_flow"); + let target = resolve_optional_metric_f64!(self, &self.target, resolution_maps, phase, "target"); + // Water elevation is used in both phases; so can be safely resolved if provided. let water_elevation = - resolve_optional_metric_f64!(self, &self.water_elevation, resolution_maps, "water_elevation"); + resolve_optional_metric_f64!(self, &self.water_elevation, resolution_maps, phase, "water_elevation"); + // min and max flow have a chance of being unused if they are supplied but "before" is not computed. + let (min_flow, max_flow) = match phase { + MetricConsumerPhase::Before => { + if self.min_flow.is_some() { + return Err(ParameterBuildError::UnusedMetric { + attr: "min_flow".to_string(), + message: "HydropowerParameter without a `target` does not require a `min_flow` metric." + .to_string(), + }); + } + + if self.max_flow.is_some() { + return Err(ParameterBuildError::UnusedMetric { + attr: "max_flow".to_string(), + message: "HydropowerParameter without a `target` does not require a `max_flow` metric." + .to_string(), + }); + } + + (None, None) + } + MetricConsumerPhase::After | MetricConsumerPhase::Both => { + let max_flow = resolve_optional_metric_f64!(self, &self.max_flow, resolution_maps, phase, "max_flow"); + let min_flow = resolve_optional_metric_f64!(self, &self.min_flow, resolution_maps, phase, "min_flow"); + + (min_flow, max_flow) + } + }; let p = HydropowerTargetParameter { meta: self.meta, @@ -213,19 +256,10 @@ impl ParameterBuilder for HydropowerTargetParameterBuilder { energy_unit_conversion: self.energy_unit_conversion, }; - // Determine which calculation phase(s) the parameter will be used in - // based on whether the target and actual flow are provided. - // If neither is provided, return an error. - let entry = match (p.target.is_some(), p.actual_flow.is_some()) { - (true, true) => GeneralParameterEntry::both(p), - (true, false) => GeneralParameterEntry::before(p), - (false, true) => GeneralParameterEntry::after(p), - (false, false) => { - return Err(ParameterBuildError::NoCalculationPhase { - detail: "HydropowerTargetParameter must have at least one of `target` or `actual_flow` defined." - .into(), - }); - } + let entry = match phase { + MetricConsumerPhase::Both => GeneralParameterEntry::both(p), + MetricConsumerPhase::Before => GeneralParameterEntry::before(p), + MetricConsumerPhase::After => GeneralParameterEntry::after(p), }; Ok(BuiltParameter::General(entry).into()) diff --git a/pywr-core/src/parameters/indexed_array.rs b/pywr-core/src/parameters/indexed_array.rs index bb0cc8e0..af3363ec 100644 --- a/pywr-core/src/parameters/indexed_array.rs +++ b/pywr-core/src/parameters/indexed_array.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, MetricU64, UnresolvedMetricF64, UnresolvedMetricU64}; +use crate::metric::{MetricConsumerPhase, MetricF64, MetricU64, UnresolvedMetricF64, UnresolvedMetricU64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -82,8 +82,11 @@ impl ParameterBuilder for IndexedArrayParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let index_parameter = + resolve_metric_u64!(self, self.index_parameter, resolution_maps, phase, "index_parameter"); + let metrics = resolve_metric_f64_vec!(self, &self.metrics, resolution_maps, phase, "metrics"); let p = IndexedArrayParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/interpolated.rs b/pywr-core/src/parameters/interpolated.rs index 3d335ef4..03dadfd3 100644 --- a/pywr-core/src/parameters/interpolated.rs +++ b/pywr-core/src/parameters/interpolated.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::interpolate::linear_interpolation; @@ -95,12 +95,14 @@ impl ParameterBuilder for InterpolatedParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let x = resolve_metric_f64!(self, self.x, resolution_maps, "x"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let x = resolve_metric_f64!(self, self.x, resolution_maps, phase, "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")); + let xp = resolve_metric_f64!(self, uxp, resolution_maps, phase, &format!("points[{i}].x")); + let fp = resolve_metric_f64!(self, ufp, resolution_maps, phase, &format!("points[{i}].f")); points.push((xp, fp)); } diff --git a/pywr-core/src/parameters/max.rs b/pywr-core/src/parameters/max.rs index a38679ce..73c58f7a 100644 --- a/pywr-core/src/parameters/max.rs +++ b/pywr-core/src/parameters/max.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -68,7 +68,9 @@ impl ParameterBuilder for MaxParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = MaxParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/min.rs b/pywr-core/src/parameters/min.rs index bd341ef8..3c8bd39f 100644 --- a/pywr-core/src/parameters/min.rs +++ b/pywr-core/src/parameters/min.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -66,7 +66,9 @@ impl ParameterBuilder for MinParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = MinParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 0ed5008b..6cabfab4 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -29,20 +29,18 @@ mod profiles; #[cfg(feature = "pyo3")] mod py; mod rolling; +#[cfg(test)] +pub(crate) mod test_utils; mod threshold; mod vector; use std::any::Any; +use std::collections::HashSet; // Re-imports -use crate::metric::{ - ConstantMetricF64, ConstantMetricU64, MetricF64, MetricF64Error, MetricF64ResolutionError, MetricU64, - MetricU64ResolutionError, SimpleMetricF64, SimpleMetricU64, -}; +use crate::metric::{MetricF64Error, MetricF64ResolutionError, MetricU64ResolutionError}; use crate::network::{Network, ResolutionMaps}; use crate::scenario::{ScenarioGroupNotFound, ScenarioIndex}; -use crate::state::{ - ConstParameterValues, MultiValue, ParameterReturnValue, SetStateError, SimpleParameterValues, State, -}; +use crate::state::{ConstParameterValues, MultiValue, SetStateError, SimpleParameterValues, State}; use crate::timestep::Timestep; pub use activation_function::ActivationFunction; pub use aggregated::{AggregatedParameter, AggregatedParameterBuilder}; @@ -100,7 +98,7 @@ use thiserror::Error; pub use threshold::{Predicate, ThresholdParameter, ThresholdParameterBuilder}; pub use vector::{VectorParameter, VectorParameterBuilder}; -/// Simple parameter index. +/// Constant parameter index. /// /// This is a wrapper around usize that is used to index parameters in the state. It is /// generic over the type of the value that the parameter returns. @@ -202,7 +200,7 @@ impl Display for SimpleParameterIndex { /// Generic parameter index. /// -/// This is a wrapper around usize that is used to index parameters in the state. It is +/// This is a wrapper around usize that is used to index parameters in the collection. It is /// generic over the type of the value that the parameter returns. #[derive(Debug)] pub struct GeneralParameterIndex { @@ -257,166 +255,175 @@ impl Hash for GeneralParameterIndex { } } -#[derive(Debug, Copy, Clone)] -pub enum ParameterIndex { - Const(ConstParameterIndex), - Simple(SimpleParameterIndex), - General(GeneralParameterIndex), +/// An index for a general parameter's before value in the state. +#[derive(Debug)] +pub struct GeneralBeforeValueIndex { + idx: usize, + phantom: PhantomData, } -impl PartialEq for ParameterIndex { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Const(idx1), Self::Const(idx2)) => idx1 == idx2, - (Self::Simple(idx1), Self::Simple(idx2)) => idx1 == idx2, - (Self::General(idx1), Self::General(idx2)) => idx1 == idx2, - _ => false, +impl GeneralBeforeValueIndex { + fn new(idx: usize) -> Self { + Self { + idx, + phantom: PhantomData, } } } -impl Eq for ParameterIndex {} - -impl Display for ParameterIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Self::Const(idx) => write!(f, "{idx}",), - Self::Simple(idx) => write!(f, "{idx}",), - Self::General(idx) => write!(f, "{idx}",), - } +// These implementations are required because the derive macro does not work well with PhantomData. +// See issue: https://github.com/rust-lang/rust/issues/26925 +impl Clone for GeneralBeforeValueIndex { + fn clone(&self) -> Self { + *self } } -impl From> for ParameterIndex { - fn from(idx: GeneralParameterIndex) -> Self { - Self::General(idx) + +impl Copy for GeneralBeforeValueIndex {} + +impl Deref for GeneralBeforeValueIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.idx } } -impl From> for ParameterIndex { - fn from(idx: SimpleParameterIndex) -> Self { - Self::Simple(idx) +impl Display for GeneralBeforeValueIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.idx) } } -impl From> for ParameterIndex { - fn from(idx: ConstParameterIndex) -> Self { - Self::Const(idx) +impl PartialEq for GeneralBeforeValueIndex { + fn eq(&self, other: &Self) -> bool { + self.idx == other.idx } } -impl ParameterIndex { - /// Convert the parameter index into a metric. - pub fn into_metric_f64(self, return_value: ParameterReturnValue) -> MetricF64 { - match self { - ParameterIndex::Const(idx) => ConstantMetricF64::ParameterValue(idx).into(), - ParameterIndex::Simple(index) => SimpleMetricF64::ParameterValue { index, return_value }.into(), - ParameterIndex::General(index) => MetricF64::ParameterValue { index, return_value }, +impl Eq for GeneralBeforeValueIndex {} + +/// An index for a general parameter's after value in the state. +#[derive(Debug)] +pub struct GeneralAfterValueIndex { + idx: usize, + phantom: PhantomData, +} +impl GeneralAfterValueIndex { + fn new(idx: usize) -> Self { + Self { + idx, + phantom: PhantomData, } } +} - /// Convert the parameter index into a metric that returns the "before" value. - /// - /// This is a convenience method for `into_metric_f64(ParameterReturnValue::Before)`. - pub fn into_metric_f64_before(self) -> MetricF64 { - self.into_metric_f64(ParameterReturnValue::Before) +// These implementations are required because the derive macro does not work well with PhantomData. +// See issue: https://github.com/rust-lang/rust/issues/26925 +impl Clone for GeneralAfterValueIndex { + fn clone(&self) -> Self { + *self } +} - /// Convert the parameter index into a metric that returns the "after" value. - /// - /// This is a convenience method for `into_metric_f64(ParameterReturnValue::After)`. - pub fn into_metric_f64_after(self) -> MetricF64 { - self.into_metric_f64(ParameterReturnValue::After) +impl Copy for GeneralAfterValueIndex {} + +impl Deref for GeneralAfterValueIndex { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.idx } } -impl ParameterIndex { - /// Convert the parameter index into a metric. - pub fn into_metric_f64(self, return_value: ParameterReturnValue) -> MetricF64 { - match self { - ParameterIndex::Const(idx) => ConstantMetricF64::IndexParameterValue(idx).into(), - ParameterIndex::Simple(index) => SimpleMetricF64::IndexParameterValue { index, return_value }.into(), - ParameterIndex::General(index) => MetricF64::IndexParameterValue { index, return_value }, - } +impl Display for GeneralAfterValueIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.idx) } +} - /// Convert the parameter index into a metric that returns the "before" value. - /// - /// This is a convenience method for `into_metric(ParameterReturnValue::Before)`. - pub fn into_metric_f64_before(self) -> MetricF64 { - self.into_metric_f64(ParameterReturnValue::Before) +impl PartialEq for GeneralAfterValueIndex { + fn eq(&self, other: &Self) -> bool { + self.idx == other.idx } +} - /// Convert the parameter index into a metric. - pub fn into_metric_u64(self, return_value: ParameterReturnValue) -> MetricU64 { - match self { - ParameterIndex::Const(idx) => ConstantMetricU64::IndexParameterValue(idx).into(), - ParameterIndex::Simple(index) => SimpleMetricU64::IndexParameterValue { index, return_value }.into(), - ParameterIndex::General(index) => MetricU64::IndexParameterValue { index, return_value }, - } - } +impl Eq for GeneralAfterValueIndex {} +#[derive(Debug, PartialEq, Eq)] +pub struct GeneralParameterRegistration { + pub parameter: GeneralParameterIndex, + pub before: Option>, + pub after: Option>, +} - /// Convert the parameter index into a metric that returns the "before" value. - /// - /// This is a convenience method for `into_metric(ParameterReturnValue::Before)`. - pub fn into_metric_u64_before(self) -> MetricU64 { - self.into_metric_u64(ParameterReturnValue::Before) +// These implementations are required because the derive macro does not work well with PhantomData. +// See issue: https://github.com/rust-lang/rust/issues/26925 +impl Clone for GeneralParameterRegistration { + fn clone(&self) -> Self { + *self } } -impl ParameterIndex { - /// Convert the parameter index into a metric. - pub fn into_metric_f64(self, key: &str, return_value: ParameterReturnValue) -> MetricF64 { - let key = key.to_string(); - match self { - ParameterIndex::Const(index) => ConstantMetricF64::MultiParameterValue { index, key }.into(), - ParameterIndex::Simple(index) => SimpleMetricF64::MultiParameterValue { - index, - key, - return_value, - } - .into(), - ParameterIndex::General(index) => MetricF64::MultiParameterValue { - index, - key, - return_value, - }, +impl Copy for GeneralParameterRegistration {} + +#[derive(Debug, Copy, Clone)] +pub enum ParameterIndex { + Const(ConstParameterIndex), + Simple(SimpleParameterIndex), + General(GeneralParameterRegistration), +} + +impl PartialEq for ParameterIndex +where + T: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Const(idx1), Self::Const(idx2)) => idx1 == idx2, + (Self::Simple(idx1), Self::Simple(idx2)) => idx1 == idx2, + (Self::General(idx1), Self::General(idx2)) => idx1 == idx2, + _ => false, } } +} - /// Convert the parameter index into a metric that returns the "before" value. - /// - /// This is a convenience method for `into_metric_f64(key, ParameterReturnValue::Before)`. - pub fn into_metric_f64_before(self, key: &str) -> MetricF64 { - self.into_metric_f64(key, ParameterReturnValue::Before) - } +impl Eq for ParameterIndex where T: Eq {} - /// Convert the parameter index into a metric. - pub fn into_metric_u64(self, key: &str, return_value: ParameterReturnValue) -> MetricU64 { - let key = key.to_string(); +impl Display for ParameterIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - ParameterIndex::Const(index) => ConstantMetricU64::MultiParameterValue { index, key }.into(), - ParameterIndex::Simple(index) => SimpleMetricU64::MultiParameterValue { - index, - key, - return_value, - } - .into(), - ParameterIndex::General(index) => MetricU64::MultiParameterValue { - index, - key, - return_value, - }, + Self::Const(idx) => write!(f, "{idx}",), + Self::Simple(idx) => write!(f, "{idx}",), + Self::General(idx) => write!(f, "{}", idx.parameter), } } +} +impl From> for ParameterIndex { + fn from(idx: GeneralParameterRegistration) -> Self { + Self::General(idx) + } +} - /// Convert the parameter index into a metric that returns the "before" value. - /// - /// This is a convenience method for `into_metric_u64(key, ParameterReturnValue::Before)`. - pub fn into_metric_u64_before(self, key: &str) -> MetricU64 { - self.into_metric_u64(key, ParameterReturnValue::Before) +impl From> for ParameterIndex { + fn from(idx: SimpleParameterIndex) -> Self { + Self::Simple(idx) + } +} + +impl From> for ParameterIndex { + fn from(idx: ConstParameterIndex) -> Self { + Self::Const(idx) } } +/// Specifies whether to use the 'before' or 'after' parameter values. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ParameterReturnValue { + Before, + After, + AfterOrElseInitial, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParameterName { name: String, @@ -536,7 +543,7 @@ impl ParameterStates { match index { ParameterIndex::Const(idx) => self.constant.f64.get(*idx.deref()), ParameterIndex::Simple(idx) => self.simple.f64.get(*idx.deref()), - ParameterIndex::General(idx) => self.general.f64.get(*idx.deref()), + ParameterIndex::General(idx) => self.general.f64.get(*idx.parameter), } } pub fn get_general_f64_state(&self, index: GeneralParameterIndex) -> Option<&Option>> { @@ -547,7 +554,7 @@ impl ParameterStates { self.simple.f64.get(*index.deref()) } - pub fn get_const_f64_state(&self, index: SimpleParameterIndex) -> Option<&Option>> { + pub fn get_const_f64_state(&self, index: ConstParameterIndex) -> Option<&Option>> { self.constant.f64.get(*index.deref()) } @@ -555,7 +562,7 @@ impl ParameterStates { match index { ParameterIndex::Const(idx) => self.constant.f64.get_mut(*idx.deref()), ParameterIndex::Simple(idx) => self.simple.f64.get_mut(*idx.deref()), - ParameterIndex::General(idx) => self.general.f64.get_mut(*idx.deref()), + ParameterIndex::General(idx) => self.general.f64.get_mut(*idx.parameter), } } @@ -765,7 +772,7 @@ pub trait GeneralAfterParameterHook: GeneralParameter { ) -> Result<(), GeneralCalculationError>; } -#[derive(Debug)] +#[derive(Debug, Clone)] enum GeneralAfterOperation { Value(Arc>), Hook(Arc>), @@ -842,14 +849,6 @@ impl GeneralParameterEntry { fn as_parameter(&self) -> &dyn Parameter { self.parameter.as_parameter() } - - fn has_before(&self) -> bool { - self.before.is_some() - } - - fn has_after(&self) -> bool { - self.after.is_some() - } } #[derive(Debug, Error)] @@ -893,6 +892,8 @@ pub enum ParameterBuildError { InvalidDayOfYear { day: u32, month: u32 }, #[error("Parameter is configured without a valid calculation phase: {detail}")] NoCalculationPhase { detail: String }, + #[error("Metric for `{attr}` attribute is unused. {message}")] + UnusedMetric { attr: String, message: String }, } pub enum BuiltParameter { @@ -937,12 +938,12 @@ pub trait ParameterBuilder: Debug { /// /// # Example /// ```ignore -/// let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); +/// let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); /// ``` #[macro_export] macro_rules! resolve_metric_f64 { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => { - match $unresolved.resolve($maps) { + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => { + match $unresolved.resolve($maps, $phase) { Ok(m) => m, Err(err) => { return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { @@ -971,12 +972,12 @@ macro_rules! resolve_metric_f64 { /// /// # Example /// ```ignore -/// let metric = resolve_metric_u64!(self, self.metric, resolution_maps, "metric"); +/// let metric = resolve_metric_u64!(self, self.metric, resolution_maps, phase, "metric"); /// ``` #[macro_export] macro_rules! resolve_metric_u64 { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => { - match $unresolved.resolve($maps) { + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => { + match $unresolved.resolve($maps, $phase) { Ok(m) => m, Err(err) => { return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { parameter } = err { @@ -1005,9 +1006,9 @@ macro_rules! resolve_metric_u64 { /// #[macro_export] macro_rules! resolve_optional_metric_f64 { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => { + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => { match $unresolved { - Some(u) => match u.resolve($maps) { + Some(u) => match u.resolve($maps, $phase) { Ok(m) => Some(m), Err(err) => { return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { @@ -1038,15 +1039,15 @@ macro_rules! resolve_optional_metric_f64 { /// # Example /// ```ignore /// let control_curves = -/// resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, "control_curves"); +/// resolve_metric_f64_vec!(self, &self.control_curves, resolution_maps, phase, "control_curves"); /// ``` #[macro_export] macro_rules! resolve_metric_f64_vec { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => {{ let unresolved = $unresolved; let mut resolved = Vec::with_capacity(unresolved.len()); for m in unresolved.iter() { - match m.resolve($maps) { + match m.resolve($maps, $phase) { Ok(m) => resolved.push(m), Err(err) => { return if let $crate::metric::MetricF64ResolutionError::ParameterNotFound { parameter } = err { @@ -1076,11 +1077,11 @@ macro_rules! resolve_metric_f64_vec { /// #[macro_export] macro_rules! resolve_metric_u64_vec { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => {{ let unresolved = $unresolved; let mut resolved = Vec::with_capacity(unresolved.len()); for m in unresolved.iter() { - match m.resolve($maps) { + match m.resolve($maps, $phase) { Ok(m) => resolved.push(m), Err(err) => { return if let $crate::metric::MetricU64ResolutionError::ParameterNotFound { parameter } = err { @@ -1110,11 +1111,11 @@ macro_rules! resolve_metric_u64_vec { /// #[macro_export] macro_rules! resolve_metric_f64_hashmap { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => {{ let unresolved = $unresolved; let mut resolved = HashMap::with_capacity(unresolved.len()); for (k, m) in unresolved.iter() { - match m.resolve($maps) { + match m.resolve($maps, $phase) { Ok(m) => { resolved.insert(k.clone(), m); } @@ -1146,11 +1147,11 @@ macro_rules! resolve_metric_f64_hashmap { /// #[macro_export] macro_rules! resolve_metric_u64_hashmap { - ($self:ident, $unresolved:expr, $maps:expr, $attr:expr $(,)?) => {{ + ($self:ident, $unresolved:expr, $maps:expr, $phase:expr, $attr:expr $(,)?) => {{ let unresolved = $unresolved; let mut resolved = HashMap::with_capacity(unresolved.len()); for (k, m) in unresolved.iter() { - match m.resolve($maps) { + match m.resolve($maps, $phase) { Ok(m) => { resolved.insert(k.clone(), m); } @@ -1184,8 +1185,6 @@ pub struct SimpleParameterContext<'a> { /// A trait that defines a component that may produce a value each time-step, and may have an /// internal state that is updated each time-step. /// -/// See [`SimpleBeforeParameter`] and [`SimpleAfterParameter`] for more specific traits that define -/// the behaviour of parameters that produce values before or after the network is updated. pub trait SimpleParameter: Parameter { fn compute( &self, @@ -1344,17 +1343,24 @@ pub trait VariableParameter { fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option>; } +/// A struct that holds the required state sizes for a parameter collection. #[derive(Debug, Clone, Copy)] -pub struct ParameterCollectionSize { +pub struct ParameterCollectionStateSize { pub const_f64: usize, - pub const_usize: usize, + pub const_u64: usize, pub const_multi: usize, + pub simple_f64: usize, - pub simple_usize: usize, + pub simple_u64: usize, pub simple_multi: usize, - pub general_f64: usize, - pub general_usize: usize, - pub general_multi: usize, + + pub general_before_f64: usize, + pub general_before_u64: usize, + pub general_before_multi: usize, + + pub general_after_f64: usize, + pub general_after_u64: usize, + pub general_after_multi: usize, } /// Error types for the parameter collection. @@ -1594,6 +1600,52 @@ impl ParameterTimings { } } +#[derive(Debug)] +enum GeneralBeforeScheduleEntry { + F64 { + index: GeneralParameterIndex, + parameter: Arc>, + output: GeneralBeforeValueIndex, + }, + U64 { + index: GeneralParameterIndex, + parameter: Arc>, + output: GeneralBeforeValueIndex, + }, + Multi { + index: GeneralParameterIndex, + parameter: Arc>, + output: GeneralBeforeValueIndex, + }, +} + +#[derive(Debug)] +enum GeneralAfterScheduleOperation { + Value { + parameter: Arc>, + output: GeneralAfterValueIndex, + }, + Hook { + parameter: Arc>, + }, +} + +#[derive(Debug)] +enum GeneralAfterScheduleEntry { + F64 { + index: GeneralParameterIndex, + op: GeneralAfterScheduleOperation, + }, + U64 { + index: GeneralParameterIndex, + op: GeneralAfterScheduleOperation, + }, + Multi { + index: GeneralParameterIndex, + op: GeneralAfterScheduleOperation, + }, +} + #[derive(Error, Debug)] #[error("Error calculating general parameter '{name}': {source}")] pub enum ParameterCollectionGeneralCalculationError { @@ -1609,23 +1661,41 @@ pub enum ParameterCollectionGeneralCalculationError { #[source] source: Box, }, - #[error("Error setting state for general F64 parameter '{name}': {source}")] - F64SetStateError { + #[error("Error setting before state for general F64 parameter '{name}': {source}")] + F64SetBeforeStateError { name: ParameterName, #[source] - source: SetStateError>, + source: SetStateError>, }, - #[error("Error setting state for general U64 parameter '{name}': {source}")] - U64SetStateError { + #[error("Error setting after state for general F64 parameter '{name}': {source}")] + F64SetAfterStateError { name: ParameterName, #[source] - source: SetStateError>, + source: SetStateError>, }, - #[error("Error setting state for general Multi parameter '{name}': {source}")] - MultiSetStateError { + #[error("Error setting before state for general U64 parameter '{name}': {source}")] + U64SetBeforeStateError { + name: ParameterName, + #[source] + source: SetStateError>, + }, + #[error("Error setting after state for general U64 parameter '{name}': {source}")] + U64SetAfterStateError { name: ParameterName, #[source] - source: SetStateError>, + source: SetStateError>, + }, + #[error("Error setting before state for general Multi parameter '{name}': {source}")] + MultiSetBeforeStateError { + name: ParameterName, + #[source] + source: SetStateError>, + }, + #[error("Error setting after state for general Multi parameter '{name}': {source}")] + MultiSetAfterStateError { + name: ParameterName, + #[source] + source: SetStateError>, }, #[error("The timing data was created with from a different parameter collection. ")] TimingsFromAnotherCollection, @@ -1651,8 +1721,14 @@ pub struct ParameterCollection { general_f64: Vec>, general_u64: Vec>, general_multi: Vec>, - general_before_order: Vec, - general_after_order: Vec, + num_general_before_f64: usize, + num_general_before_u64: usize, + num_general_before_multi: usize, + num_general_after_f64: usize, + num_general_after_u64: usize, + num_general_after_multi: usize, + general_before_order: Vec, + general_after_order: Vec, id: u64, } @@ -1670,6 +1746,12 @@ impl Default for ParameterCollection { general_f64: Vec::new(), general_u64: Vec::new(), general_multi: Vec::new(), + num_general_before_f64: 0, + num_general_before_u64: 0, + num_general_before_multi: 0, + num_general_after_f64: 0, + num_general_after_u64: 0, + num_general_after_multi: 0, general_before_order: Vec::new(), general_after_order: Vec::new(), id: PARAMETER_COLLECTION_ID.fetch_add(1, Ordering::Relaxed), @@ -1678,17 +1760,20 @@ impl Default for ParameterCollection { } impl ParameterCollection { - pub fn size(&self) -> ParameterCollectionSize { - ParameterCollectionSize { + pub fn size(&self) -> ParameterCollectionStateSize { + ParameterCollectionStateSize { const_f64: self.constant_f64.len(), - const_usize: self.constant_u64.len(), + const_u64: self.constant_u64.len(), const_multi: self.constant_multi.len(), simple_f64: self.simple_f64.len(), - simple_usize: self.simple_u64.len(), + simple_u64: self.simple_u64.len(), simple_multi: self.simple_multi.len(), - general_f64: self.general_f64.len(), - general_usize: self.general_u64.len(), - general_multi: self.general_multi.len(), + general_before_f64: self.num_general_before_f64, + general_before_u64: self.num_general_before_u64, + general_before_multi: self.num_general_before_multi, + general_after_f64: self.num_general_after_f64, + general_after_u64: self.num_general_after_u64, + general_after_multi: self.num_general_after_multi, } } fn general_initial_states( @@ -1866,17 +1951,47 @@ impl ParameterCollection { fn push_general_f64(&mut self, entry: GeneralParameterEntry) -> ParameterIndex { let index = GeneralParameterIndex::new(self.general_f64.len()); - if entry.has_before() { - self.general_before_order.push(index.into()); - } + let before = entry.before.clone().map(|parameter| { + let output = GeneralBeforeValueIndex::new(self.num_general_before_f64); + self.num_general_before_f64 += 1; - if entry.has_after() { - self.general_after_order.push(index.into()); - } + self.general_before_order.push(GeneralBeforeScheduleEntry::F64 { + index, + parameter, + output, + }); + + output + }); + + let after = entry.after.clone().and_then(|op| match op { + GeneralAfterOperation::Value(parameter) => { + let output = GeneralAfterValueIndex::new(self.num_general_after_f64); + self.num_general_after_f64 += 1; + + let op = GeneralAfterScheduleOperation::Value { parameter, output }; + + self.general_after_order + .push(GeneralAfterScheduleEntry::F64 { index, op }); + + Some(output) + } + GeneralAfterOperation::Hook(parameter) => { + let op = GeneralAfterScheduleOperation::Hook { parameter }; + self.general_after_order + .push(GeneralAfterScheduleEntry::F64 { index, op }); + + None + } + }); self.general_f64.push(entry); - ParameterIndex::General(index) + ParameterIndex::General(GeneralParameterRegistration { + parameter: index, + before, + after, + }) } /// Push a new simple parameter to the collection. @@ -1907,7 +2022,7 @@ impl ParameterCollection { match index { ParameterIndex::Const(idx) => self.constant_f64.get(*idx.deref()).map(|p| p.as_parameter()), ParameterIndex::Simple(idx) => self.simple_f64.get(*idx.deref()).map(|p| p.as_parameter()), - ParameterIndex::General(idx) => self.general_f64.get(*idx.deref()).map(|p| p.as_parameter()), + ParameterIndex::General(idx) => self.general_f64.get(*idx.parameter.deref()).map(|p| p.as_parameter()), } } @@ -1916,20 +2031,66 @@ impl ParameterCollection { } pub fn get_f64_by_name(&self, name: &ParameterName) -> Option<&dyn Parameter> { - self.general_f64 + if let Some(p) = self + .general_f64 + .iter() + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) + { + Some(p) + } else if let Some(p) = self + .simple_f64 .iter() .find(|p| p.name() == name) .map(|p| p.as_parameter()) + { + Some(p) + } else { + self.constant_f64 + .iter() + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) + } } pub fn get_f64_index_by_name(&self, name: &ParameterName) -> Option> { - if let Some(idx) = self + if let Some(parameter_idx) = self .general_f64 .iter() .position(|p| p.name() == name) .map(GeneralParameterIndex::new) { - Some(idx.into()) + // Find if this index is used in the before or after schedule and return the appropriate index type. + let before = self.general_before_order.iter().find_map(|entry| { + if let GeneralBeforeScheduleEntry::F64 { index, output, .. } = entry { + if *index == parameter_idx { Some(*output) } else { None } + } else { + None + } + }); + + let after = self.general_after_order.iter().find_map(|entry| { + if let GeneralAfterScheduleEntry::F64 { index, op } = entry { + if *index == parameter_idx { + match op { + GeneralAfterScheduleOperation::Value { output, .. } => Some(*output), + GeneralAfterScheduleOperation::Hook { .. } => None, + } + } else { + None + } + } else { + None + } + }); + + let reg = GeneralParameterRegistration { + parameter: parameter_idx, + before, + after, + }; + + Some(reg.into()) } else if let Some(idx) = self .simple_f64 .iter() @@ -1952,16 +2113,47 @@ impl ParameterCollection { fn push_general_u64(&mut self, entry: GeneralParameterEntry) -> ParameterIndex { let index = GeneralParameterIndex::new(self.general_u64.len()); - if entry.has_before() { - self.general_before_order.push(index.into()); - } + let before = entry.before.clone().map(|parameter| { + let output = GeneralBeforeValueIndex::new(self.num_general_before_u64); + self.num_general_before_u64 += 1; + + self.general_before_order.push(GeneralBeforeScheduleEntry::U64 { + index, + parameter, + output, + }); + + output + }); + + let after = entry.after.clone().and_then(|op| match op { + GeneralAfterOperation::Value(parameter) => { + let output = GeneralAfterValueIndex::new(self.num_general_after_u64); + self.num_general_after_u64 += 1; + + let op = GeneralAfterScheduleOperation::Value { parameter, output }; + + self.general_after_order + .push(GeneralAfterScheduleEntry::U64 { index, op }); + + Some(output) + } + GeneralAfterOperation::Hook(parameter) => { + let op = GeneralAfterScheduleOperation::Hook { parameter }; + self.general_after_order + .push(GeneralAfterScheduleEntry::U64 { index, op }); + + None + } + }); - if entry.has_after() { - self.general_after_order.push(index.into()); - } self.general_u64.push(entry); - ParameterIndex::General(index) + ParameterIndex::General(GeneralParameterRegistration { + parameter: index, + before, + after, + }) } /// Push a new simple parameter to the collection. @@ -1994,7 +2186,7 @@ impl ParameterCollection { match index { ParameterIndex::Const(idx) => self.constant_u64.get(*idx.deref()).map(|p| p.as_parameter()), ParameterIndex::Simple(idx) => self.simple_u64.get(*idx.deref()).map(|p| p.as_parameter()), - ParameterIndex::General(idx) => self.general_u64.get(*idx.deref()).map(|p| p.as_parameter()), + ParameterIndex::General(idx) => self.general_u64.get(*idx.parameter.deref()).map(|p| p.as_parameter()), } } @@ -2003,54 +2195,131 @@ impl ParameterCollection { } pub fn get_u64_by_name(&self, name: &ParameterName) -> Option<&dyn Parameter> { - self.general_u64 + if let Some(p) = self + .general_u64 .iter() .find(|p| p.name() == name) .map(|p| p.as_parameter()) - } - - pub fn get_u64_index_by_name(&self, name: &ParameterName) -> Option> { - if let Some(idx) = self - .general_u64 - .iter() - .position(|p| p.name() == name) - .map(GeneralParameterIndex::new) { - Some(idx.into()) - } else if let Some(idx) = self + Some(p) + } else if let Some(p) = self .simple_u64 .iter() - .position(|p| p.name() == name) - .map(SimpleParameterIndex::new) + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) { - Some(idx.into()) + Some(p) } else { self.constant_u64 .iter() - .position(|p| p.name() == name) - .map(ConstParameterIndex::new) - .map(|idx| idx.into()) + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) } } - /// 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. + pub fn get_u64_index_by_name(&self, name: &ParameterName) -> Option> { + if let Some(parameter_idx) = self + .general_u64 + .iter() + .position(|p| p.name() == name) + .map(GeneralParameterIndex::new) + { + // Find if this index is used in the before or after schedule and return the appropriate index type. + let before = self.general_before_order.iter().find_map(|entry| { + if let GeneralBeforeScheduleEntry::U64 { index, output, .. } = entry { + if *index == parameter_idx { Some(*output) } else { None } + } else { + None + } + }); + + let after = self.general_after_order.iter().find_map(|entry| { + if let GeneralAfterScheduleEntry::U64 { index, op } = entry { + if *index == parameter_idx { + match op { + GeneralAfterScheduleOperation::Value { output, .. } => Some(*output), + GeneralAfterScheduleOperation::Hook { .. } => None, + } + } else { + None + } + } else { + None + } + }); + + let reg = GeneralParameterRegistration { + parameter: parameter_idx, + before, + after, + }; + + Some(reg.into()) + } else if let Some(idx) = self + .simple_u64 + .iter() + .position(|p| p.name() == name) + .map(SimpleParameterIndex::new) + { + Some(idx.into()) + } else { + self.constant_u64 + .iter() + .position(|p| p.name() == name) + .map(ConstParameterIndex::new) + .map(|idx| idx.into()) + } + } + + /// 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, entry: GeneralParameterEntry) -> ParameterIndex { let index = GeneralParameterIndex::new(self.general_multi.len()); - if entry.has_before() { - self.general_before_order.push(index.into()); - } + let before = entry.before.clone().map(|parameter| { + let output = GeneralBeforeValueIndex::new(self.num_general_before_multi); + self.num_general_before_multi += 1; + + self.general_before_order.push(GeneralBeforeScheduleEntry::Multi { + index, + parameter, + output, + }); + + output + }); + + let after = entry.after.clone().and_then(|op| match op { + GeneralAfterOperation::Value(parameter) => { + let output = GeneralAfterValueIndex::new(self.num_general_after_multi); + self.num_general_after_multi += 1; + + let op = GeneralAfterScheduleOperation::Value { parameter, output }; + + self.general_after_order + .push(GeneralAfterScheduleEntry::Multi { index, op }); + + Some(output) + } + GeneralAfterOperation::Hook(parameter) => { + let op = GeneralAfterScheduleOperation::Hook { parameter }; + self.general_after_order + .push(GeneralAfterScheduleEntry::Multi { index, op }); + + None + } + }); - if entry.has_after() { - self.general_after_order.push(index.into()); - } self.general_multi.push(entry); - ParameterIndex::General(index) + ParameterIndex::General(GeneralParameterRegistration { + parameter: index, + before, + after, + }) } /// Push a new simple parameter to the collection. @@ -2083,7 +2352,7 @@ impl ParameterCollection { match index { ParameterIndex::Const(idx) => self.constant_multi.get(*idx.deref()).map(|p| p.as_parameter()), ParameterIndex::Simple(idx) => self.simple_multi.get(*idx.deref()).map(|p| p.as_parameter()), - ParameterIndex::General(idx) => self.general_multi.get(*idx.deref()).map(|p| p.as_parameter()), + ParameterIndex::General(idx) => self.general_multi.get(*idx.parameter.deref()).map(|p| p.as_parameter()), } } @@ -2095,20 +2364,66 @@ impl ParameterCollection { } pub fn get_multi_by_name(&self, name: &ParameterName) -> Option<&dyn Parameter> { - self.general_multi + if let Some(p) = self + .general_multi + .iter() + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) + { + Some(p) + } else if let Some(p) = self + .simple_multi .iter() .find(|p| p.name() == name) .map(|p| p.as_parameter()) + { + Some(p) + } else { + self.constant_multi + .iter() + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) + } } pub fn get_multi_index_by_name(&self, name: &ParameterName) -> Option> { - if let Some(idx) = self + if let Some(parameter_idx) = self .general_multi .iter() .position(|p| p.name() == name) .map(GeneralParameterIndex::new) { - Some(idx.into()) + // Find if this index is used in the before or after schedule and return the appropriate index type. + let before = self.general_before_order.iter().find_map(|entry| { + if let GeneralBeforeScheduleEntry::Multi { index, output, .. } = entry { + if *index == parameter_idx { Some(*output) } else { None } + } else { + None + } + }); + + let after = self.general_after_order.iter().find_map(|entry| { + if let GeneralAfterScheduleEntry::Multi { index, op } = entry { + if *index == parameter_idx { + match op { + GeneralAfterScheduleOperation::Value { output, .. } => Some(*output), + GeneralAfterScheduleOperation::Hook { .. } => None, + } + } else { + None + } + } else { + None + } + }); + + let reg = GeneralParameterRegistration { + parameter: parameter_idx, + before, + after, + }; + + Some(reg.into()) } else if let Some(idx) = self .simple_multi .iter() @@ -2143,23 +2458,15 @@ impl ParameterCollection { for p in &self.general_before_order { let start = Instant::now(); match p { - GeneralParameterType::Parameter(idx) => { - // Find the parameter itself - let entry = self - .general_f64 - .get(*idx.deref()) - .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; - - // .. and its internal state + GeneralBeforeScheduleEntry::F64 { + index, + parameter, + output, + } => { + // Find any internal state let internal_state = internal_states - .get_general_mut_f64_state(*idx) - .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; - - let p = entry.before.as_ref().ok_or( - ParameterCollectionGeneralCalculationError::BeforeNotImplemented { - name: entry.name().clone(), - }, - )?; + .get_general_mut_f64_state(*index) + .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*index))?; let ctx = GeneralParameterContext { timestep, @@ -2168,42 +2475,37 @@ impl ParameterCollection { state, }; - let value = p.before(ctx, internal_state).map_err(|source| { + let value = parameter.before(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; state - .set_general_parameter_value_before(*idx, value) - .map_err(|source| ParameterCollectionGeneralCalculationError::F64SetStateError { - name: entry.name().clone(), - source, - })?; + .set_general_parameter_f64_before(*output, value) + .map_err( + |source| ParameterCollectionGeneralCalculationError::F64SetBeforeStateError { + name: parameter.name().clone(), + source, + }, + )?; if let Some(timings) = timings.as_deref_mut() { unsafe { - timings.general_f64.get_unchecked_mut(*idx.deref()).before += start.elapsed(); + timings.general_f64.get_unchecked_mut(*index.deref()).before += start.elapsed(); } } } - GeneralParameterType::Index(idx) => { - // Find the parameter itself - let entry = self - .general_u64 - .get(*idx.deref()) - .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; - // ... and its internal state + GeneralBeforeScheduleEntry::U64 { + index, + parameter, + output, + } => { + // Find the internal state let internal_state = internal_states - .get_general_mut_u64_state(*idx) - .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; - - let p = entry.before.as_ref().ok_or( - ParameterCollectionGeneralCalculationError::BeforeNotImplemented { - name: entry.name().clone(), - }, - )?; + .get_general_mut_u64_state(*index) + .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*index))?; let ctx = GeneralParameterContext { timestep, @@ -2212,42 +2514,37 @@ impl ParameterCollection { state, }; - let value = p.before(ctx, internal_state).map_err(|source| { + let value = parameter.before(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: p.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; state - .set_general_parameter_index_before(*idx, value) - .map_err(|source| ParameterCollectionGeneralCalculationError::U64SetStateError { - name: p.name().clone(), - source, - })?; + .set_general_parameter_u64_before(*output, value) + .map_err( + |source| ParameterCollectionGeneralCalculationError::U64SetBeforeStateError { + name: parameter.name().clone(), + source, + }, + )?; if let Some(timings) = timings.as_deref_mut() { unsafe { - timings.general_u64.get_unchecked_mut(*idx.deref()).before += start.elapsed(); + timings.general_u64.get_unchecked_mut(*index.deref()).before += start.elapsed(); } } } - GeneralParameterType::Multi(idx) => { - // Find the parameter itself - let entry = self - .general_multi - .get(*idx.deref()) - .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; - // ... and its internal state + GeneralBeforeScheduleEntry::Multi { + index, + parameter, + output, + } => { + // Find the internal state let internal_state = internal_states - .get_general_mut_multi_state(*idx) - .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; - - let p = entry.before.as_ref().ok_or( - ParameterCollectionGeneralCalculationError::BeforeNotImplemented { - name: entry.name().clone(), - }, - )?; + .get_general_mut_multi_state(*index) + .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*index))?; let ctx = GeneralParameterContext { timestep, @@ -2256,25 +2553,25 @@ impl ParameterCollection { state, }; - let value = p.before(ctx, internal_state).map_err(|source| { + let value = parameter.before(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: p.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; state - .set_general_multi_parameter_value_before(*idx, value) + .set_general_parameter_multi_before(*output, value) .map_err( - |source| ParameterCollectionGeneralCalculationError::MultiSetStateError { - name: p.name().clone(), + |source| ParameterCollectionGeneralCalculationError::MultiSetBeforeStateError { + name: parameter.name().clone(), source, }, )?; if let Some(timings) = timings.as_deref_mut() { unsafe { - timings.general_multi.get_unchecked_mut(*idx.deref()).before += start.elapsed(); + timings.general_multi.get_unchecked_mut(*index.deref()).before += start.elapsed(); } } } @@ -2303,22 +2600,11 @@ impl ParameterCollection { for p in &self.general_after_order { let start = Instant::now(); match p { - GeneralParameterType::Parameter(idx) => { - // Find the parameter itself - let entry = self - .general_f64 - .get(*idx.deref()) - .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; - // .. and its internal state + GeneralAfterScheduleEntry::F64 { index, op } => { + // Find the internal state let internal_state = internal_states - .get_general_mut_f64_state(*idx) - .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*idx))?; - - let op = entry.after.as_ref().ok_or( - ParameterCollectionGeneralCalculationError::AfterNotImplemented { - name: entry.name().clone(), - }, - )?; + .get_general_mut_f64_state(*index) + .ok_or(ParameterCollectionGeneralCalculationError::F64IndexNotFound(*index))?; let ctx = GeneralParameterContext { timestep, @@ -2328,25 +2614,27 @@ impl ParameterCollection { }; match op { - GeneralAfterOperation::Value(p) => { - let value = p.after(ctx, internal_state).map_err(|source| { + GeneralAfterScheduleOperation::Value { parameter, output } => { + let value = parameter.after(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; - state.set_general_parameter_value_after(*idx, value).map_err(|source| { - ParameterCollectionGeneralCalculationError::F64SetStateError { - name: entry.name().clone(), - source, - } - })?; + state + .set_general_parameter_f64_after(*output, value) + .map_err(|source| { + ParameterCollectionGeneralCalculationError::F64SetAfterStateError { + name: parameter.name().clone(), + source, + } + })?; } - GeneralAfterOperation::Hook(p) => { - p.after(ctx, internal_state).map_err(|source| { + GeneralAfterScheduleOperation::Hook { parameter } => { + parameter.after(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; @@ -2355,26 +2643,15 @@ impl ParameterCollection { if let Some(timings) = timings.as_deref_mut() { unsafe { - timings.general_f64.get_unchecked_mut(*idx.deref()).after += start.elapsed(); + timings.general_f64.get_unchecked_mut(*index.deref()).after += start.elapsed(); } } } - GeneralParameterType::Index(idx) => { - // Find the parameter itself - let entry = self - .general_u64 - .get(*idx.deref()) - .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; - // .. and its internal state + GeneralAfterScheduleEntry::U64 { index, op } => { + // Find the internal state let internal_state = internal_states - .get_general_mut_u64_state(*idx) - .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*idx))?; - - let op = entry.after.as_ref().ok_or( - ParameterCollectionGeneralCalculationError::AfterNotImplemented { - name: entry.name().clone(), - }, - )?; + .get_general_mut_u64_state(*index) + .ok_or(ParameterCollectionGeneralCalculationError::U64IndexNotFound(*index))?; let ctx = GeneralParameterContext { timestep, @@ -2384,25 +2661,27 @@ impl ParameterCollection { }; match op { - GeneralAfterOperation::Value(p) => { - let value = p.after(ctx, internal_state).map_err(|source| { + GeneralAfterScheduleOperation::Value { parameter, output } => { + let value = parameter.after(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; - state.set_general_parameter_index_after(*idx, value).map_err(|source| { - ParameterCollectionGeneralCalculationError::U64SetStateError { - name: entry.name().clone(), - source, - } - })?; + state + .set_general_parameter_u64_after(*output, value) + .map_err(|source| { + ParameterCollectionGeneralCalculationError::U64SetAfterStateError { + name: parameter.name().clone(), + source, + } + })?; } - GeneralAfterOperation::Hook(p) => { - p.after(ctx, internal_state).map_err(|source| { + GeneralAfterScheduleOperation::Hook { parameter } => { + parameter.after(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; @@ -2411,26 +2690,15 @@ impl ParameterCollection { if let Some(timings) = timings.as_deref_mut() { unsafe { - timings.general_u64.get_unchecked_mut(*idx.deref()).after += start.elapsed(); + timings.general_u64.get_unchecked_mut(*index.deref()).after += start.elapsed(); } } } - GeneralParameterType::Multi(idx) => { - // Find the parameter itself - let entry = self - .general_multi - .get(*idx.deref()) - .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; - // .. and its internal state + GeneralAfterScheduleEntry::Multi { index, op } => { + // Find the internal state let internal_state = internal_states - .get_general_mut_multi_state(*idx) - .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*idx))?; - - let op = entry.after.as_ref().ok_or( - ParameterCollectionGeneralCalculationError::AfterNotImplemented { - name: entry.name().clone(), - }, - )?; + .get_general_mut_multi_state(*index) + .ok_or(ParameterCollectionGeneralCalculationError::MultiIndexNotFound(*index))?; let ctx = GeneralParameterContext { timestep, @@ -2440,27 +2708,27 @@ impl ParameterCollection { }; match op { - GeneralAfterOperation::Value(p) => { - let value = p.after(ctx, internal_state).map_err(|source| { + GeneralAfterScheduleOperation::Value { parameter, output } => { + let value = parameter.after(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; state - .set_general_multi_parameter_value_after(*idx, value) - .map_err( - |source| ParameterCollectionGeneralCalculationError::MultiSetStateError { - name: entry.name().clone(), + .set_general_parameter_multi_after(*output, value) + .map_err(|source| { + ParameterCollectionGeneralCalculationError::MultiSetAfterStateError { + name: parameter.name().clone(), source, - }, - )?; + } + })?; } - GeneralAfterOperation::Hook(p) => { - p.after(ctx, internal_state).map_err(|source| { + GeneralAfterScheduleOperation::Hook { parameter } => { + parameter.after(ctx, internal_state).map_err(|source| { ParameterCollectionGeneralCalculationError::CalculationError { - name: entry.name().clone(), + name: parameter.name().clone(), source: Box::new(source), } })?; @@ -2469,7 +2737,7 @@ impl ParameterCollection { if let Some(timings) = timings.as_deref_mut() { unsafe { - timings.general_multi.get_unchecked_mut(*idx.deref()).after += start.elapsed(); + timings.general_multi.get_unchecked_mut(*index.deref()).after += start.elapsed(); } } } @@ -2512,7 +2780,7 @@ impl ParameterCollection { } })?; - state.set_simple_parameter_value_before(*idx, value).map_err(|source| { + state.set_simple_parameter_f64(*idx, value).map_err(|source| { ParameterCollectionSimpleCalculationError::F64SetStateError { name: p.name().clone(), source, @@ -2543,7 +2811,7 @@ impl ParameterCollection { } })?; - state.set_simple_parameter_index_before(*idx, value).map_err(|source| { + state.set_simple_parameter_u64(*idx, value).map_err(|source| { ParameterCollectionSimpleCalculationError::U64SetStateError { name: p.name().clone(), source, @@ -2574,12 +2842,12 @@ impl ParameterCollection { } })?; - state - .set_simple_multi_parameter_value_before(*idx, value) - .map_err(|source| ParameterCollectionSimpleCalculationError::MultiSetStateError { + state.set_simple_parameter_multi(*idx, value).map_err(|source| { + ParameterCollectionSimpleCalculationError::MultiSetStateError { name: p.name().clone(), source, - })?; + } + })?; } } } @@ -2614,7 +2882,7 @@ impl ParameterCollection { source, })?; - state.set_const_parameter_value(*idx, value).map_err(|source| { + state.set_const_parameter_f64(*idx, value).map_err(|source| { ParameterCollectionConstCalculationError::F64SetStateError { name: p.name().clone(), source, @@ -2638,7 +2906,7 @@ impl ParameterCollection { name: p.name().clone(), source, })?; - state.set_const_parameter_index(*idx, value).map_err(|source| { + state.set_const_parameter_u64(*idx, value).map_err(|source| { ParameterCollectionConstCalculationError::U64SetStateError { name: p.name().clone(), source, @@ -2662,7 +2930,7 @@ impl ParameterCollection { name: p.name().clone(), source, })?; - state.set_const_multi_parameter_value(*idx, value).map_err(|source| { + state.set_const_parameter_multi(*idx, value).map_err(|source| { ParameterCollectionConstCalculationError::MultiSetStateError { name: p.name().clone(), source, @@ -2738,6 +3006,21 @@ impl ParameterCollectionBuilder { mut self, resolution_maps: &mut ResolutionMaps, ) -> Result { + // Validate names before attempting resolution so duplicate builders always produce the + // same error, including when neither builder can yet be resolved. + let mut names = HashSet::with_capacity(self.len()); + for name in self + .f64 + .iter() + .map(|p| p.name()) + .chain(self.u64.iter().map(|p| p.name())) + .chain(self.multi.iter().map(|p| p.name())) + { + if !names.insert(name.clone()) { + return Err(ParameterCollectionBuilderError::DuplicateParameterName { name: name.clone() }); + } + } + let mut collection = ParameterCollection::default(); let mut num_unbuilt = self.len(); @@ -2898,167 +3181,151 @@ impl ParameterCollectionBuilder { #[cfg(test)] mod tests { + use super::test_utils::{ + TestBuildKind, TestParameter, TestParameterBuilder, TestParameterFailure, TestValueType, test_parameter_state, + }; use super::{ - BuiltParameter, ConstParameter, GeneralAfterParameter, GeneralAfterParameterHook, GeneralBeforeParameter, - GeneralCalculationError, GeneralParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, - Parameter, ParameterBuildError, ParameterBuilder, ParameterCollection, ParameterCollectionBuilder, - ParameterIndex, ParameterMeta, ParameterName, ParameterReturnValue, ParameterState, ParameterStates, - SimpleParameter, SimpleParameterContext, + GeneralCalculationError, GeneralParameterEntry, ParameterBuildError, ParameterCollection, + ParameterCollectionBuilder, ParameterCollectionBuilderError, ParameterCollectionConstCalculationError, + ParameterCollectionGeneralCalculationError, ParameterCollectionSetupError, + ParameterCollectionSimpleCalculationError, ParameterIndex, ParameterName, ParameterSetupError, ParameterState, + ParameterStates, ParameterTimings, }; - use crate::metric::{MetricF64, SimpleMetricF64}; use crate::network::{Network, ResolutionMaps}; use crate::parameters::errors::{ConstCalculationError, SimpleCalculationError}; use crate::scenario::ScenarioIndex; - use crate::state::{ConstParameterValues, MultiValue, StateBuilder}; + use crate::state::{MultiValue, SetStateError, StateBuilder}; use crate::test_utils::default_domain; + use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; - #[derive(Debug)] - struct TestParameterBuilder { - meta: ParameterMeta, - } - - impl Default for TestParameterBuilder { - fn default() -> Self { - Self { - meta: ParameterMeta::new("test-parameter".into()), + fn add_test_parameter_builder( + collection: &mut ParameterCollectionBuilder, + value_type: TestValueType, + builder: TestParameterBuilder, + ) { + match value_type { + TestValueType::F64 => { + collection.f64(Box::new(builder)); + } + TestValueType::U64 => { + collection.u64(Box::new(builder)); + } + TestValueType::Multi => { + collection.multi(Box::new(builder)); } } } - 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)))) + fn assert_index_kind(index: &ParameterIndex, kind: TestBuildKind, expected_position: usize) { + match (index, kind) { + (ParameterIndex::Const(index), TestBuildKind::Const) => assert_eq!(**index, expected_position), + (ParameterIndex::Simple(index), TestBuildKind::Simple) => assert_eq!(**index, expected_position), + (ParameterIndex::General(registration), TestBuildKind::General) => { + assert_eq!(*registration.parameter, expected_position); + assert!(registration.before.is_some()); + assert!(registration.after.is_none()); + } + (actual, expected) => panic!("expected {expected:?} index, got {actual:?}"), } } - 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)))) + fn assert_general_registration(index: &ParameterIndex, has_before: bool, has_after: bool) { + let ParameterIndex::General(registration) = index else { + panic!("expected a general parameter index, got {index:?}"); + }; + assert_eq!(registration.before.is_some(), has_before); + assert_eq!(registration.after.is_some(), has_after); + } + + fn assert_f64_lookup(collection: &ParameterCollection, index: ParameterIndex, expected_name: &str) { + let name: ParameterName = expected_name.into(); + assert_eq!(collection.get_f64(index).unwrap().name(), &name); + assert_eq!(collection.get_f64_by_name(&name).unwrap().name(), &name); + assert_eq!(collection.get_f64_index_by_name(&name).as_ref(), Some(&index)); + if let ParameterIndex::General(registration) = index { + assert_eq!( + collection.get_general_f64(registration.parameter).unwrap().name(), + &name + ); } } - /// Parameter for testing purposes - #[derive(Debug)] - struct TestParameter { - meta: ParameterMeta, - } - - impl Default for TestParameter { - fn default() -> Self { - Self { - meta: ParameterMeta::new("test-parameter".into()), - } + fn assert_u64_lookup(collection: &ParameterCollection, index: ParameterIndex, expected_name: &str) { + let name: ParameterName = expected_name.into(); + assert_eq!(collection.get_u64(index).unwrap().name(), &name); + assert_eq!(collection.get_u64_by_name(&name).unwrap().name(), &name); + assert_eq!(collection.get_u64_index_by_name(&name).as_ref(), Some(&index)); + if let ParameterIndex::General(registration) = index { + assert_eq!( + collection.get_general_u64(registration.parameter).unwrap().name(), + &name + ); } } - impl Parameter for TestParameter { - fn meta(&self) -> &ParameterMeta { - &self.meta - } - } - - impl ConstParameter for TestParameter - where - T: From, - { - fn compute( - &self, - _scenario_index: &ScenarioIndex, - _values: &ConstParameterValues, - _internal_state: &mut Option>, - ) -> Result { - Ok(T::from(1)) - } - fn as_parameter(&self) -> &dyn Parameter { - self + fn assert_multi_lookup(collection: &ParameterCollection, index: &ParameterIndex, expected_name: &str) { + let name: ParameterName = expected_name.into(); + assert_eq!(collection.get_multi(index).unwrap().name(), &name); + assert_eq!(collection.get_multi_by_name(&name).unwrap().name(), &name); + assert_eq!(collection.get_multi_index_by_name(&name).as_ref(), Some(index)); + if let ParameterIndex::General(registration) = index { + assert_eq!( + collection.get_general_multi(®istration.parameter).unwrap().name(), + &name + ); } } - impl ConstParameter for TestParameter { - fn compute( - &self, - _scenario_index: &ScenarioIndex, - _values: &ConstParameterValues, - _internal_state: &mut Option>, - ) -> Result { - Ok(MultiValue::default()) - } - - fn as_parameter(&self) -> &dyn Parameter { - self - } - } - impl SimpleParameter for TestParameter - where - T: From, - { - fn compute( - &self, - _ctx: SimpleParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - Ok(T::from(1)) - } - fn as_parameter(&self) -> &dyn Parameter { - self + fn expect_const_index(index: ParameterIndex) -> super::ConstParameterIndex { + match index { + ParameterIndex::Const(index) => index, + _ => panic!("expected a constant parameter index"), } } - impl SimpleParameter for TestParameter { - fn compute( - &self, - _ctx: SimpleParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - Ok(MultiValue::default()) - } - fn as_parameter(&self) -> &dyn Parameter { - self - } - } - impl GeneralParameter for TestParameter { - fn as_parameter(&self) -> &dyn Parameter { - self + fn expect_simple_index(index: ParameterIndex) -> super::SimpleParameterIndex { + match index { + ParameterIndex::Simple(index) => index, + _ => panic!("expected a simple parameter index"), } } - impl GeneralBeforeParameter for TestParameter - where - T: From, - { - fn before( - &self, - _ctx: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - Ok(T::from(1)) + fn expect_general_registration(index: ParameterIndex) -> super::GeneralParameterRegistration { + match index { + ParameterIndex::General(registration) => registration, + _ => panic!("expected a general parameter index"), } } - impl GeneralBeforeParameter for TestParameter { - fn before( - &self, - _ctx: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - Ok(MultiValue::default()) + fn assert_test_parameter_state( + state: &Option>, + owner: &str, + timestep_count: usize, + scenario_id: usize, + calls: usize, + ) { + let state = test_parameter_state(state).expect("expected test parameter state"); + assert_eq!(state.owner(), owner); + assert_eq!(state.timestep_count(), timestep_count); + assert_eq!(state.scenario_id(), scenario_id); + assert_eq!(state.calls(), calls); + } + + fn assert_general_calculation_error( + error: ParameterCollectionGeneralCalculationError, + expected_name: &str, + expected_message: &str, + ) { + match error { + ParameterCollectionGeneralCalculationError::CalculationError { name, source } => { + assert_eq!(name, ParameterName::from(expected_name)); + assert!(matches!( + source.as_ref(), + GeneralCalculationError::Internal { message } if message == expected_message + )); + } + other => panic!("expected a general calculation error, got {other:?}"), } } @@ -3087,497 +3354,1233 @@ mod tests { assert!(collection.build(&mut ResolutionMaps::new(default_domain())).is_err()); } - #[derive(Debug)] - struct PhaseTestParameter { - meta: ParameterMeta, - events: Arc>>, - } - - impl PhaseTestParameter { - fn new(name: &str, events: Arc>>) -> Self { - Self { - meta: ParameterMeta::new(name.into()), - events, + #[test] + fn builder_registers_each_value_and_parameter_kind() { + let mut builder = ParameterCollectionBuilder::default(); + for (value_type, prefix) in [ + (TestValueType::F64, "f64"), + (TestValueType::U64, "u64"), + (TestValueType::Multi, "multi"), + ] { + for (kind, suffix) in [ + (TestBuildKind::Const, "const"), + (TestBuildKind::Simple, "simple"), + (TestBuildKind::General, "general"), + ] { + add_test_parameter_builder( + &mut builder, + value_type, + TestParameterBuilder::new(&format!("{prefix}-{suffix}"), kind), + ); } } - fn record(&self, phase: &str) { - self.events.lock().unwrap().push(format!("{}:{phase}", self.name())); + let mut maps = ResolutionMaps::new(default_domain()); + let collection = builder.build(&mut maps).unwrap(); + let size = collection.size(); + assert_eq!(size.const_f64, 1); + assert_eq!(size.const_u64, 1); + assert_eq!(size.const_multi, 1); + assert_eq!(size.simple_f64, 1); + assert_eq!(size.simple_u64, 1); + assert_eq!(size.simple_multi, 1); + assert_eq!(size.general_before_f64, 1); + assert_eq!(size.general_before_u64, 1); + assert_eq!(size.general_before_multi, 1); + assert_eq!(size.general_after_f64, 0); + assert_eq!(size.general_after_u64, 0); + assert_eq!(size.general_after_multi, 0); + + for (name, kind) in [ + ("f64-const", TestBuildKind::Const), + ("f64-simple", TestBuildKind::Simple), + ("f64-general", TestBuildKind::General), + ] { + let name: ParameterName = name.into(); + let mapped = maps.parameters_f64.get(&name).unwrap(); + assert_index_kind(mapped, kind, 0); + assert_eq!(collection.get_f64(*mapped).unwrap().name(), &name); + assert_eq!(collection.get_f64_by_name(&name).unwrap().name(), &name); + assert_eq!(collection.get_f64_index_by_name(&name).as_ref(), Some(mapped)); + assert!(!maps.parameters_u64.contains_key(&name)); + assert!(!maps.parameters_multi.contains_key(&name)); } - } - - impl Parameter for PhaseTestParameter { - fn meta(&self) -> &ParameterMeta { - &self.meta + for (name, kind) in [ + ("u64-const", TestBuildKind::Const), + ("u64-simple", TestBuildKind::Simple), + ("u64-general", TestBuildKind::General), + ] { + let name: ParameterName = name.into(); + let mapped = maps.parameters_u64.get(&name).unwrap(); + assert_index_kind(mapped, kind, 0); + assert_eq!(collection.get_u64(*mapped).unwrap().name(), &name); + assert_eq!(collection.get_u64_by_name(&name).unwrap().name(), &name); + assert_eq!(collection.get_u64_index_by_name(&name).as_ref(), Some(mapped)); + assert!(!maps.parameters_f64.contains_key(&name)); + assert!(!maps.parameters_multi.contains_key(&name)); + } + for (name, kind) in [ + ("multi-const", TestBuildKind::Const), + ("multi-simple", TestBuildKind::Simple), + ("multi-general", TestBuildKind::General), + ] { + let name: ParameterName = name.into(); + let mapped = maps.parameters_multi.get(&name).unwrap(); + assert_index_kind(mapped, kind, 0); + assert_eq!(collection.get_multi(mapped).unwrap().name(), &name); + assert_eq!(collection.get_multi_by_name(&name).unwrap().name(), &name); + assert_eq!(collection.get_multi_index_by_name(&name).as_ref(), Some(mapped)); + assert!(!maps.parameters_f64.contains_key(&name)); + assert!(!maps.parameters_u64.contains_key(&name)); } } - impl SimpleParameter for PhaseTestParameter { - fn compute( - &self, - _context: SimpleParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - self.record("before"); - Ok(11.0) + #[test] + fn duplicate_names_return_exact_error_for_all_type_combinations() { + for (first, second) in [ + (TestValueType::F64, TestValueType::F64), + (TestValueType::U64, TestValueType::U64), + (TestValueType::Multi, TestValueType::Multi), + (TestValueType::F64, TestValueType::U64), + (TestValueType::F64, TestValueType::Multi), + (TestValueType::U64, TestValueType::Multi), + ] { + let mut builder = ParameterCollectionBuilder::default(); + add_test_parameter_builder( + &mut builder, + first, + TestParameterBuilder::new("duplicate", TestBuildKind::Const), + ); + add_test_parameter_builder( + &mut builder, + second, + TestParameterBuilder::new("duplicate", TestBuildKind::Simple), + ); + + assert!(matches!( + builder.build(&mut ResolutionMaps::new(default_domain())), + Err(ParameterCollectionBuilderError::DuplicateParameterName { name }) + if name == ParameterName::from("duplicate") + )); } + } - fn as_parameter(&self) -> &dyn Parameter { - self - } + #[test] + fn duplicate_unresolved_builders_are_reported_as_duplicates() { + let mut builder = ParameterCollectionBuilder::default(); + builder + .f64(Box::new( + TestParameterBuilder::new("duplicate", TestBuildKind::General).depending_on("missing"), + )) + .f64(Box::new( + TestParameterBuilder::new("duplicate", TestBuildKind::General).depending_on("missing"), + )); + + assert!(matches!( + builder.build(&mut ResolutionMaps::new(default_domain())), + Err(ParameterCollectionBuilderError::DuplicateParameterName { name }) + if name == ParameterName::from("duplicate") + )); } - impl GeneralParameter for PhaseTestParameter { - fn as_parameter(&self) -> &dyn Parameter { - self - } + #[test] + fn forward_reference_builds_after_dependency() { + let order = Arc::new(Mutex::new(Vec::new())); + let dependent = TestParameterBuilder::with_build_order("dependent", TestBuildKind::General, order.clone()) + .depending_on("source"); + let dependent_attempts = dependent.attempts(); + let source = TestParameterBuilder::with_build_order("source", TestBuildKind::Const, order.clone()); + let source_attempts = source.attempts(); + let mut builder = ParameterCollectionBuilder::default(); + builder.f64(Box::new(dependent)).f64(Box::new(source)); + + let mut maps = ResolutionMaps::new(default_domain()); + let collection = builder.build(&mut maps).unwrap(); + + assert_eq!(order.lock().unwrap().as_slice(), ["source", "dependent"]); + assert_eq!(dependent_attempts.load(Ordering::Relaxed), 2); + assert_eq!(source_attempts.load(Ordering::Relaxed), 1); + assert!(collection.has_name(&"source".into())); + assert!(collection.has_name(&"dependent".into())); } - impl GeneralBeforeParameter for PhaseTestParameter { - fn before( - &self, - _context: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - self.record("before"); - Ok(11.0) - } + #[test] + fn reverse_order_dependency_chain_is_topologically_built() { + let order = Arc::new(Mutex::new(Vec::new())); + let a = TestParameterBuilder::with_build_order("a", TestBuildKind::General, order.clone()).depending_on("b"); + let b = TestParameterBuilder::with_build_order("b", TestBuildKind::General, order.clone()).depending_on("c"); + let c = TestParameterBuilder::with_build_order("c", TestBuildKind::Const, order.clone()); + let a_attempts = a.attempts(); + let b_attempts = b.attempts(); + let c_attempts = c.attempts(); + let mut builder = ParameterCollectionBuilder::default(); + builder.f64(Box::new(a)).f64(Box::new(b)).f64(Box::new(c)); + + builder.build(&mut ResolutionMaps::new(default_domain())).unwrap(); + + assert_eq!(order.lock().unwrap().as_slice(), ["c", "b", "a"]); + assert_eq!(a_attempts.load(Ordering::Relaxed), 3); + assert_eq!(b_attempts.load(Ordering::Relaxed), 2); + assert_eq!(c_attempts.load(Ordering::Relaxed), 1); } - impl GeneralAfterParameter for PhaseTestParameter { - fn after( - &self, - _context: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - self.record("after"); - Ok(22.0) - } + #[test] + fn dependencies_can_progress_across_typed_builder_vectors() { + let order = Arc::new(Mutex::new(Vec::new())); + let f64_builder = + TestParameterBuilder::with_build_order("f64", TestBuildKind::General, order.clone()).depending_on("u64"); + let u64_builder = + TestParameterBuilder::with_build_order("u64", TestBuildKind::General, order.clone()).depending_on("multi"); + let multi_builder = TestParameterBuilder::with_build_order("multi", TestBuildKind::Const, order.clone()); + let f64_attempts = f64_builder.attempts(); + let u64_attempts = u64_builder.attempts(); + let multi_attempts = multi_builder.attempts(); + let mut builder = ParameterCollectionBuilder::default(); + builder + .f64(Box::new(f64_builder)) + .u64(Box::new(u64_builder)) + .multi(Box::new(multi_builder)); + + let mut maps = ResolutionMaps::new(default_domain()); + let collection = builder.build(&mut maps).unwrap(); + + assert_eq!(order.lock().unwrap().as_slice(), ["multi", "u64", "f64"]); + assert_eq!(f64_attempts.load(Ordering::Relaxed), 3); + assert_eq!(u64_attempts.load(Ordering::Relaxed), 2); + assert_eq!(multi_attempts.load(Ordering::Relaxed), 1); + assert!(collection.get_f64_by_name(&"f64".into()).is_some()); + assert!(collection.get_u64_by_name(&"u64".into()).is_some()); + assert!(collection.get_multi_by_name(&"multi".into()).is_some()); } - impl GeneralAfterParameterHook for PhaseTestParameter { - fn after( - &self, - _context: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result<(), GeneralCalculationError> { - self.record("hook"); - Ok(()) + #[test] + fn missing_dependency_returns_exact_missing_name_for_all_types() { + for value_type in [TestValueType::F64, TestValueType::U64, TestValueType::Multi] { + let mut builder = ParameterCollectionBuilder::default(); + add_test_parameter_builder( + &mut builder, + value_type, + TestParameterBuilder::new("dependent", TestBuildKind::General).depending_on("missing"), + ); + + assert!(matches!( + builder.build(&mut ResolutionMaps::new(default_domain())), + Err(ParameterCollectionBuilderError::ParameterNotFound { name }) + if name == ParameterName::from("missing") + )); } } - fn simple_index(index: ParameterIndex) -> super::SimpleParameterIndex { - match index { - ParameterIndex::Simple(index) => index, - _ => panic!("expected a simple parameter index"), + #[test] + fn self_reference_returns_circular_reference() { + let mut builder = ParameterCollectionBuilder::default(); + builder.f64(Box::new( + TestParameterBuilder::new("self", TestBuildKind::General).depending_on("self"), + )); + + match builder.build(&mut ResolutionMaps::new(default_domain())) { + Err(ParameterCollectionBuilderError::CircularParameterReference { names }) => { + assert_eq!(names, vec![ParameterName::from("self")]); + } + result => panic!("expected a circular reference error, got {result:?}"), } } - fn general_index(index: ParameterIndex) -> super::GeneralParameterIndex { - match index { - ParameterIndex::General(index) => index, - _ => panic!("expected a general parameter index"), + #[test] + fn three_parameter_cycle_returns_all_cycle_names() { + let mut builder = ParameterCollectionBuilder::default(); + builder + .f64(Box::new( + TestParameterBuilder::new("a", TestBuildKind::General).depending_on("b"), + )) + .f64(Box::new( + TestParameterBuilder::new("b", TestBuildKind::General).depending_on("c"), + )) + .f64(Box::new( + TestParameterBuilder::new("c", TestBuildKind::General).depending_on("a"), + )); + + match builder.build(&mut ResolutionMaps::new(default_domain())) { + Err(ParameterCollectionBuilderError::CircularParameterReference { names }) => { + assert_eq!(names, ["a", "b", "c"].map(ParameterName::from)); + } + result => panic!("expected a circular reference error, got {result:?}"), } } - #[derive(Debug)] - struct SimpleDependencyTestParameter { - meta: ParameterMeta, - metric: SimpleMetricF64, - observed_values: Arc>>, + #[test] + fn mixed_cycle_and_missing_dependency_prefers_missing_error() { + let mut builder = ParameterCollectionBuilder::default(); + builder + .f64(Box::new( + TestParameterBuilder::new("a", TestBuildKind::General).depending_on("b"), + )) + .f64(Box::new( + TestParameterBuilder::new("b", TestBuildKind::General).depending_on("a"), + )) + .f64(Box::new( + TestParameterBuilder::new("c", TestBuildKind::General).depending_on("missing"), + )); + + assert!(matches!( + builder.build(&mut ResolutionMaps::new(default_domain())), + Err(ParameterCollectionBuilderError::ParameterNotFound { name }) + if name == ParameterName::from("missing") + )); } - impl SimpleDependencyTestParameter { - fn new(name: &str, metric: SimpleMetricF64, observed_values: Arc>>) -> Self { - Self { - meta: ParameterMeta::new(name.into()), - metric, - observed_values, + #[test] + fn parameter_build_error_preserves_parameter_name_and_source() { + let mut builder = ParameterCollectionBuilder::default(); + builder.f64(Box::new( + TestParameterBuilder::new("broken", TestBuildKind::General).failing("intentional failure"), + )); + + let error = builder + .build(&mut ResolutionMaps::new(default_domain())) + .expect_err("the scripted builder should fail"); + let display = error.to_string(); + match error { + ParameterCollectionBuilderError::ParameterBuildError { name, source } => { + assert_eq!(name, ParameterName::from("broken")); + assert!(matches!( + source.as_ref(), + ParameterBuildError::NoCalculationPhase { detail } if detail == "intentional failure" + )); } + other => panic!("expected a wrapped parameter build error, got {other:?}"), } + assert!(display.contains("broken")); + assert!(display.contains("intentional failure")); + } - fn calculate(&self, context: SimpleParameterContext<'_>) -> Result { - let value = self.metric.get_value(context.values)?; - self.observed_values.lock().unwrap().push(value); + #[test] + fn size_counts_every_storage_and_general_output_category() { + let mut collection = ParameterCollection::default(); - // Returning a transformed value makes it clear that this parameter - // consumed the dependency rather than simply reproducing a constant. - Ok(value * 2.0) - } - } + collection.push_const_f64(Box::new(TestParameter::named("f64-const"))); + collection.push_simple_f64(Box::new(TestParameter::named("f64-simple"))); + collection.push_general_f64(GeneralParameterEntry::before(TestParameter::named("f64-before"))); + collection.push_general_f64(GeneralParameterEntry::after(TestParameter::named("f64-after"))); + collection.push_general_f64(GeneralParameterEntry::both(TestParameter::named("f64-both"))); + collection.push_general_f64(GeneralParameterEntry::before_with_after_hook(TestParameter::named( + "f64-hook", + ))); - impl Parameter for SimpleDependencyTestParameter { - fn meta(&self) -> &ParameterMeta { - &self.meta - } - } + collection.push_const_u64(Box::new(TestParameter::named("u64-const"))); + collection.push_simple_u64(Box::new(TestParameter::named("u64-simple"))); + collection.push_general_u64(GeneralParameterEntry::before(TestParameter::named("u64-before"))); + collection.push_general_u64(GeneralParameterEntry::after(TestParameter::named("u64-after"))); + collection.push_general_u64(GeneralParameterEntry::both(TestParameter::named("u64-both"))); + collection.push_general_u64(GeneralParameterEntry::before_with_after_hook(TestParameter::named( + "u64-hook", + ))); - impl SimpleParameter for SimpleDependencyTestParameter { - fn compute( - &self, - context: SimpleParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - self.calculate(context) - } - fn as_parameter(&self) -> &dyn Parameter { - self - } - } + collection.push_const_multi(Box::new(TestParameter::named("multi-const"))); + collection.push_simple_multi(Box::new(TestParameter::named("multi-simple"))); + collection.push_general_multi(GeneralParameterEntry::before(TestParameter::named("multi-before"))); + collection.push_general_multi(GeneralParameterEntry::after(TestParameter::named("multi-after"))); + collection.push_general_multi(GeneralParameterEntry::both(TestParameter::named("multi-both"))); + collection.push_general_multi(GeneralParameterEntry::before_with_after_hook(TestParameter::named( + "multi-hook", + ))); - #[derive(Debug)] - struct GeneralDependencyTestParameter { - meta: ParameterMeta, - metric: MetricF64, - observed_values: Arc>>, + let size = collection.size(); + assert_eq!(size.const_f64, 1); + assert_eq!(size.const_u64, 1); + assert_eq!(size.const_multi, 1); + assert_eq!(size.simple_f64, 1); + assert_eq!(size.simple_u64, 1); + assert_eq!(size.simple_multi, 1); + assert_eq!(size.general_before_f64, 3); + assert_eq!(size.general_before_u64, 3); + assert_eq!(size.general_before_multi, 3); + assert_eq!(size.general_after_f64, 2); + assert_eq!(size.general_after_u64, 2); + assert_eq!(size.general_after_multi, 2); } - impl GeneralDependencyTestParameter { - fn new(name: &str, metric: MetricF64, observed_values: Arc>>) -> Self { - Self { - meta: ParameterMeta::new(name.into()), - metric, - observed_values, - } - } - - fn calculate(&self, context: GeneralParameterContext<'_>) -> Result { - let value = self.metric.get_value(context.network, context.state)?; - self.observed_values.lock().unwrap().push(value); + #[test] + fn typed_lookup_round_trips_indices_names_and_phase_registrations() { + let mut collection = ParameterCollection::default(); - Ok(value * 2.0) + let f64_const = collection.push_const_f64(Box::new(TestParameter::named("f64-const"))); + let f64_simple = collection.push_simple_f64(Box::new(TestParameter::named("f64-simple"))); + let f64_before = collection.push_general_f64(GeneralParameterEntry::before(TestParameter::named("f64-before"))); + let f64_after = collection.push_general_f64(GeneralParameterEntry::after(TestParameter::named("f64-after"))); + let f64_both = collection.push_general_f64(GeneralParameterEntry::both(TestParameter::named("f64-both"))); + let f64_hook = collection.push_general_f64(GeneralParameterEntry::before_with_after_hook( + TestParameter::named("f64-hook"), + )); + + let u64_const = collection.push_const_u64(Box::new(TestParameter::named("u64-const"))); + let u64_simple = collection.push_simple_u64(Box::new(TestParameter::named("u64-simple"))); + let u64_before = collection.push_general_u64(GeneralParameterEntry::before(TestParameter::named("u64-before"))); + let u64_after = collection.push_general_u64(GeneralParameterEntry::after(TestParameter::named("u64-after"))); + let u64_both = collection.push_general_u64(GeneralParameterEntry::both(TestParameter::named("u64-both"))); + let u64_hook = collection.push_general_u64(GeneralParameterEntry::before_with_after_hook( + TestParameter::named("u64-hook"), + )); + + let multi_const = collection.push_const_multi(Box::new(TestParameter::named("multi-const"))); + let multi_simple = collection.push_simple_multi(Box::new(TestParameter::named("multi-simple"))); + let multi_before = + collection.push_general_multi(GeneralParameterEntry::before(TestParameter::named("multi-before"))); + let multi_after = + collection.push_general_multi(GeneralParameterEntry::after(TestParameter::named("multi-after"))); + let multi_both = collection.push_general_multi(GeneralParameterEntry::both(TestParameter::named("multi-both"))); + let multi_hook = collection.push_general_multi(GeneralParameterEntry::before_with_after_hook( + TestParameter::named("multi-hook"), + )); + + assert_f64_lookup(&collection, f64_const, "f64-const"); + assert_f64_lookup(&collection, f64_simple, "f64-simple"); + assert_f64_lookup(&collection, f64_before, "f64-before"); + assert_f64_lookup(&collection, f64_after, "f64-after"); + assert_f64_lookup(&collection, f64_both, "f64-both"); + assert_f64_lookup(&collection, f64_hook, "f64-hook"); + + assert_u64_lookup(&collection, u64_const, "u64-const"); + assert_u64_lookup(&collection, u64_simple, "u64-simple"); + assert_u64_lookup(&collection, u64_before, "u64-before"); + assert_u64_lookup(&collection, u64_after, "u64-after"); + assert_u64_lookup(&collection, u64_both, "u64-both"); + assert_u64_lookup(&collection, u64_hook, "u64-hook"); + + assert_multi_lookup(&collection, &multi_const, "multi-const"); + assert_multi_lookup(&collection, &multi_simple, "multi-simple"); + assert_multi_lookup(&collection, &multi_before, "multi-before"); + assert_multi_lookup(&collection, &multi_after, "multi-after"); + assert_multi_lookup(&collection, &multi_both, "multi-both"); + assert_multi_lookup(&collection, &multi_hook, "multi-hook"); + + for (index, has_before, has_after) in [ + (&f64_before, true, false), + (&f64_after, false, true), + (&f64_both, true, true), + (&f64_hook, true, false), + ] { + assert_general_registration(index, has_before, has_after); } - } - - impl Parameter for GeneralDependencyTestParameter { - fn meta(&self) -> &ParameterMeta { - &self.meta + for (index, has_before, has_after) in [ + (&u64_before, true, false), + (&u64_after, false, true), + (&u64_both, true, true), + (&u64_hook, true, false), + ] { + assert_general_registration(index, has_before, has_after); } - } - - impl GeneralParameter for GeneralDependencyTestParameter { - fn as_parameter(&self) -> &dyn Parameter { - self + for (index, has_before, has_after) in [ + (&multi_before, true, false), + (&multi_after, false, true), + (&multi_both, true, true), + (&multi_hook, true, false), + ] { + assert_general_registration(index, has_before, has_after); } } - impl GeneralBeforeParameter for GeneralDependencyTestParameter { - fn before( - &self, - context: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - self.calculate(context) - } - } + #[test] + fn parameter_setup_initializes_state_for_all_kinds_and_types() { + let events = Arc::new(Mutex::new(Vec::new())); + let mut collection = ParameterCollection::default(); + + let f64_const_probe = TestParameter::::new("f64-const-state", events.clone()); + let f64_const_calls = f64_const_probe.setup_calls(); + let f64_const = expect_const_index(collection.push_const_f64(Box::new(f64_const_probe))); + let f64_simple_probe = TestParameter::::new("f64-simple-state", events.clone()); + let f64_simple_calls = f64_simple_probe.setup_calls(); + let f64_simple = expect_simple_index(collection.push_simple_f64(Box::new(f64_simple_probe))); + let f64_general_probe = TestParameter::::new("f64-general-state", events.clone()); + let f64_general_calls = f64_general_probe.setup_calls(); + let f64_general = + expect_general_registration(collection.push_general_f64(GeneralParameterEntry::before(f64_general_probe))); + + let u64_const_probe = TestParameter::::new("u64-const-state", events.clone()); + let u64_const_calls = u64_const_probe.setup_calls(); + let u64_const = expect_const_index(collection.push_const_u64(Box::new(u64_const_probe))); + let u64_simple_probe = TestParameter::::new("u64-simple-state", events.clone()); + let u64_simple_calls = u64_simple_probe.setup_calls(); + let u64_simple = expect_simple_index(collection.push_simple_u64(Box::new(u64_simple_probe))); + let u64_general_probe = TestParameter::::new("u64-general-state", events.clone()); + let u64_general_calls = u64_general_probe.setup_calls(); + let u64_general = + expect_general_registration(collection.push_general_u64(GeneralParameterEntry::before(u64_general_probe))); + + let multi_const_probe = TestParameter::::new("multi-const-state", events.clone()); + let multi_const_calls = multi_const_probe.setup_calls(); + let multi_const = expect_const_index(collection.push_const_multi(Box::new(multi_const_probe))); + let multi_simple_probe = TestParameter::::new("multi-simple-state", events.clone()); + let multi_simple_calls = multi_simple_probe.setup_calls(); + let multi_simple = expect_simple_index(collection.push_simple_multi(Box::new(multi_simple_probe))); + let multi_general_probe = TestParameter::::new("multi-general-state", events); + let multi_general_calls = multi_general_probe.setup_calls(); + let multi_general = expect_general_registration( + collection.push_general_multi(GeneralParameterEntry::before(multi_general_probe)), + ); + + let domain = default_domain(); + let timesteps = domain.time().timesteps(); + let scenario = ScenarioIndex::default(); + let mut states = ParameterStates::from_collection(&collection, timesteps, &scenario).unwrap(); + + assert_test_parameter_state( + states.get_const_f64_state(f64_const).unwrap(), + "f64-const-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_simple_f64_state(f64_simple).unwrap(), + "f64-simple-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_general_f64_state(f64_general.parameter).unwrap(), + "f64-general-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_const_mut_u64_state(u64_const).unwrap(), + "u64-const-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_simple_mut_u64_state(u64_simple).unwrap(), + "u64-simple-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_general_mut_u64_state(u64_general.parameter).unwrap(), + "u64-general-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_const_mut_multi_state(multi_const).unwrap(), + "multi-const-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_simple_mut_multi_state(multi_simple).unwrap(), + "multi-simple-state", + timesteps.len(), + 0, + 0, + ); + assert_test_parameter_state( + states.get_general_mut_multi_state(multi_general.parameter).unwrap(), + "multi-general-state", + timesteps.len(), + 0, + 0, + ); - impl GeneralAfterParameter for GeneralDependencyTestParameter { - fn after( - &self, - context: GeneralParameterContext<'_>, - _internal_state: &mut Option>, - ) -> Result { - self.calculate(context) + for calls in [ + f64_const_calls, + f64_simple_calls, + f64_general_calls, + u64_const_calls, + u64_simple_calls, + u64_general_calls, + multi_const_calls, + multi_simple_calls, + multi_general_calls, + ] { + assert_eq!(calls.load(Ordering::Relaxed), 1); } } - /// Extract a simple metric from the public ParameterIndex conversion API. - /// - /// Going through `ParameterIndex::into_metric_f64_*` is important for the - /// regression test: this is currently where the requested phase is discarded - /// for simple parameters. - fn expect_simple_metric(metric: MetricF64) -> SimpleMetricF64 { - match metric { - MetricF64::Simple(metric) => metric, - other => panic!("expected a simple metric, got {other:?}"), + #[test] + fn parameter_setup_error_reports_parameter_name_for_each_kind() { + pyo3::Python::initialize(); + for kind in [TestBuildKind::Const, TestBuildKind::Simple, TestBuildKind::General] { + let expected_name = format!("broken-{kind:?}"); + let probe = TestParameter::::new(&expected_name, Arc::new(Mutex::new(Vec::new()))) + .failing(TestParameterFailure::Setup); + let mut collection = ParameterCollection::default(); + match kind { + TestBuildKind::Const => { + collection.push_const_f64(Box::new(probe)); + } + TestBuildKind::Simple => { + collection.push_simple_f64(Box::new(probe)); + } + TestBuildKind::General => { + collection.push_general_f64(GeneralParameterEntry::before(probe)); + } + } + + let domain = default_domain(); + let error = match ParameterStates::from_collection( + &collection, + domain.time().timesteps(), + &ScenarioIndex::default(), + ) { + Err(error) => error, + Ok(_) => panic!("setup should fail"), + }; + let ParameterCollectionSetupError { name, source } = error; + assert_eq!(*name, ParameterName::from(expected_name.as_str())); + assert!(matches!(source.as_ref(), + ParameterSetupError::TestError(msg) if msg == "lifecycle-probe")); } } #[test] - fn simple_parameters_run() { + fn compute_const_covers_all_types_dependency_order_and_internal_state() { let events = Arc::new(Mutex::new(Vec::new())); let mut collection = ParameterCollection::default(); - - let before_index = simple_index( - collection.push_simple_f64(Box::new(PhaseTestParameter::new("simple-before", events.clone()))), + let f64_source = expect_const_index( + collection.push_const_f64(Box::new(TestParameter::::new("const-f64-source", events.clone()))), + ); + let u64_index = expect_const_index( + collection.push_const_u64(Box::new(TestParameter::::new("const-u64", events.clone()))), ); + let multi_index = expect_const_index(collection.push_const_multi(Box::new(TestParameter::::new( + "const-multi", + events.clone(), + )))); + let f64_dependent = expect_const_index(collection.push_const_f64(Box::new( + TestParameter::::new("const-f64-dependent", events.clone()).with_const_dependency(f64_source), + ))); let domain = default_domain(); let timesteps = domain.time().timesteps(); - let timestep = ×teps[0]; - let scenario_index = ScenarioIndex::default(); - + let scenario = ScenarioIndex::default(); + let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario).unwrap(); let mut state = StateBuilder::new(Vec::new(), 0).with_parameters(&collection).build(); - - let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario_index).unwrap(); - collection - .compute_simple(timestep, &scenario_index, &mut state, &mut internal_states) + .compute_const(&scenario, &mut state, &mut internal_states) .unwrap(); - // Only entries with a before implementation should have run. - assert_eq!(events.lock().unwrap().as_slice(), ["simple-before:before",]); - - let values = state.get_simple_parameter_values(); - assert_eq!( - values.get_f64(before_index, ParameterReturnValue::Before).unwrap(), - 11.0 + events.lock().unwrap().as_slice(), + [ + "const-f64-source:const", + "const-u64:const", + "const-multi:const", + "const-f64-dependent:const" + ] + ); + let values = state.get_const_parameter_values(); + assert_eq!(values.get_f64(f64_source).unwrap(), 11.0); + assert_eq!(values.get_f64(f64_dependent).unwrap(), 12.0); + assert_eq!(values.get_u64(u64_index).unwrap(), 12); + assert_eq!(values.get_multi_f64(multi_index, "value").unwrap(), 13.0); + assert_eq!(values.get_multi_u64(multi_index, "index").unwrap(), 14); + assert_test_parameter_state( + internal_states.get_const_f64_state(f64_source).unwrap(), + "const-f64-source", + timesteps.len(), + 0, + 1, ); + assert_test_parameter_state( + internal_states.get_const_f64_state(f64_dependent).unwrap(), + "const-f64-dependent", + timesteps.len(), + 0, + 1, + ); + assert_test_parameter_state( + internal_states.get_const_mut_u64_state(u64_index).unwrap(), + "const-u64", + timesteps.len(), + 0, + 1, + ); + assert_test_parameter_state( + internal_states.get_const_mut_multi_state(multi_index).unwrap(), + "const-multi", + timesteps.len(), + 0, + 1, + ); + } - assert_eq!(events.lock().unwrap().as_slice(), ["simple-before:before",]); + #[test] + fn compute_simple_covers_all_types_dependency_order_and_internal_state() { + let events = Arc::new(Mutex::new(Vec::new())); + let mut collection = ParameterCollection::default(); + let f64_source = expect_simple_index( + collection.push_simple_f64(Box::new(TestParameter::::new("simple-f64-source", events.clone()))), + ); + let u64_index = expect_simple_index( + collection.push_simple_u64(Box::new(TestParameter::::new("simple-u64", events.clone()))), + ); + let multi_index = expect_simple_index(collection.push_simple_multi(Box::new( + TestParameter::::new("simple-multi", events.clone()), + ))); + let f64_dependent = expect_simple_index(collection.push_simple_f64(Box::new( + TestParameter::::new("simple-f64-dependent", events.clone()).with_simple_dependency(f64_source), + ))); - // SimpleParameterValues exposes before-phase values. These should not be - // changed by after-phase calculations. - let values = state.get_simple_parameter_values(); + let domain = default_domain(); + let timesteps = domain.time().timesteps(); + let scenario = ScenarioIndex::default(); + let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario).unwrap(); + let mut state = StateBuilder::new(Vec::new(), 0).with_parameters(&collection).build(); + collection + .compute_simple(×teps[0], &scenario, &mut state, &mut internal_states) + .unwrap(); + collection + .compute_simple(×teps[1], &scenario, &mut state, &mut internal_states) + .unwrap(); assert_eq!( - values.get_f64(before_index, ParameterReturnValue::Before).unwrap(), - 11.0 + events.lock().unwrap().as_slice(), + [ + "simple-f64-source:simple", + "simple-u64:simple", + "simple-multi:simple", + "simple-f64-dependent:simple", + "simple-f64-source:simple", + "simple-u64:simple", + "simple-multi:simple", + "simple-f64-dependent:simple", + ] + ); + let values = state.get_simple_parameter_values(); + assert_eq!(values.get_f64(f64_source).unwrap(), 11.0); + assert_eq!(values.get_f64(f64_dependent).unwrap(), 12.0); + assert_eq!(values.get_u64(u64_index).unwrap(), 12); + assert_eq!(values.get_multi_f64(multi_index, "value").unwrap(), 13.0); + assert_eq!(values.get_multi_u64(multi_index, "index").unwrap(), 14); + assert_test_parameter_state( + internal_states.get_simple_f64_state(f64_source).unwrap(), + "simple-f64-source", + timesteps.len(), + 0, + 2, + ); + assert_test_parameter_state( + internal_states.get_simple_f64_state(f64_dependent).unwrap(), + "simple-f64-dependent", + timesteps.len(), + 0, + 2, + ); + assert_test_parameter_state( + internal_states.get_simple_mut_u64_state(u64_index).unwrap(), + "simple-u64", + timesteps.len(), + 0, + 2, + ); + assert_test_parameter_state( + internal_states.get_simple_mut_multi_state(multi_index).unwrap(), + "simple-multi", + timesteps.len(), + 0, + 2, ); } #[test] - fn general_parameters_run_their_configured_phases() { + fn compute_const_and_simple_wrap_calculation_errors_with_name() { let events = Arc::new(Mutex::new(Vec::new())); - let mut collection = ParameterCollection::default(); + let domain = default_domain(); + let timesteps = domain.time().timesteps(); + let scenario = ScenarioIndex::default(); + + let mut const_collection = ParameterCollection::default(); + const_collection.push_const_f64(Box::new( + TestParameter::::new("broken-const", events.clone()).failing(TestParameterFailure::Const), + )); + const_collection.push_const_f64(Box::new(TestParameter::::new("not-run-const", events.clone()))); + let mut const_states = ParameterStates::from_collection(&const_collection, timesteps, &scenario).unwrap(); + let mut const_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&const_collection) + .build(); + match const_collection.compute_const(&scenario, &mut const_state, &mut const_states) { + Err(ParameterCollectionConstCalculationError::CalculationError { name, source }) => { + assert_eq!(name, ParameterName::from("broken-const")); + assert!(matches!(source, ConstCalculationError::ConstantMetricF64Error(_))); + } + result => panic!("expected a constant calculation error, got {result:?}"), + } - let before_index = general_index(collection.push_general_f64(GeneralParameterEntry::before( - PhaseTestParameter::new("general-before", events.clone()), - ))); + let mut simple_collection = ParameterCollection::default(); + simple_collection.push_simple_f64(Box::new( + TestParameter::::new("broken-simple", events.clone()).failing(TestParameterFailure::Simple), + )); + simple_collection.push_simple_f64(Box::new(TestParameter::::new("not-run-simple", events.clone()))); + let mut simple_states = ParameterStates::from_collection(&simple_collection, timesteps, &scenario).unwrap(); + let mut simple_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&simple_collection) + .build(); + match simple_collection.compute_simple(×teps[0], &scenario, &mut simple_state, &mut simple_states) { + Err(ParameterCollectionSimpleCalculationError::CalculationError { name, source }) => { + assert_eq!(name, ParameterName::from("broken-simple")); + assert!(matches!( + source, + SimpleCalculationError::Internal { message } if message == "intentional simple failure" + )); + } + result => panic!("expected a simple calculation error, got {result:?}"), + } + assert!(events.lock().unwrap().is_empty()); + } - let after_index = general_index(collection.push_general_f64(GeneralParameterEntry::after( - PhaseTestParameter::new("general-after", events.clone()), - ))); + #[test] + fn compute_rejects_mismatched_parameter_states_and_model_state() { + let domain = default_domain(); + let timesteps = domain.time().timesteps(); + let scenario = ScenarioIndex::default(); + let timestep = ×teps[0]; - let both_index = general_index(collection.push_general_f64(GeneralParameterEntry::both( - PhaseTestParameter::new("general-both", events.clone()), + let mut f64_collection = ParameterCollection::default(); + let f64_index = expect_simple_index(f64_collection.push_simple_f64(Box::new(TestParameter::::new( + "simple-f64-mismatch", + Arc::new(Mutex::new(Vec::new())), + )))); + let empty_collection = ParameterCollection::default(); + let mut empty_states = ParameterStates::from_collection(&empty_collection, timesteps, &scenario).unwrap(); + let mut f64_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&f64_collection) + .build(); + assert!(matches!( + f64_collection.compute_simple(timestep, &scenario, &mut f64_state, &mut empty_states), + Err(ParameterCollectionSimpleCalculationError::F64IndexNotFound(index)) if index == f64_index + )); + + let mut u64_collection = ParameterCollection::default(); + let u64_index = expect_simple_index(u64_collection.push_simple_u64(Box::new(TestParameter::::new( + "simple-u64-mismatch", + Arc::new(Mutex::new(Vec::new())), + )))); + let mut empty_states = ParameterStates::from_collection(&empty_collection, timesteps, &scenario).unwrap(); + let mut u64_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&u64_collection) + .build(); + assert!(matches!( + u64_collection.compute_simple(timestep, &scenario, &mut u64_state, &mut empty_states), + Err(ParameterCollectionSimpleCalculationError::U64IndexNotFound(index)) if index == u64_index + )); + + let mut multi_collection = ParameterCollection::default(); + let multi_index = expect_simple_index(multi_collection.push_simple_multi(Box::new( + TestParameter::::new("simple-multi-mismatch", Arc::new(Mutex::new(Vec::new()))), ))); + let mut empty_states = ParameterStates::from_collection(&empty_collection, timesteps, &scenario).unwrap(); + let mut multi_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&multi_collection) + .build(); + assert!(matches!( + multi_collection.compute_simple(timestep, &scenario, &mut multi_state, &mut empty_states), + Err(ParameterCollectionSimpleCalculationError::MultiIndexNotFound(index)) if index == multi_index + )); + + let mut missing_internal = ParameterStates::from_collection(&f64_collection, timesteps, &scenario).unwrap(); + *missing_internal.get_simple_mut_f64_state(f64_index).unwrap() = None; + let mut correctly_sized_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&f64_collection) + .build(); + assert!(matches!( + f64_collection.compute_simple(timestep, &scenario, &mut correctly_sized_state, &mut missing_internal), + Err(ParameterCollectionSimpleCalculationError::CalculationError { name, source }) + if name == ParameterName::from("simple-f64-mismatch") + && matches!(&source, SimpleCalculationError::Internal { message } if message == "missing or invalid probe state") + )); + + let mut correct_internal = ParameterStates::from_collection(&f64_collection, timesteps, &scenario).unwrap(); + let mut empty_model_state = StateBuilder::new(Vec::new(), 0).build(); + assert!(matches!( + f64_collection.compute_simple(timestep, &scenario, &mut empty_model_state, &mut correct_internal), + Err(ParameterCollectionSimpleCalculationError::F64SetStateError { name, source }) + if name == ParameterName::from("simple-f64-mismatch") + && matches!(source, SetStateError::IndexNotFound(index) if index == f64_index) + )); + } - let hook_index = general_index( - collection.push_general_f64(GeneralParameterEntry::before_with_after_hook(PhaseTestParameter::new( - "general-hook", - events.clone(), - ))), - ); + #[test] + fn general_schedule_preserves_registration_order_across_types() { + let events = Arc::new(Mutex::new(Vec::new())); + let mut collection = ParameterCollection::default(); + let f64_before = expect_general_registration(collection.push_general_f64(GeneralParameterEntry::before( + TestParameter::::new("f64-before-order", events.clone()), + ))); + let u64_both = expect_general_registration(collection.push_general_u64(GeneralParameterEntry::both( + TestParameter::::new("u64-both-order", events.clone()), + ))); + let multi_after = expect_general_registration(collection.push_general_multi(GeneralParameterEntry::after( + TestParameter::::new("multi-after-order", events.clone()), + ))); + let multi_before = expect_general_registration(collection.push_general_multi(GeneralParameterEntry::before( + TestParameter::::new("multi-before-order", events.clone()), + ))); + let f64_both = expect_general_registration(collection.push_general_f64(GeneralParameterEntry::both( + TestParameter::::new("f64-both-order", events.clone()), + ))); + let u64_after = expect_general_registration(collection.push_general_u64(GeneralParameterEntry::after( + TestParameter::::new("u64-after-order", events.clone()), + ))); let domain = default_domain(); let timesteps = domain.time().timesteps(); - let timestep = ×teps[0]; - let scenario_index = ScenarioIndex::default(); + let scenario = ScenarioIndex::default(); let network = Network::default(); - + let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario).unwrap(); let mut state = StateBuilder::new(Vec::new(), 0).with_parameters(&collection).build(); - - let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario_index).unwrap(); - collection .before_general( - timestep, - &scenario_index, + ×teps[0], + &scenario, &network, &mut state, &mut internal_states, None, ) .unwrap(); - assert_eq!( events.lock().unwrap().as_slice(), - ["general-before:before", "general-both:before", "general-hook:before",] + [ + "f64-before-order:before", + "u64-both-order:before", + "multi-before-order:before", + "f64-both-order:before" + ] ); - assert_eq!( state - .get_general_parameter_value(before_index, ParameterReturnValue::Before,) + .get_general_parameter_f64_before(f64_before.before.unwrap()) .unwrap(), 11.0 ); assert_eq!( state - .get_general_parameter_value(after_index, ParameterReturnValue::Before,) + .get_general_parameter_u64_before(u64_both.before.unwrap()) .unwrap(), - 0.0 + 12 ); assert_eq!( state - .get_general_parameter_value(both_index, ParameterReturnValue::Before,) - .unwrap(), - 11.0 + .get_general_parameter_multi_before(multi_before.before.unwrap()) + .unwrap() + .get_value("value"), + Some(&13.0) ); assert_eq!( state - .get_general_parameter_value(hook_index, ParameterReturnValue::Before,) + .get_general_parameter_f64_before(f64_both.before.unwrap()) .unwrap(), 11.0 ); collection .after_general( - timestep, - &scenario_index, + ×teps[0], + &scenario, &network, &mut state, &mut internal_states, None, ) .unwrap(); - assert_eq!( events.lock().unwrap().as_slice(), [ - "general-before:before", - "general-both:before", - "general-hook:before", - "general-after:after", - "general-both:after", - "general-hook:hook", + "f64-before-order:before", + "u64-both-order:before", + "multi-before-order:before", + "f64-both-order:before", + "u64-both-order:after", + "multi-after-order:after", + "f64-both-order:after", + "u64-after-order:after", ] ); - - // Value-producing after operations write their result. assert_eq!( - state - .get_general_parameter_value(after_index, ParameterReturnValue::After,) - .unwrap(), - 22.0 + state.get_general_parameter_u64_after(u64_both.after.unwrap()).unwrap(), + 22 ); assert_eq!( state - .get_general_parameter_value(both_index, ParameterReturnValue::After,) - .unwrap(), - 22.0 + .get_general_parameter_multi_after(multi_after.after.unwrap()) + .unwrap() + .get_index("index"), + Some(&24) ); - - // Before-only entries and after hooks do not write an after value. assert_eq!( - state - .get_general_parameter_value(before_index, ParameterReturnValue::After,) - .unwrap(), - 0.0 + state.get_general_parameter_f64_after(f64_both.after.unwrap()).unwrap(), + 21.0 ); assert_eq!( - state - .get_general_parameter_value(hook_index, ParameterReturnValue::After,) - .unwrap(), - 0.0 + state.get_general_parameter_u64_after(u64_after.after.unwrap()).unwrap(), + 22 ); } #[test] - fn simple_before_parameter_can_depend_on_simple_before_value() { + fn general_before_and_after_share_internal_state() { let events = Arc::new(Mutex::new(Vec::new())); - let observed_values = Arc::new(Mutex::new(Vec::new())); let mut collection = ParameterCollection::default(); - - let source_index = collection.push_simple_f64(Box::new(PhaseTestParameter::new("source", events))); - - let dependency_metric = expect_simple_metric(source_index.into_metric_f64_before()); - - let dependent_index = simple_index(collection.push_simple_f64(Box::new(SimpleDependencyTestParameter::new( - "dependent", - dependency_metric, - observed_values.clone(), - )))); - + let registration = expect_general_registration(collection.push_general_f64(GeneralParameterEntry::both( + TestParameter::::new("shared-general-state", events.clone()), + ))); let domain = default_domain(); let timesteps = domain.time().timesteps(); - let timestep = ×teps[0]; - let scenario_index = ScenarioIndex::default(); - + let scenario = ScenarioIndex::default(); + let network = Network::default(); + let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario).unwrap(); let mut state = StateBuilder::new(Vec::new(), 0).with_parameters(&collection).build(); - let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario_index).unwrap(); - collection - .compute_simple(timestep, &scenario_index, &mut state, &mut internal_states) + .before_general( + ×teps[0], + &scenario, + &network, + &mut state, + &mut internal_states, + None, + ) + .unwrap(); + collection + .after_general( + ×teps[0], + &scenario, + &network, + &mut state, + &mut internal_states, + None, + ) .unwrap(); - // The source returns 11.0 and the dependent returns source * 2. - assert_eq!(observed_values.lock().unwrap().as_slice(), [11.0]); + assert_eq!( + events.lock().unwrap().as_slice(), + ["shared-general-state:before", "shared-general-state:after"] + ); assert_eq!( state - .get_simple_parameter_values() - .get_f64(dependent_index, ParameterReturnValue::Before) + .get_general_parameter_f64_before(registration.before.unwrap()) .unwrap(), - 22.0 + 11.0 + ); + assert_eq!( + state + .get_general_parameter_f64_after(registration.after.unwrap()) + .unwrap(), + 21.0 + ); + assert_test_parameter_state( + internal_states.get_general_f64_state(registration.parameter).unwrap(), + "shared-general-state", + timesteps.len(), + 0, + 2, ); } #[test] - fn general_before_parameter_can_depend_on_general_before_value() { - let events = Arc::new(Mutex::new(Vec::new())); - let observed_values = Arc::new(Mutex::new(Vec::new())); - let mut collection = ParameterCollection::default(); + fn general_calculation_errors_identify_before_after_and_hook_parameters() { + let domain = default_domain(); + let timesteps = domain.time().timesteps(); + let scenario = ScenarioIndex::default(); + let network = Network::default(); - let source_index = - collection.push_general_f64(GeneralParameterEntry::before(PhaseTestParameter::new("source", events))); + let mut before_collection = ParameterCollection::default(); + before_collection.push_general_f64(GeneralParameterEntry::before( + TestParameter::::new("broken-before", Arc::new(Mutex::new(Vec::new()))) + .failing(TestParameterFailure::GeneralBefore), + )); + let mut before_states = ParameterStates::from_collection(&before_collection, timesteps, &scenario).unwrap(); + let mut before_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&before_collection) + .build(); + let error = before_collection + .before_general( + ×teps[0], + &scenario, + &network, + &mut before_state, + &mut before_states, + None, + ) + .unwrap_err(); + assert_general_calculation_error(error, "broken-before", "intentional general before failure"); + + let mut after_collection = ParameterCollection::default(); + after_collection.push_general_f64(GeneralParameterEntry::after( + TestParameter::::new("broken-after", Arc::new(Mutex::new(Vec::new()))) + .failing(TestParameterFailure::GeneralAfter), + )); + let mut after_states = ParameterStates::from_collection(&after_collection, timesteps, &scenario).unwrap(); + let mut after_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&after_collection) + .build(); + let error = after_collection + .after_general( + ×teps[0], + &scenario, + &network, + &mut after_state, + &mut after_states, + None, + ) + .unwrap_err(); + assert_general_calculation_error(error, "broken-after", "intentional general after failure"); + + let mut hook_collection = ParameterCollection::default(); + let hook_registration = expect_general_registration( + hook_collection.push_general_f64(GeneralParameterEntry::before_with_after_hook( + TestParameter::::new("broken-hook", Arc::new(Mutex::new(Vec::new()))) + .failing(TestParameterFailure::GeneralHook), + )), + ); + assert!(hook_registration.after.is_none()); + let mut hook_states = ParameterStates::from_collection(&hook_collection, timesteps, &scenario).unwrap(); + let mut hook_state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&hook_collection) + .build(); + hook_collection + .before_general( + ×teps[0], + &scenario, + &network, + &mut hook_state, + &mut hook_states, + None, + ) + .unwrap(); + let error = hook_collection + .after_general( + ×teps[0], + &scenario, + &network, + &mut hook_state, + &mut hook_states, + None, + ) + .unwrap_err(); + assert_general_calculation_error(error, "broken-hook", "intentional general hook failure"); + } - let dependent_index = general_index(collection.push_general_f64(GeneralParameterEntry::before( - GeneralDependencyTestParameter::new( - "dependent", - source_index.into_metric_f64_before(), - observed_values.clone(), - ), + #[test] + fn timings_reject_another_parameter_collection() { + let mut source_collection = ParameterCollection::default(); + source_collection.push_general_f64(GeneralParameterEntry::both(TestParameter::::new( + "timing-source", + Arc::new(Mutex::new(Vec::new())), ))); + let mut other_collection = ParameterCollection::default(); + other_collection.push_general_f64(GeneralParameterEntry::both(TestParameter::::new( + "timing-other", + Arc::new(Mutex::new(Vec::new())), + ))); + let mut timings = ParameterTimings::from_collection(&source_collection); + let domain = default_domain(); + let timesteps = domain.time().timesteps(); + let scenario = ScenarioIndex::default(); + let network = Network::default(); + let mut internal_states = ParameterStates::from_collection(&other_collection, timesteps, &scenario).unwrap(); + let mut state = StateBuilder::new(Vec::new(), 0) + .with_parameters(&other_collection) + .build(); + + assert!(matches!( + other_collection.before_general( + ×teps[0], + &scenario, + &network, + &mut state, + &mut internal_states, + Some(&mut timings), + ), + Err(ParameterCollectionGeneralCalculationError::TimingsFromAnotherCollection) + )); + assert!(matches!( + other_collection.after_general( + ×teps[0], + &scenario, + &network, + &mut state, + &mut internal_states, + Some(&mut timings), + ), + Err(ParameterCollectionGeneralCalculationError::TimingsFromAnotherCollection) + )); + + let error = match timings.slowest_parameters_named(1, &other_collection) { + Err(error) => error, + Ok(_) => panic!("timings from another collection should be rejected"), + }; + assert_eq!(error.expected, other_collection.id); + assert_eq!(error.actual, source_collection.id); + assert!(error.context.contains("same ID")); + } + + #[test] + fn simple_parameters_run() { + let events = Arc::new(Mutex::new(Vec::new())); + let mut collection = ParameterCollection::default(); + + let before_index = expect_simple_index( + collection.push_simple_f64(Box::new(TestParameter::::phase("simple-before", events.clone()))), + ); let domain = default_domain(); let timesteps = domain.time().timesteps(); let timestep = ×teps[0]; let scenario_index = ScenarioIndex::default(); - let network = Network::default(); let mut state = StateBuilder::new(Vec::new(), 0).with_parameters(&collection).build(); let mut internal_states = ParameterStates::from_collection(&collection, timesteps, &scenario_index).unwrap(); collection - .before_general( - timestep, - &scenario_index, - &network, - &mut state, - &mut internal_states, - None, - ) + .compute_simple(timestep, &scenario_index, &mut state, &mut internal_states) .unwrap(); - assert_eq!(observed_values.lock().unwrap().as_slice(), [11.0]); - assert_eq!( - state - .get_general_parameter_value(dependent_index, ParameterReturnValue::Before,) - .unwrap(), - 22.0 - ); + // Only entries with a before implementation should have run. + assert_eq!(events.lock().unwrap().as_slice(), ["simple-before:before",]); + + let values = state.get_simple_parameter_values(); + + assert_eq!(values.get_f64(before_index).unwrap(), 11.0); + + assert_eq!(events.lock().unwrap().as_slice(), ["simple-before:before",]); + + // SimpleParameterValues exposes before-phase values. These should not be + // changed by after-phase calculations. + let values = state.get_simple_parameter_values(); + + assert_eq!(values.get_f64(before_index).unwrap(), 11.0); } #[test] - fn general_after_dependencies_respect_the_requested_phase() { + fn general_parameters_run_their_configured_phases() { let events = Arc::new(Mutex::new(Vec::new())); - let observed_before = Arc::new(Mutex::new(Vec::new())); - let observed_after = Arc::new(Mutex::new(Vec::new())); let mut collection = ParameterCollection::default(); - // This source produces 11.0 before and 22.0 after. - let source_index = - collection.push_general_f64(GeneralParameterEntry::both(PhaseTestParameter::new("source", events))); + let before_reg = expect_general_registration(collection.push_general_f64(GeneralParameterEntry::before( + TestParameter::::phase("general-before", events.clone()), + ))); - let dependent_on_before_index = general_index(collection.push_general_f64(GeneralParameterEntry::after( - GeneralDependencyTestParameter::new( - "dependent-on-before", - source_index.into_metric_f64_before(), - observed_before.clone(), - ), + let after_reg = expect_general_registration(collection.push_general_f64(GeneralParameterEntry::after( + TestParameter::::phase("general-after", events.clone()), ))); - let dependent_on_after_index = general_index(collection.push_general_f64(GeneralParameterEntry::after( - GeneralDependencyTestParameter::new( - "dependent-on-after", - source_index.into_metric_f64_after(), - observed_after.clone(), - ), + let both_index = expect_general_registration(collection.push_general_f64(GeneralParameterEntry::both( + TestParameter::::phase("general-both", events.clone()), ))); + let hook_reg = expect_general_registration(collection.push_general_f64( + GeneralParameterEntry::before_with_after_hook(TestParameter::::phase("general-hook", events.clone())), + )); + let domain = default_domain(); let timesteps = domain.time().timesteps(); let timestep = ×teps[0]; @@ -3599,6 +4602,33 @@ mod tests { ) .unwrap(); + assert_eq!( + events.lock().unwrap().as_slice(), + ["general-before:before", "general-both:before", "general-hook:before",] + ); + + assert_eq!( + state + .get_general_parameter_f64_before(before_reg.before.unwrap()) + .unwrap(), + 11.0 + ); + + assert_eq!( + state + .get_general_parameter_f64_before(both_index.before.unwrap()) + .unwrap(), + 11.0 + ); + assert_eq!( + state + .get_general_parameter_f64_before(hook_reg.before.unwrap()) + .unwrap(), + 11.0 + ); + // After-only entries should not be registered for the before phase. + assert!(after_reg.before.is_none()); + collection .after_general( timestep, @@ -3610,21 +4640,32 @@ mod tests { ) .unwrap(); - assert_eq!(observed_before.lock().unwrap().as_slice(), [11.0]); - assert_eq!(observed_after.lock().unwrap().as_slice(), [22.0]); + assert_eq!( + events.lock().unwrap().as_slice(), + [ + "general-before:before", + "general-both:before", + "general-hook:before", + "general-after:after", + "general-both:after", + "general-hook:hook", + ] + ); + // Value-producing after operations write their result. assert_eq!( - state - .get_general_parameter_value(dependent_on_before_index, ParameterReturnValue::After,) - .unwrap(), + state.get_general_parameter_f64_after(after_reg.after.unwrap()).unwrap(), 22.0 ); - assert_eq!( state - .get_general_parameter_value(dependent_on_after_index, ParameterReturnValue::After,) + .get_general_parameter_f64_after(both_index.after.unwrap()) .unwrap(), - 44.0 + 22.0 ); + + // Before-only entries and after hooks should not be registered for the after phase. + assert!(before_reg.after.is_none()); + assert!(hook_reg.after.is_none()); } } diff --git a/pywr-core/src/parameters/multi_threshold.rs b/pywr-core/src/parameters/multi_threshold.rs index 4c47a5c6..15b12c09 100644 --- a/pywr-core/src/parameters/multi_threshold.rs +++ b/pywr-core/src/parameters/multi_threshold.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ @@ -122,8 +122,10 @@ impl ParameterBuilder for MultiThresholdParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); - let thresholds = resolve_metric_f64_vec!(self, &self.thresholds, resolution_maps, "thresholds"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); + let thresholds = resolve_metric_f64_vec!(self, &self.thresholds, resolution_maps, phase, "thresholds"); let p = MultiThresholdParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/muskingum.rs b/pywr-core/src/parameters/muskingum.rs index 2120d84d..8f8e8906 100644 --- a/pywr-core/src/parameters/muskingum.rs +++ b/pywr-core/src/parameters/muskingum.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::{ BuiltParameter, GeneralBeforeParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, @@ -164,10 +164,12 @@ impl ParameterBuilder for MuskingumParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let inflow = resolve_metric_f64!(self, self.inflow, resolution_maps, phase, "inflow"); + let outflow = resolve_metric_f64!(self, self.outflow, resolution_maps, phase, "outflow"); + let travel_time = resolve_metric_f64!(self, self.travel_time, resolution_maps, phase, "travel_time"); + let weight = resolve_metric_f64!(self, self.weight, resolution_maps, phase, "weight"); let p = MuskingumParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/negative.rs b/pywr-core/src/parameters/negative.rs index 4644b52f..020761ba 100644 --- a/pywr-core/src/parameters/negative.rs +++ b/pywr-core/src/parameters/negative.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -65,7 +65,9 @@ impl ParameterBuilder for NegativeParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = NegativeParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/negativemax.rs b/pywr-core/src/parameters/negativemax.rs index df7b0375..698564da 100644 --- a/pywr-core/src/parameters/negativemax.rs +++ b/pywr-core/src/parameters/negativemax.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -66,7 +66,9 @@ impl ParameterBuilder for NegativeMaxParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = NegativeMaxParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/negativemin.rs b/pywr-core/src/parameters/negativemin.rs index 58c6badd..0659e00f 100644 --- a/pywr-core/src/parameters/negativemin.rs +++ b/pywr-core/src/parameters/negativemin.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -66,7 +66,9 @@ impl ParameterBuilder for NegativeMinParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = NegativeMinParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/offset.rs b/pywr-core/src/parameters/offset.rs index 8a75155b..f85d6731 100644 --- a/pywr-core/src/parameters/offset.rs +++ b/pywr-core/src/parameters/offset.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -136,7 +136,9 @@ impl ParameterBuilder for OffsetParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = OffsetParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/polynomial.rs b/pywr-core/src/parameters/polynomial.rs index 545a081f..b36e8dc5 100644 --- a/pywr-core/src/parameters/polynomial.rs +++ b/pywr-core/src/parameters/polynomial.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::GeneralCalculationError; use crate::parameters::{ @@ -91,7 +91,9 @@ impl ParameterBuilder for Polynomial1DParameterBuilder { self: Box, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); let p = Polynomial1DParameter { meta: self.meta, diff --git a/pywr-core/src/parameters/py.rs b/pywr-core/src/parameters/py.rs index 46ad3d05..20ff0fb0 100644 --- a/pywr-core/src/parameters/py.rs +++ b/pywr-core/src/parameters/py.rs @@ -3,7 +3,7 @@ use super::{ GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, ParameterName, ParameterState, Timestep, }; -use crate::metric::{MetricF64, MetricU64, UnresolvedMetricF64, UnresolvedMetricU64}; +use crate::metric::{MetricConsumerPhase, MetricF64, MetricU64, UnresolvedMetricF64, UnresolvedMetricU64}; use crate::network::{Network, ResolutionMaps}; use crate::parameters::downcast_internal_state_mut; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; @@ -390,8 +390,26 @@ impl ParameterBuilder for PyClassParameterBuilder { 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 (has_before, has_after) = Python::attach(|py| { + let has_before = self.class.getattr(py, "before").is_ok(); + let has_after = self.class.getattr(py, "after").is_ok(); + (has_before, has_after) + }); + + let phase = match (has_before, has_after) { + (true, true) => MetricConsumerPhase::Both, + (true, false) => MetricConsumerPhase::Before, + (false, true) => MetricConsumerPhase::After, + (false, false) => { + return Err(ParameterBuildError::NoCalculationPhase { + detail: "PyClassParameterBuilder must have at least one of `before` or `after` methods defined." + .into(), + }); + } + }; + + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, phase, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, phase, "indices"); let common = PyCommon { meta: self.common.meta, @@ -401,27 +419,15 @@ impl ParameterBuilder for PyClassParameterBuilder { indices, }; - let (has_before, has_after) = Python::attach(|py| { - let has_before = self.class.getattr(py, "before").is_ok(); - let has_after = self.class.getattr(py, "after").is_ok(); - (has_before, has_after) - }); - let p = PyClassParameter { class: self.class, common, }; - let entry = match (has_before, has_after) { - (true, true) => GeneralParameterEntry::both(p), - (true, false) => GeneralParameterEntry::before(p), - (false, true) => GeneralParameterEntry::after(p), - (false, false) => { - return Err(ParameterBuildError::NoCalculationPhase { - detail: "PyClassParameterBuilder must have at least one of `before` or `after` methods defined." - .into(), - }); - } + let entry = match phase { + MetricConsumerPhase::Both => GeneralParameterEntry::both(p), + MetricConsumerPhase::Before => GeneralParameterEntry::before(p), + MetricConsumerPhase::After => GeneralParameterEntry::after(p), }; Ok(BuiltParameter::General(entry).into()) @@ -437,8 +443,26 @@ impl ParameterBuilder for PyClassParameterBuilder { 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 (has_before, has_after) = Python::attach(|py| { + let has_before = self.class.getattr(py, "before").is_ok(); + let has_after = self.class.getattr(py, "after").is_ok(); + (has_before, has_after) + }); + + let phase = match (has_before, has_after) { + (true, true) => MetricConsumerPhase::Both, + (true, false) => MetricConsumerPhase::Before, + (false, true) => MetricConsumerPhase::After, + (false, false) => { + return Err(ParameterBuildError::NoCalculationPhase { + detail: "PyClassParameterBuilder must have at least one of `before` or `after` methods defined." + .into(), + }); + } + }; + + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, phase, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, phase, "indices"); let common = PyCommon { meta: self.common.meta, @@ -448,27 +472,15 @@ impl ParameterBuilder for PyClassParameterBuilder { indices, }; - let (has_before, has_after) = Python::attach(|py| { - let has_before = self.class.getattr(py, "before").is_ok(); - let has_after = self.class.getattr(py, "after").is_ok(); - (has_before, has_after) - }); - let p = PyClassParameter { class: self.class, common, }; - let entry = match (has_before, has_after) { - (true, true) => GeneralParameterEntry::both(p), - (true, false) => GeneralParameterEntry::before(p), - (false, true) => GeneralParameterEntry::after(p), - (false, false) => { - return Err(ParameterBuildError::NoCalculationPhase { - detail: "PyClassParameterBuilder must have at least one of `before` or `after` methods defined." - .into(), - }); - } + let entry = match phase { + MetricConsumerPhase::Both => GeneralParameterEntry::both(p), + MetricConsumerPhase::Before => GeneralParameterEntry::before(p), + MetricConsumerPhase::After => GeneralParameterEntry::after(p), }; Ok(BuiltParameter::General(entry).into()) @@ -484,8 +496,26 @@ impl ParameterBuilder for PyClassParameterBuilder { 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 (has_before, has_after) = Python::attach(|py| { + let has_before = self.class.getattr(py, "before").is_ok(); + let has_after = self.class.getattr(py, "after").is_ok(); + (has_before, has_after) + }); + + let phase = match (has_before, has_after) { + (true, true) => MetricConsumerPhase::Both, + (true, false) => MetricConsumerPhase::Before, + (false, true) => MetricConsumerPhase::After, + (false, false) => { + return Err(ParameterBuildError::NoCalculationPhase { + detail: "PyClassParameterBuilder must have at least one of `before` or `after` methods defined." + .into(), + }); + } + }; + + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, phase, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, phase, "indices"); let common = PyCommon { meta: self.common.meta, @@ -495,27 +525,15 @@ impl ParameterBuilder for PyClassParameterBuilder { indices, }; - let (has_before, has_after) = Python::attach(|py| { - let has_before = self.class.getattr(py, "before").is_ok(); - let has_after = self.class.getattr(py, "after").is_ok(); - (has_before, has_after) - }); - let p = PyClassParameter { class: self.class, common, }; - let entry = match (has_before, has_after) { - (true, true) => GeneralParameterEntry::both(p), - (true, false) => GeneralParameterEntry::before(p), - (false, true) => GeneralParameterEntry::after(p), - (false, false) => { - return Err(ParameterBuildError::NoCalculationPhase { - detail: "PyClassParameterBuilder must have at least one of `before` or `after` methods defined." - .into(), - }); - } + let entry = match phase { + MetricConsumerPhase::Both => GeneralParameterEntry::both(p), + MetricConsumerPhase::Before => GeneralParameterEntry::before(p), + MetricConsumerPhase::After => GeneralParameterEntry::after(p), }; Ok(BuiltParameter::General(entry).into()) @@ -723,8 +741,10 @@ impl ParameterBuilder for PyFuncParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, phase, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, phase, "indices"); let common = PyCommon { meta: self.common.meta, @@ -752,8 +772,10 @@ impl ParameterBuilder for PyFuncParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, phase, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, phase, "indices"); let common = PyCommon { meta: self.common.meta, @@ -781,8 +803,10 @@ impl ParameterBuilder for PyFuncParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metrics = resolve_metric_f64_hashmap!(self, &self.common.metrics, resolution_maps, phase, "metrics"); + let indices = resolve_metric_u64_hashmap!(self, &self.common.indices, resolution_maps, phase, "indices"); let common = PyCommon { meta: self.common.meta, diff --git a/pywr-core/src/parameters/rolling.rs b/pywr-core/src/parameters/rolling.rs index 5dc6757a..181f6b93 100644 --- a/pywr-core/src/parameters/rolling.rs +++ b/pywr-core/src/parameters/rolling.rs @@ -1,7 +1,7 @@ use crate::agg_funcs::{AggFuncF64, AggFuncU64}; use crate::metric::{ - MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64, UnresolvedMetricF64, - UnresolvedMetricU64, + MetricConsumerPhase, MetricF64, MetricF64Error, MetricU64, MetricU64Error, SimpleMetricF64, SimpleMetricU64, + UnresolvedMetricF64, UnresolvedMetricU64, }; use crate::network::ResolutionMaps; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError, SimpleCalculationError}; @@ -320,7 +320,10 @@ impl ParameterBuilder for RollingParameterBuilder, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_f64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "after" for this parameter, as it only uses the metric + // values in the after phase to update internal state. + let phase = MetricConsumerPhase::After; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); // We can make a simple version if the metric can be simplified if let Ok(metric) = metric.clone().try_into() { @@ -360,7 +363,10 @@ impl ParameterBuilder for RollingParameterBuilder, resolution_maps: &ResolutionMaps, ) -> Result, ParameterBuildError> { - let metric = resolve_metric_u64!(self, self.metric, resolution_maps, "metric"); + // Phase is hardcoded to "both" for this parameter, as it only implements the + // `GeneralBeforeParameter` and `GeneralAfterParameterHook` traits. + let phase = MetricConsumerPhase::Both; + let metric = resolve_metric_u64!(self, self.metric, resolution_maps, phase, "metric"); // We can make a simple version if the metric can be simplified if let Ok(metric) = metric.clone().try_into() { diff --git a/pywr-core/src/parameters/test_utils.rs b/pywr-core/src/parameters/test_utils.rs new file mode 100644 index 00000000..f4db8a0b --- /dev/null +++ b/pywr-core/src/parameters/test_utils.rs @@ -0,0 +1,569 @@ +use super::{ + BuiltParameter, ConstCalculationError, ConstParameter, ConstParameterIndex, GeneralAfterParameter, + GeneralAfterParameterHook, GeneralBeforeParameter, GeneralCalculationError, GeneralParameter, + GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, + ParameterBuilder, ParameterMeta, ParameterName, ParameterSetupError, ParameterState, SimpleCalculationError, + SimpleParameter, SimpleParameterContext, SimpleParameterIndex, +}; +use crate::metric::{ConstantMetricF64Error, SimpleMetricF64Error}; +use crate::network::ResolutionMaps; +use crate::scenario::ScenarioIndex; +use crate::state::{ConstParameterValues, ConstParameterValuesError, MultiValue, SimpleParameterValues}; +use crate::timestep::Timestep; +use std::collections::HashMap; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +pub(crate) type EventLog = Arc>>; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum TestBuildKind { + Const, + Simple, + General, +} + +#[derive(Debug, Clone, Copy)] +enum TestGeneralPhase { + Before, + Both, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum TestValueType { + F64, + U64, + Multi, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TestParameterFailure { + None, + Setup, + Const, + Simple, + GeneralBefore, + GeneralAfter, + GeneralHook, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TestParameterMode { + Default, + Phase, + NetworkLifecycle, + ScenarioCounter, +} + +pub(crate) trait TestValue: Debug + Send + Sync + 'static { + fn before_value(mode: TestParameterMode, state: Option<&TestParameterState>) -> Self; + fn after_value(mode: TestParameterMode) -> Self; + + fn const_value( + mode: TestParameterMode, + state: Option<&TestParameterState>, + _values: &ConstParameterValues<'_>, + _dependency: Option>, + ) -> Result + where + Self: Sized, + { + Ok(Self::before_value(mode, state)) + } + + fn simple_value( + mode: TestParameterMode, + state: Option<&TestParameterState>, + _values: &SimpleParameterValues<'_>, + _dependency: Option>, + ) -> Result + where + Self: Sized, + { + Ok(Self::before_value(mode, state)) + } +} + +impl TestValue for f64 { + fn before_value(mode: TestParameterMode, state: Option<&TestParameterState>) -> Self { + match mode { + TestParameterMode::Default | TestParameterMode::Phase => 11.0, + TestParameterMode::NetworkLifecycle => 30.0, + TestParameterMode::ScenarioCounter => { + let state = state.expect("scenario counter requires internal state"); + state.scenario_id as f64 * 100.0 + state.calls as f64 + } + } + } + + fn after_value(mode: TestParameterMode) -> Self { + match mode { + TestParameterMode::Default | TestParameterMode::ScenarioCounter => 21.0, + TestParameterMode::Phase => 22.0, + TestParameterMode::NetworkLifecycle => 40.0, + } + } + + fn const_value( + mode: TestParameterMode, + state: Option<&TestParameterState>, + values: &ConstParameterValues<'_>, + dependency: Option>, + ) -> Result { + dependency.map_or_else( + || Ok(Self::before_value(mode, state)), + |index| { + values.get_f64(index).map(|value| value + 1.0).map_err(|source| { + ConstCalculationError::ConstantMetricF64Error(ConstantMetricF64Error::ConstParameterValuesError( + source, + )) + }) + }, + ) + } + + fn simple_value( + mode: TestParameterMode, + state: Option<&TestParameterState>, + values: &SimpleParameterValues<'_>, + dependency: Option>, + ) -> Result { + dependency.map_or_else( + || Ok(Self::before_value(mode, state)), + |index| { + values.get_f64(index).map(|value| value + 1.0).map_err(|source| { + SimpleCalculationError::SimpleMetricF64Error(SimpleMetricF64Error::SimpleParameterValuesError( + source, + )) + }) + }, + ) + } +} + +impl TestValue for u64 { + fn before_value(_mode: TestParameterMode, _state: Option<&TestParameterState>) -> Self { + 12 + } + + fn after_value(_mode: TestParameterMode) -> Self { + 22 + } +} + +impl TestValue for MultiValue { + fn before_value(_mode: TestParameterMode, _state: Option<&TestParameterState>) -> Self { + MultiValue::new( + HashMap::from([("value".to_string(), 13.0)]), + HashMap::from([("index".to_string(), 14)]), + ) + } + + fn after_value(_mode: TestParameterMode) -> Self { + MultiValue::new( + HashMap::from([("value".to_string(), 23.0)]), + HashMap::from([("index".to_string(), 24)]), + ) + } +} + +#[derive(Debug)] +pub(crate) struct TestParameterState { + owner: String, + calls: usize, + timestep_count: usize, + scenario_id: usize, +} + +impl TestParameterState { + pub(crate) fn owner(&self) -> &str { + &self.owner + } + + pub(crate) fn calls(&self) -> usize { + self.calls + } + + pub(crate) fn timestep_count(&self) -> usize { + self.timestep_count + } + + pub(crate) fn scenario_id(&self) -> usize { + self.scenario_id + } +} + +pub(crate) fn test_parameter_state(state: &Option>) -> Option<&TestParameterState> { + state + .as_deref() + .and_then(|state| state.as_any().downcast_ref::()) +} + +#[derive(Debug)] +pub(crate) struct TestParameter { + meta: ParameterMeta, + events: Option, + setup_calls: Arc, + failure: TestParameterFailure, + state_enabled: bool, + record_setup: bool, + mode: TestParameterMode, + const_dependency: Option>, + simple_dependency: Option>, + phantom: PhantomData, +} + +impl TestParameter { + pub(crate) fn named(name: &str) -> Self { + Self { + meta: ParameterMeta::new(name.into()), + events: None, + setup_calls: Arc::new(AtomicUsize::new(0)), + failure: TestParameterFailure::None, + state_enabled: false, + record_setup: false, + mode: TestParameterMode::Default, + const_dependency: None, + simple_dependency: None, + phantom: PhantomData, + } + } + + pub(crate) fn new(name: &str, events: EventLog) -> Self { + Self::named(name).with_events(events).with_state() + } + + pub(crate) fn phase(name: &str, events: EventLog) -> Self { + let mut parameter = Self::named(name).with_events(events); + parameter.mode = TestParameterMode::Phase; + parameter + } + + pub(crate) fn network_lifecycle(name: &str, events: EventLog) -> Self { + let mut parameter = Self::new(name, events); + parameter.mode = TestParameterMode::NetworkLifecycle; + parameter.record_setup = true; + parameter + } + + pub(crate) fn scenario_counter(name: &str) -> Self { + let mut parameter = Self::named(name).with_state(); + parameter.mode = TestParameterMode::ScenarioCounter; + parameter + } + + pub(crate) fn with_events(mut self, events: EventLog) -> Self { + self.events = Some(events); + self + } + + pub(crate) fn with_state(mut self) -> Self { + self.state_enabled = true; + self + } + + pub(crate) fn failing(mut self, failure: TestParameterFailure) -> Self { + self.failure = failure; + self + } + + pub(crate) fn with_const_dependency(mut self, dependency: ConstParameterIndex) -> Self { + self.const_dependency = Some(dependency); + self + } + + pub(crate) fn with_simple_dependency(mut self, dependency: SimpleParameterIndex) -> Self { + self.simple_dependency = Some(dependency); + self + } + + pub(crate) fn setup_calls(&self) -> Arc { + self.setup_calls.clone() + } + + fn record(&self, phase: &str) { + if let Some(events) = &self.events { + events.lock().unwrap().push(format!("{}:{phase}", self.meta.name)); + } + } + + fn state_mut<'a>( + &self, + internal_state: &'a mut Option>, + scenario_id: usize, + ) -> Result, String> { + if !self.state_enabled { + return Ok(None); + } + let state = internal_state + .as_deref_mut() + .and_then(|state| state.as_any_mut().downcast_mut::()) + .ok_or_else(|| "missing or invalid probe state".to_string())?; + if state.owner != self.meta.name.to_string() { + return Err("probe state belongs to another parameter".to_string()); + } + if state.scenario_id != scenario_id { + return Err("probe state belongs to another scenario".to_string()); + } + state.calls += 1; + Ok(Some(state)) + } +} + +impl Parameter for TestParameter { + fn meta(&self) -> &ParameterMeta { + &self.meta + } + + fn setup( + &self, + timesteps: &[Timestep], + scenario_index: &ScenarioIndex, + ) -> Result>, ParameterSetupError> { + self.setup_calls.fetch_add(1, Ordering::Relaxed); + if self.failure == TestParameterFailure::Setup { + return Err(ParameterSetupError::TestError("lifecycle-probe".to_string())); + } + if self.record_setup { + self.record("setup"); + } + Ok(self.state_enabled.then(|| { + Box::new(TestParameterState { + owner: self.meta.name.to_string(), + calls: 0, + timestep_count: timesteps.len(), + scenario_id: scenario_index.simulation_id(), + }) as Box + })) + } +} + +fn intentional_const_error() -> ConstCalculationError { + ConstCalculationError::ConstantMetricF64Error(ConstantMetricF64Error::ConstParameterValuesError( + ConstParameterValuesError::ConstParameterIndexNotFound(ConstParameterIndex::new(usize::MAX)), + )) +} + +impl ConstParameter for TestParameter { + fn compute( + &self, + scenario_index: &ScenarioIndex, + values: &ConstParameterValues, + internal_state: &mut Option>, + ) -> Result { + if self.failure == TestParameterFailure::Const { + return Err(intentional_const_error()); + } + let state = self + .state_mut(internal_state, scenario_index.simulation_id()) + .map_err(|_| intentional_const_error())?; + self.record("const"); + T::const_value(self.mode, state.as_deref(), values, self.const_dependency) + } + + fn as_parameter(&self) -> &dyn Parameter { + self + } +} + +impl SimpleParameter for TestParameter { + fn compute( + &self, + context: SimpleParameterContext<'_>, + internal_state: &mut Option>, + ) -> Result { + if self.failure == TestParameterFailure::Simple { + return Err(SimpleCalculationError::Internal { + message: "intentional simple failure".to_string(), + }); + } + let state = self + .state_mut(internal_state, context.scenario_index.simulation_id()) + .map_err(|message| SimpleCalculationError::Internal { message })?; + self.record(if self.mode == TestParameterMode::Phase { + "before" + } else { + "simple" + }); + T::simple_value(self.mode, state.as_deref(), context.values, self.simple_dependency) + } + + fn as_parameter(&self) -> &dyn Parameter { + self + } +} + +impl GeneralParameter for TestParameter { + fn as_parameter(&self) -> &dyn Parameter { + self + } +} + +impl GeneralBeforeParameter for TestParameter { + fn before( + &self, + context: GeneralParameterContext<'_>, + internal_state: &mut Option>, + ) -> Result { + if self.failure == TestParameterFailure::GeneralBefore { + return Err(GeneralCalculationError::Internal { + message: "intentional general before failure".to_string(), + }); + } + let state = self + .state_mut(internal_state, context.scenario_index.simulation_id()) + .map_err(|message| GeneralCalculationError::Internal { message })?; + self.record("before"); + Ok(T::before_value(self.mode, state.as_deref())) + } +} + +impl GeneralAfterParameter for TestParameter { + fn after( + &self, + context: GeneralParameterContext<'_>, + internal_state: &mut Option>, + ) -> Result { + if self.failure == TestParameterFailure::GeneralAfter { + return Err(GeneralCalculationError::Internal { + message: "intentional general after failure".to_string(), + }); + } + self.state_mut(internal_state, context.scenario_index.simulation_id()) + .map_err(|message| GeneralCalculationError::Internal { message })?; + self.record("after"); + Ok(T::after_value(self.mode)) + } +} + +impl GeneralAfterParameterHook for TestParameter { + fn after( + &self, + context: GeneralParameterContext<'_>, + internal_state: &mut Option>, + ) -> Result<(), GeneralCalculationError> { + if self.failure == TestParameterFailure::GeneralHook { + return Err(GeneralCalculationError::Internal { + message: "intentional general hook failure".to_string(), + }); + } + self.state_mut(internal_state, context.scenario_index.simulation_id()) + .map_err(|message| GeneralCalculationError::Internal { message })?; + self.record("hook"); + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct TestParameterBuilder { + meta: ParameterMeta, + kind: TestBuildKind, + general_phase: TestGeneralPhase, + dependency: Option, + build_error: Option, + attempts: Arc, + build_order: EventLog, + events: Option, + mode: TestParameterMode, +} + +impl Default for TestParameterBuilder { + fn default() -> Self { + Self::new("test-parameter", TestBuildKind::Const) + } +} + +impl TestParameterBuilder { + pub(crate) fn new(name: &str, kind: TestBuildKind) -> Self { + Self::with_build_order(name, kind, Arc::new(Mutex::new(Vec::new()))) + } + + pub(crate) fn with_build_order(name: &str, kind: TestBuildKind, build_order: EventLog) -> Self { + Self { + meta: ParameterMeta::new(name.into()), + kind, + general_phase: TestGeneralPhase::Before, + dependency: None, + build_error: None, + attempts: Arc::new(AtomicUsize::new(0)), + build_order, + events: None, + mode: TestParameterMode::Default, + } + } + + pub(crate) fn network_lifecycle(name: &str, events: EventLog) -> Self { + let mut builder = Self::new(name, TestBuildKind::General); + builder.general_phase = TestGeneralPhase::Both; + builder.events = Some(events); + builder.mode = TestParameterMode::NetworkLifecycle; + builder + } + + pub(crate) fn scenario_counter(name: &str) -> Self { + let mut builder = Self::new(name, TestBuildKind::General); + builder.mode = TestParameterMode::ScenarioCounter; + builder + } + + pub(crate) fn depending_on(mut self, dependency: &str) -> Self { + self.dependency = Some(dependency.into()); + self + } + + pub(crate) fn failing(mut self, detail: &str) -> Self { + self.build_error = Some(detail.to_string()); + self + } + + pub(crate) fn attempts(&self) -> Arc { + self.attempts.clone() + } + + fn parameter(&self) -> TestParameter { + match self.mode { + TestParameterMode::Default | TestParameterMode::Phase => TestParameter::named(&self.meta.name.to_string()), + TestParameterMode::NetworkLifecycle => { + TestParameter::network_lifecycle(&self.meta.name.to_string(), self.events.as_ref().unwrap().clone()) + } + TestParameterMode::ScenarioCounter => TestParameter::scenario_counter(&self.meta.name.to_string()), + } + } +} + +impl ParameterBuilder for TestParameterBuilder { + fn name(&self) -> &ParameterName { + &self.meta.name + } + + fn build(self: Box, resolution_maps: &ResolutionMaps) -> Result, ParameterBuildError> { + self.attempts.fetch_add(1, Ordering::Relaxed); + if let Some(detail) = &self.build_error { + return Err(ParameterBuildError::NoCalculationPhase { detail: detail.clone() }); + } + if let Some(dependency) = &self.dependency + && !resolution_maps.parameters_f64.contains_key(dependency) + && !resolution_maps.parameters_u64.contains_key(dependency) + && !resolution_maps.parameters_multi.contains_key(dependency) + { + return Ok(MaybeBuiltParameter::Retry { + parameter_not_found: dependency.clone(), + builder: self, + }); + } + + self.build_order.lock().unwrap().push(self.meta.name.to_string()); + let parameter = self.parameter::(); + let built = match self.kind { + TestBuildKind::Const => BuiltParameter::Const(Box::new(parameter)), + TestBuildKind::Simple => BuiltParameter::Simple(Box::new(parameter)), + TestBuildKind::General => BuiltParameter::General(match self.general_phase { + TestGeneralPhase::Before => GeneralParameterEntry::before(parameter), + TestGeneralPhase::Both => GeneralParameterEntry::both(parameter), + }), + }; + Ok(MaybeBuiltParameter::Built(built)) + } +} diff --git a/pywr-core/src/parameters/threshold.rs b/pywr-core/src/parameters/threshold.rs index 997b1eff..2a426089 100644 --- a/pywr-core/src/parameters/threshold.rs +++ b/pywr-core/src/parameters/threshold.rs @@ -1,5 +1,5 @@ use crate::FLOAT_EQ_TOLERANCE; -use crate::metric::{MetricF64, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64}; use crate::network::ResolutionMaps; use crate::parameters::errors::{GeneralCalculationError, ParameterSetupError}; use crate::parameters::{ @@ -140,8 +140,10 @@ impl ParameterBuilder for ThresholdParameterBuilder { 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"); + // Phase is hardcoded to "before" for this parameter, as it only implements the `GeneralBeforeParameter` trait. + let phase = MetricConsumerPhase::Before; + let metric = resolve_metric_f64!(self, self.metric, resolution_maps, phase, "metric"); + let threshold = resolve_metric_f64!(self, self.threshold, resolution_maps, phase, "threshold"); let p = ThresholdParameter { meta: self.meta, diff --git a/pywr-core/src/recorders/metric_set.rs b/pywr-core/src/recorders/metric_set.rs index 6182d8aa..360d13c5 100644 --- a/pywr-core/src/recorders/metric_set.rs +++ b/pywr-core/src/recorders/metric_set.rs @@ -1,4 +1,4 @@ -use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; +use crate::metric::{MetricConsumerPhase, MetricF64, MetricF64Error, MetricF64ResolutionError, UnresolvedMetricF64}; use crate::network::{Network, ResolutionMaps}; use crate::recorders::aggregator::{Aggregator, AggregatorState, PeriodValue}; use crate::scenario::ScenarioIndex; @@ -65,7 +65,7 @@ impl UnresolvedOutputMetric { } pub fn resolve(self, resolution_maps: &ResolutionMaps) -> Result { - let metric = self.metric.resolve(resolution_maps)?; + let metric = self.metric.resolve(resolution_maps, MetricConsumerPhase::After)?; Ok(OutputMetric { name: self.name, attribute: self.attribute, diff --git a/pywr-core/src/recorders/mod.rs b/pywr-core/src/recorders/mod.rs index 07b097b8..39c84a0d 100644 --- a/pywr-core/src/recorders/mod.rs +++ b/pywr-core/src/recorders/mod.rs @@ -8,8 +8,8 @@ mod metric_set; mod py; use crate::metric::{ - MetricF64, MetricF64Error, MetricF64ResolutionError, MetricU64, MetricU64Error, MetricU64ResolutionError, - UnresolvedMetricF64, UnresolvedMetricU64, + MetricConsumerPhase, MetricF64, MetricF64Error, MetricF64ResolutionError, MetricU64, MetricU64Error, + MetricU64ResolutionError, UnresolvedMetricF64, UnresolvedMetricU64, }; use crate::models::ModelDomain; use crate::network::{MetricSetIndex, Network, ResolutionMaps}; @@ -303,13 +303,13 @@ impl RecorderBuilder for Array2RecorderBuilder { 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 metric = self + .metric + .resolve(resolution_maps, MetricConsumerPhase::After) + .map_err(|source| RecorderBuilderError::ResolveMetricF64Error { + attr: "metric".to_string(), + source, + })?; let r = Array2Recorder { meta: self.meta, @@ -420,13 +420,13 @@ impl RecorderBuilder for AssertionF64RecorderBuilder { &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 metric = self + .metric + .resolve(resolution_maps, MetricConsumerPhase::After) + .map_err(|source| RecorderBuilderError::ResolveMetricF64Error { + attr: "metric".to_string(), + source, + })?; let r = AssertionF64Recorder { meta: self.meta, @@ -518,13 +518,13 @@ impl RecorderBuilder for AssertionU64RecorderBuilder { &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 metric = self + .metric + .resolve(resolution_maps, MetricConsumerPhase::After) + .map_err(|source| RecorderBuilderError::ResolveMetricU64Error { + attr: "metric".to_string(), + source, + })?; let r = AssertionU64Recorder { meta: self.meta, @@ -649,13 +649,13 @@ where &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 metric = self + .metric + .resolve(resolution_maps, MetricConsumerPhase::After) + .map_err(|source| RecorderBuilderError::ResolveMetricF64Error { + attr: "metric".to_string(), + source, + })?; let r = AssertionFnRecorder { meta: self.meta, diff --git a/pywr-core/src/state/mod.rs b/pywr-core/src/state/mod.rs index 1ba90082..7ea58c03 100644 --- a/pywr-core/src/state/mod.rs +++ b/pywr-core/src/state/mod.rs @@ -7,7 +7,8 @@ use crate::models::MultiNetworkTransferIndex; use crate::network::{EdgeIndex, Network, NodeIndex, VirtualStorageIndex}; use crate::node::Node; use crate::parameters::{ - ConstParameterIndex, GeneralParameterIndex, ParameterCollection, ParameterCollectionSize, SimpleParameterIndex, + ConstParameterIndex, GeneralAfterValueIndex, GeneralBeforeValueIndex, ParameterCollection, + ParameterCollectionStateSize, SimpleParameterIndex, }; use crate::timestep::Timestep; use flow::FlowState; @@ -281,22 +282,6 @@ impl ParameterValues { } } -#[derive(Debug, Clone)] -pub struct ParameterValuesCollection { - simple: ParameterValues, - general: ParameterValues, -} - -impl ParameterValuesCollection { - fn get_simple_parameter_values(&self) -> ParameterValuesRef<'_> { - ParameterValuesRef { - values: &self.simple.values, - indices: &self.simple.indices, - multi_values: &self.simple.multi_values, - } - } -} - #[derive(Default)] pub struct ParameterValuesRef<'a> { values: &'a [f64], @@ -337,69 +322,18 @@ pub enum SimpleParameterValuesError { pub struct SimpleParameterValues<'a> { constant: ConstParameterValues<'a>, - before: ParameterValuesRef<'a>, - after: ParameterValuesRef<'a>, + simple: ParameterValuesRef<'a>, } impl SimpleParameterValues<'_> { - pub fn get_f64( - &self, - idx: SimpleParameterIndex, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - ParameterReturnValue::Before => self.get_f64_before(idx), - ParameterReturnValue::After => self.get_f64_after(idx), - ParameterReturnValue::BeforeOrElseAfter => match self.get_f64_before(idx) { - Ok(v) => Ok(v), - Err(_) => self.get_f64_after(idx), - }, - ParameterReturnValue::AfterOrElseBefore => match self.get_f64_after(idx) { - Ok(v) => Ok(v), - Err(_) => self.get_f64_before(idx), - }, - } - } - - fn get_f64_before(&self, idx: SimpleParameterIndex) -> Result { - self.before + pub fn get_f64(&self, idx: SimpleParameterIndex) -> Result { + self.simple .get_value(*idx.deref()) .ok_or(SimpleParameterValuesError::SimpleParameterIndexNotFound(idx)) } - fn get_f64_after(&self, idx: SimpleParameterIndex) -> Result { - self.after - .get_value(*idx.deref()) - .ok_or(SimpleParameterValuesError::SimpleParameterIndexNotFound(idx)) - } - - pub fn get_u64( - &self, - idx: SimpleParameterIndex, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - ParameterReturnValue::Before => self.get_u64_before(idx), - ParameterReturnValue::After => self.get_u64_after(idx), - ParameterReturnValue::BeforeOrElseAfter => match self.get_u64_before(idx) { - Ok(v) => Ok(v), - Err(_) => self.get_u64_after(idx), - }, - ParameterReturnValue::AfterOrElseBefore => match self.get_u64_after(idx) { - Ok(v) => Ok(v), - Err(_) => self.get_u64_before(idx), - }, - } - } - - fn get_u64_before(&self, idx: SimpleParameterIndex) -> Result { - self.before - .get_index(*idx.deref()) - .ok_or(SimpleParameterValuesError::SimpleIndexParameterIndexNotFound(idx)) - } - - fn get_u64_after(&self, idx: SimpleParameterIndex) -> Result { - self.after + pub fn get_u64(&self, idx: SimpleParameterIndex) -> Result { + self.simple .get_index(*idx.deref()) .ok_or(SimpleParameterValuesError::SimpleIndexParameterIndexNotFound(idx)) } @@ -408,47 +342,9 @@ impl SimpleParameterValues<'_> { &self, idx: SimpleParameterIndex, key: &str, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - ParameterReturnValue::Before => self.get_multi_f64_before(idx, key), - ParameterReturnValue::After => self.get_multi_f64_after(idx, key), - ParameterReturnValue::BeforeOrElseAfter => match self.get_multi_f64_before(idx, key) { - Ok(v) => Ok(v), - Err(_) => self.get_multi_f64_after(idx, key), - }, - ParameterReturnValue::AfterOrElseBefore => match self.get_multi_f64_after(idx, key) { - Ok(v) => Ok(v), - Err(_) => self.get_multi_f64_before(idx, key), - }, - } - } - - fn get_multi_f64_before( - &self, - idx: SimpleParameterIndex, - key: &str, ) -> Result { let mv = self - .before - .get_multi_value(*idx.deref()) - .ok_or(SimpleParameterValuesError::SimpleMultiValueParameterIndexNotFound(idx))?; - - mv.get_value(key) - .ok_or_else(|| SimpleParameterValuesError::SimpleMultiValueParameterKeyNotFound { - index: idx, - key: key.to_string(), - }) - .copied() - } - - fn get_multi_f64_after( - &self, - idx: SimpleParameterIndex, - key: &str, - ) -> Result { - let mv = self - .after + .simple .get_multi_value(*idx.deref()) .ok_or(SimpleParameterValuesError::SimpleMultiValueParameterIndexNotFound(idx))?; @@ -464,47 +360,9 @@ impl SimpleParameterValues<'_> { &self, idx: SimpleParameterIndex, key: &str, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - ParameterReturnValue::Before => self.get_multi_u64_before(idx, key), - ParameterReturnValue::After => self.get_multi_u64_after(idx, key), - ParameterReturnValue::BeforeOrElseAfter => match self.get_multi_u64_before(idx, key) { - Ok(v) => Ok(v), - Err(_) => self.get_multi_u64_after(idx, key), - }, - ParameterReturnValue::AfterOrElseBefore => match self.get_multi_u64_after(idx, key) { - Ok(v) => Ok(v), - Err(_) => self.get_multi_u64_before(idx, key), - }, - } - } - - fn get_multi_u64_before( - &self, - idx: SimpleParameterIndex, - key: &str, - ) -> Result { - let mv = self - .before - .get_multi_value(*idx.deref()) - .ok_or(SimpleParameterValuesError::SimpleMultiValueParameterIndexNotFound(idx))?; - - mv.get_index(key) - .ok_or_else(|| SimpleParameterValuesError::SimpleMultiValueParameterKeyNotFound { - index: idx, - key: key.to_string(), - }) - .copied() - } - - fn get_multi_u64_after( - &self, - idx: SimpleParameterIndex, - key: &str, ) -> Result { let mv = self - .after + .simple .get_multi_value(*idx.deref()) .ok_or(SimpleParameterValuesError::SimpleMultiValueParameterIndexNotFound(idx))?; @@ -885,18 +743,18 @@ impl NetworkState { #[derive(Error, Debug)] pub enum StateError { - #[error("General parameter index not found: {0}")] - GeneralParameterIndexNotFound(GeneralParameterIndex), - #[error("General index parameter index not found: {0}")] - GeneralIndexParameterIndexNotFound(GeneralParameterIndex), - #[error("General parameter index not found: {0}")] - GeneralMultiValueParameterIndexNotFound(GeneralParameterIndex), - #[error("General parameter with index {index} has no key: {key}")] - GeneralMultiValueParameterKeyNotFound { - index: GeneralParameterIndex, - key: String, - }, - + #[error("General f64 parameter before value not found: {0}")] + GeneralF64ParameterBeforeValueNotFound(GeneralBeforeValueIndex), + #[error("General f64 parameter after value not found: {0}")] + GeneralF64ParameterAfterValueNotFound(GeneralAfterValueIndex), + #[error("General u64 parameter before value not found: {0}")] + GeneralU64ParameterBeforeValueNotFound(GeneralBeforeValueIndex), + #[error("General u64 parameter after value not found: {0}")] + GeneralU64ParameterAfterValueNotFound(GeneralAfterValueIndex), + #[error("General multi parameter before value not found: {0}")] + GeneralMultiParameterBeforeValueNotFound(GeneralBeforeValueIndex), + #[error("General multi parameter after value not found: {0}")] + GeneralMultiParameterAfterValueNotFound(GeneralAfterValueIndex), #[error("Multi-network transfer index not found: {0}")] MultiNetworkTransferIndexNotFound(MultiNetworkTransferIndex), #[error("Network state error: {0}")] @@ -913,15 +771,6 @@ pub enum SetStateError { NaNValue(I), } -/// Specifies whether to use the 'before' or 'after' parameter values. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ParameterReturnValue { - Before, - After, - BeforeOrElseAfter, - AfterOrElseBefore, -} - /// State of the model simulation. /// /// This struct contains the state of the model simulation at a given point in time. The state @@ -936,10 +785,13 @@ pub struct State { network: NetworkState, // Constant parameter values that do not change during the simulation parameters_constant: ParameterValues, + // Simple parameter values calculated before the current time-step's solve + parameters_simple: ParameterValues, // Parameter values calculated before the current time-step's solve - parameters_before: ParameterValuesCollection, + parameters_general_before: ParameterValues, // Parameter values calculated after the current time-step's solve - parameters_after: ParameterValuesCollection, + parameters_general_after: ParameterValues, + inter_network_values: Vec, } @@ -954,48 +806,26 @@ impl State { &mut self.network } - pub fn get_general_parameter_value( - &self, - idx: GeneralParameterIndex, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - 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_general_parameter_value_after(idx), - }, - ParameterReturnValue::AfterOrElseBefore => match self.get_general_parameter_value_after(idx) { - Ok(v) => Ok(v), - Err(_) => self.get_general_parameter_value_before(idx), - }, - } - } - - fn get_general_parameter_value_before(&self, idx: GeneralParameterIndex) -> Result { - self.parameters_before - .general + pub fn get_general_parameter_f64_before(&self, idx: GeneralBeforeValueIndex) -> Result { + self.parameters_general_before .get_value(*idx) - .ok_or(StateError::GeneralParameterIndexNotFound(idx)) + .ok_or(StateError::GeneralF64ParameterBeforeValueNotFound(idx)) } - fn get_general_parameter_value_after(&self, idx: GeneralParameterIndex) -> Result { - self.parameters_after - .general + pub fn get_general_parameter_f64_after(&self, idx: GeneralAfterValueIndex) -> Result { + self.parameters_general_after .get_value(*idx) - .ok_or(StateError::GeneralParameterIndexNotFound(idx)) + .ok_or(StateError::GeneralF64ParameterAfterValueNotFound(idx)) } /// Set the "before" value of a general parameter. - pub fn set_general_parameter_value_before( + pub fn set_general_parameter_f64_before( &mut self, - idx: GeneralParameterIndex, + idx: GeneralBeforeValueIndex, value: f64, - ) -> Result<(), SetStateError>> { + ) -> Result<(), SetStateError>> { let v = self - .parameters_before - .general + .parameters_general_before .get_value_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1009,34 +839,13 @@ impl State { } /// Set the "after" value of a general parameter. - pub fn set_general_parameter_value_after( - &mut self, - idx: GeneralParameterIndex, - value: f64, - ) -> Result<(), SetStateError>> { - let v = self - .parameters_after - .general - .get_value_mut(*idx) - .ok_or(SetStateError::IndexNotFound(idx))?; - - if value.is_nan() { - return Err(SetStateError::NaNValue(idx)); - } - - *v = value; - - Ok(()) - } - - pub fn set_simple_parameter_value_before( + pub fn set_general_parameter_f64_after( &mut self, - idx: SimpleParameterIndex, + idx: GeneralAfterValueIndex, value: f64, - ) -> Result<(), SetStateError>> { + ) -> Result<(), SetStateError>> { let v = self - .parameters_before - .simple + .parameters_general_after .get_value_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1049,14 +858,13 @@ impl State { Ok(()) } - pub fn set_simple_parameter_value_after( + pub fn set_simple_parameter_f64( &mut self, idx: SimpleParameterIndex, value: f64, ) -> Result<(), SetStateError>> { let v = self - .parameters_after - .simple + .parameters_simple .get_value_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1069,7 +877,7 @@ impl State { Ok(()) } - pub fn set_const_parameter_value( + pub fn set_const_parameter_f64( &mut self, idx: ConstParameterIndex, value: f64, @@ -1088,47 +896,25 @@ impl State { Ok(()) } - pub fn get_general_parameter_index( - &self, - idx: GeneralParameterIndex, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - 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_general_parameter_index_after(idx), - }, - ParameterReturnValue::AfterOrElseBefore => match self.get_general_parameter_index_after(idx) { - Ok(v) => Ok(v), - Err(_) => self.get_general_parameter_index_before(idx), - }, - } - } - - fn get_general_parameter_index_before(&self, idx: GeneralParameterIndex) -> Result { - self.parameters_before - .general + pub fn get_general_parameter_u64_before(&self, idx: GeneralBeforeValueIndex) -> Result { + self.parameters_general_before .get_index(*idx) - .ok_or(StateError::GeneralIndexParameterIndexNotFound(idx)) + .ok_or(StateError::GeneralU64ParameterBeforeValueNotFound(idx)) } - fn get_general_parameter_index_after(&self, idx: GeneralParameterIndex) -> Result { - self.parameters_after - .general + pub fn get_general_parameter_u64_after(&self, idx: GeneralAfterValueIndex) -> Result { + self.parameters_general_after .get_index(*idx) - .ok_or(StateError::GeneralIndexParameterIndexNotFound(idx)) + .ok_or(StateError::GeneralU64ParameterAfterValueNotFound(idx)) } - pub fn set_general_parameter_index_before( + pub fn set_general_parameter_u64_before( &mut self, - idx: GeneralParameterIndex, + idx: GeneralBeforeValueIndex, value: u64, - ) -> Result<(), SetStateError>> { + ) -> Result<(), SetStateError>> { let v = self - .parameters_before - .general + .parameters_general_before .get_index_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1137,14 +923,13 @@ impl State { Ok(()) } - pub fn set_general_parameter_index_after( + pub fn set_general_parameter_u64_after( &mut self, - idx: GeneralParameterIndex, + idx: GeneralAfterValueIndex, value: u64, - ) -> Result<(), SetStateError>> { + ) -> Result<(), SetStateError>> { let v = self - .parameters_after - .general + .parameters_general_after .get_index_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1153,14 +938,13 @@ impl State { Ok(()) } - pub fn set_simple_parameter_index_before( + pub fn set_simple_parameter_u64( &mut self, idx: SimpleParameterIndex, value: u64, ) -> Result<(), SetStateError>> { let v = self - .parameters_before - .simple + .parameters_simple .get_index_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1169,23 +953,7 @@ impl State { Ok(()) } - pub fn set_simple_parameter_index_after( - &mut self, - idx: SimpleParameterIndex, - value: u64, - ) -> Result<(), SetStateError>> { - let v = self - .parameters_after - .simple - .get_index_mut(*idx) - .ok_or(SetStateError::IndexNotFound(idx))?; - - *v = value; - - Ok(()) - } - - pub fn set_const_parameter_index( + pub fn set_const_parameter_u64( &mut self, idx: ConstParameterIndex, value: u64, @@ -1200,149 +968,31 @@ impl State { Ok(()) } - pub fn get_general_multi_parameter_value( + pub fn get_general_parameter_multi_before( &self, - idx: GeneralParameterIndex, - key: &str, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - 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_general_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_general_multi_parameter_value_before(idx, key), - }, - } - } - fn get_general_multi_parameter_value_before( - &self, - idx: GeneralParameterIndex, - key: &str, - ) -> Result { - let mv = self - .parameters_before - .general + idx: GeneralBeforeValueIndex, + ) -> Result<&MultiValue, StateError> { + self.parameters_general_before .get_multi_value(*idx) - .ok_or(StateError::GeneralMultiValueParameterIndexNotFound(idx))?; - - mv.get_value(key) - .ok_or_else(|| StateError::GeneralMultiValueParameterKeyNotFound { - index: idx, - key: key.to_string(), - }) - .copied() + .ok_or(StateError::GeneralMultiParameterBeforeValueNotFound(idx)) } - fn get_general_multi_parameter_value_after( + pub fn get_general_parameter_multi_after( &self, - idx: GeneralParameterIndex, - key: &str, - ) -> Result { - let mv = self - .parameters_after - .general + idx: GeneralAfterValueIndex, + ) -> Result<&MultiValue, StateError> { + self.parameters_general_after .get_multi_value(*idx) - .ok_or(StateError::GeneralMultiValueParameterIndexNotFound(idx))?; - - mv.get_value(key) - .ok_or_else(|| StateError::GeneralMultiValueParameterKeyNotFound { - index: idx, - key: key.to_string(), - }) - .copied() + .ok_or(StateError::GeneralMultiParameterAfterValueNotFound(idx)) } - pub fn get_general_multi_parameter_index( - &self, - idx: GeneralParameterIndex, - key: &str, - return_value: ParameterReturnValue, - ) -> Result { - match return_value { - 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_general_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_general_multi_parameter_index_before(idx, key), - }, - } - } - - fn get_general_multi_parameter_index_before( - &self, - idx: GeneralParameterIndex, - key: &str, - ) -> Result { - let mv = self - .parameters_before - .general - .get_multi_value(*idx) - .ok_or(StateError::GeneralMultiValueParameterIndexNotFound(idx))?; - - mv.get_index(key) - .ok_or_else(|| StateError::GeneralMultiValueParameterKeyNotFound { - index: idx, - key: key.to_string(), - }) - .copied() - } - - fn get_general_multi_parameter_index_after( - &self, - idx: GeneralParameterIndex, - key: &str, - ) -> Result { - let mv = self - .parameters_after - .general - .get_multi_value(*idx) - .ok_or(StateError::GeneralMultiValueParameterIndexNotFound(idx))?; - - mv.get_index(key) - .ok_or_else(|| StateError::GeneralMultiValueParameterKeyNotFound { - index: idx, - key: key.to_string(), - }) - .copied() - } - - pub fn set_general_multi_parameter_value_before( - &mut self, - idx: GeneralParameterIndex, - value: MultiValue, - ) -> Result<(), SetStateError>> { - let mv = self - .parameters_before - .general - .get_multi_value_mut(*idx) - .ok_or(SetStateError::IndexNotFound(idx))?; - - if value.has_nan() { - return Err(SetStateError::NaNValue(idx)); - } - - *mv = value; - - Ok(()) - } - - pub fn set_general_multi_parameter_value_after( + pub fn set_general_parameter_multi_before( &mut self, - idx: GeneralParameterIndex, + idx: GeneralBeforeValueIndex, value: MultiValue, - ) -> Result<(), SetStateError>> { + ) -> Result<(), SetStateError>> { let mv = self - .parameters_after - .general + .parameters_general_before .get_multi_value_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1355,14 +1005,13 @@ impl State { Ok(()) } - pub fn set_simple_multi_parameter_value_before( + pub fn set_general_parameter_multi_after( &mut self, - idx: SimpleParameterIndex, + idx: GeneralAfterValueIndex, value: MultiValue, - ) -> Result<(), SetStateError>> { + ) -> Result<(), SetStateError>> { let mv = self - .parameters_before - .simple + .parameters_general_after .get_multi_value_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1375,14 +1024,13 @@ impl State { Ok(()) } - pub fn set_simple_multi_parameter_value_after( + pub fn set_simple_parameter_multi( &mut self, idx: SimpleParameterIndex, value: MultiValue, ) -> Result<(), SetStateError>> { let mv = self - .parameters_after - .simple + .parameters_simple .get_multi_value_mut(*idx) .ok_or(SetStateError::IndexNotFound(idx))?; @@ -1395,7 +1043,7 @@ impl State { Ok(()) } - pub fn set_const_multi_parameter_value( + pub fn set_const_parameter_multi( &mut self, idx: ConstParameterIndex, value: MultiValue, @@ -1417,8 +1065,11 @@ impl State { pub fn get_simple_parameter_values(&self) -> SimpleParameterValues<'_> { SimpleParameterValues { constant: self.get_const_parameter_values(), - before: self.parameters_before.get_simple_parameter_values(), - after: self.parameters_after.get_simple_parameter_values(), + simple: ParameterValuesRef { + values: &self.parameters_simple.values, + indices: &self.parameters_simple.indices, + multi_values: &self.parameters_simple.multi_values, + }, } } @@ -1526,7 +1177,7 @@ pub struct StateBuilder { initial_node_states: Vec, num_edges: usize, initial_virtual_storage_states: Option>, - num_parameters: Option, + num_parameters: Option, num_derived_metrics: Option, num_inter_network_values: Option, } @@ -1575,24 +1226,29 @@ impl StateBuilder { /// Build the [`State`] from the builder. pub fn build(self) -> State { - let constant = ParameterValues::new( + let parameters_constant = ParameterValues::new( self.num_parameters.map(|s| s.const_f64).unwrap_or(0), - self.num_parameters.map(|s| s.const_usize).unwrap_or(0), + self.num_parameters.map(|s| s.const_u64).unwrap_or(0), self.num_parameters.map(|s| s.const_multi).unwrap_or(0), ); - let simple = ParameterValues::new( + let parameters_simple = ParameterValues::new( self.num_parameters.map(|s| s.simple_f64).unwrap_or(0), - self.num_parameters.map(|s| s.simple_usize).unwrap_or(0), + self.num_parameters.map(|s| s.simple_u64).unwrap_or(0), self.num_parameters.map(|s| s.simple_multi).unwrap_or(0), ); - let general = ParameterValues::new( - self.num_parameters.map(|s| s.general_f64).unwrap_or(0), - self.num_parameters.map(|s| s.general_usize).unwrap_or(0), - self.num_parameters.map(|s| s.general_multi).unwrap_or(0), + + let parameters_general_before = ParameterValues::new( + self.num_parameters.map(|s| s.general_before_f64).unwrap_or(0), + self.num_parameters.map(|s| s.general_before_u64).unwrap_or(0), + self.num_parameters.map(|s| s.general_before_multi).unwrap_or(0), ); - let parameters = ParameterValuesCollection { simple, general }; + let parameters_general_after = ParameterValues::new( + self.num_parameters.map(|s| s.general_after_f64).unwrap_or(0), + self.num_parameters.map(|s| s.general_after_u64).unwrap_or(0), + self.num_parameters.map(|s| s.general_after_multi).unwrap_or(0), + ); State { network: NetworkState::new( @@ -1600,9 +1256,10 @@ impl StateBuilder { self.num_edges, self.initial_virtual_storage_states.unwrap_or_default(), ), - parameters_constant: constant, - parameters_before: parameters.clone(), - parameters_after: parameters, + parameters_constant, + parameters_simple, + parameters_general_before, + parameters_general_after, inter_network_values: vec![0.0; self.num_inter_network_values.unwrap_or(0)], } } diff --git a/pywr-core/src/test_utils.rs b/pywr-core/src/test_utils.rs index 4c5b4875..cd1b3faf 100644 --- a/pywr-core/src/test_utils.rs +++ b/pywr-core/src/test_utils.rs @@ -91,7 +91,7 @@ pub fn simple_network(builder: &mut NetworkBuilder, inflow_scenario: &str, num_i let demand_factor = ConstantParameterBuilder::new("demand-factor".into(), 1.2); builder.parameters().f64(Box::new(demand_factor)); - let mut total_demand = AggregatedParameterBuilder::new("total-demand".into(), AggFuncF64::Product); + let mut total_demand = AggregatedParameterBuilder::before("total-demand".into(), AggFuncF64::Product); total_demand.metric(base_demand.into()); total_demand.metric(UnresolvedMetricF64::new_parameter_before("demand-factor")); diff --git a/pywr-core/src/virtual_storage.rs b/pywr-core/src/virtual_storage.rs index d61158fc..3500d06d 100644 --- a/pywr-core/src/virtual_storage.rs +++ b/pywr-core/src/virtual_storage.rs @@ -1,6 +1,8 @@ use crate::NodeIndex; use crate::aggregated_node::RelationshipBuildError; -use crate::metric::{MetricF64, MetricF64Error, MetricF64ResolutionError, SimpleMetricF64Error, UnresolvedMetricF64}; +use crate::metric::{ + MetricConsumerPhase, 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}; @@ -134,7 +136,7 @@ impl VirtualStorageNodeBuilder { .as_ref() .map(|min_volume| { min_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { attr: "min_volume".to_string(), source, @@ -152,7 +154,7 @@ impl VirtualStorageNodeBuilder { .as_ref() .map(|max_volume| { max_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { attr: "max_volume".to_string(), source, @@ -182,7 +184,7 @@ impl VirtualStorageNodeBuilder { prior_max_volume, } => { let prior_max_volume = prior_max_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { attr: "prior_max_volume".to_string(), source, @@ -203,7 +205,7 @@ impl VirtualStorageNodeBuilder { prior_max_volume, } => { let total_volume = total_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { attr: "total_volume".to_string(), source, @@ -214,7 +216,7 @@ impl VirtualStorageNodeBuilder { source, })?; let prior_max_volume = prior_max_volume - .resolve(resolution_maps) + .resolve(resolution_maps, MetricConsumerPhase::Before) .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { attr: "prior_max_volume".to_string(), source, @@ -261,7 +263,7 @@ impl VirtualStorageNodeBuilder { let cost = self .cost .as_ref() - .map(|cost| cost.resolve(resolution_maps)) + .map(|cost| cost.resolve(resolution_maps, MetricConsumerPhase::Before)) .transpose() .map_err(|source| VirtualStorageNodeBuilderError::ResolveMetricF64Error { attr: "cost".to_string(), @@ -728,7 +730,7 @@ mod tests { network_builder.virtual_storage_node(vs_builder); // Virtual storage node cost increases with decreasing volume - let mut cost_param = ControlCurveInterpolatedParameterBuilder::new( + let mut cost_param = ControlCurveInterpolatedParameterBuilder::before( "cost".into(), UnresolvedMetricF64::VirtualStorageProportionalVolume("vs".into()), ); diff --git a/pywr-schema/src/metric.rs b/pywr-schema/src/metric.rs index 5c4d97f5..86ba2772 100644 --- a/pywr-schema/src/metric.rs +++ b/pywr-schema/src/metric.rs @@ -479,18 +479,16 @@ pub enum ParameterReturnValue { #[default] Before, After, - BeforeOrElseAfter, - AfterOrElseBefore, + AfterOrElseInitial, } #[cfg(feature = "core")] -impl From for pywr_core::state::ParameterReturnValue { +impl From for pywr_core::parameters::ParameterReturnValue { fn from(v: ParameterReturnValue) -> Self { match v { ParameterReturnValue::Before => Self::Before, ParameterReturnValue::After => Self::After, - ParameterReturnValue::BeforeOrElseAfter => Self::BeforeOrElseAfter, - ParameterReturnValue::AfterOrElseBefore => Self::AfterOrElseBefore, + ParameterReturnValue::AfterOrElseInitial => Self::AfterOrElseInitial, } } } @@ -509,14 +507,6 @@ pub struct ParameterReference { } impl ParameterReference { - pub fn new(name: &str, key: Option) -> Self { - Self { - name: name.to_string(), - key, - return_value: None, - } - } - /// Load a parameter reference into a [`MetricF64`] by attempting to retrieve the parameter /// from the `network`. If `parent` is the optional parameter name space from which to load /// the parameter. @@ -582,6 +572,41 @@ impl ParameterReference { } } +/// A builder for creating a [`ParameterReference`]. +pub struct ParameterReferenceBuilder { + name: String, + key: Option, + return_value: Option, +} + +impl ParameterReferenceBuilder { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + key: None, + return_value: None, + } + } + + pub fn key(&mut self, key: &str) -> &mut Self { + self.key = Some(key.to_string()); + self + } + + pub fn return_value(&mut self, return_value: ParameterReturnValue) -> &mut Self { + self.return_value = Some(return_value); + self + } + + pub fn build(self) -> ParameterReference { + ParameterReference { + name: self.name, + key: self.key, + return_value: self.return_value, + } + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] #[cfg_attr(feature = "pyo3", pyclass(from_py_object))] diff --git a/pywr-schema/src/metric_sets/mod.rs b/pywr-schema/src/metric_sets/mod.rs index aed10227..1c3062c2 100644 --- a/pywr-schema/src/metric_sets/mod.rs +++ b/pywr-schema/src/metric_sets/mod.rs @@ -3,11 +3,11 @@ use crate::agg_funcs::AggFunc; use crate::error::SchemaError; use crate::metric::Metric; #[cfg(feature = "core")] -use crate::metric::{EdgeReference, VirtualNodeAttrReference}; +use crate::metric::{EdgeReference, ParameterReferenceBuilder, ParameterReturnValue, VirtualNodeAttrReference}; #[cfg(feature = "core")] use crate::network::LoadArgs; #[cfg(feature = "core")] -use crate::parameters::{Parameter, PythonReturnType}; +use crate::parameters::{Parameter, ParameterPhase, PythonReturnType}; #[cfg(feature = "core")] use pywr_core::recorders::UnresolvedOutputMetric; use pywr_schema_macros::skip_serializing_none; @@ -92,7 +92,7 @@ pub struct MetricSetFilters { #[cfg(feature = "core")] impl MetricSetFilters { fn create_metrics(&self, args: &LoadArgs) -> Vec { - use crate::metric::{NodeAttrReference, ParameterReference}; + use crate::metric::NodeAttrReference; let mut metrics = vec![]; @@ -124,7 +124,25 @@ impl MetricSetFilters { } } - metrics.push(Metric::Parameter(ParameterReference::new(parameter.name(), None))); + // Make sure we create a reference to the correct phase(s) that the parameter + // will produce a value in. + let (add_before, add_after) = match parameter.phase() { + ParameterPhase::Before => (true, false), + ParameterPhase::After => (false, true), + ParameterPhase::Both => (true, true), + }; + + if add_before { + let mut p_ref_builder = ParameterReferenceBuilder::new(parameter.name()); + p_ref_builder.return_value(ParameterReturnValue::Before); + metrics.push(Metric::Parameter(p_ref_builder.build())); + } + + if add_after { + let mut p_ref_builder = ParameterReferenceBuilder::new(parameter.name()); + p_ref_builder.return_value(ParameterReturnValue::After); + metrics.push(Metric::Parameter(p_ref_builder.build())); + } } } } diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index 2ebd6723..a954b29f 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -1178,7 +1178,7 @@ mod core_tests { use super::{ModelSchema, MultiNetworkModelSchema}; use crate::agg_funcs::AggFunc; use crate::metric::{Metric, ParameterReference}; - use crate::parameters::{AggregatedParameter, ConstantParameter, Parameter, ParameterMeta}; + use crate::parameters::{AggregatedParameter, ConstantParameter, Parameter, ParameterMeta, ParameterPhase}; use ndarray::{Array1, Array2, Axis}; use pywr_core::metric::UnresolvedMetricF64; use pywr_core::recorders::AssertionF64RecorderBuilder; @@ -1233,6 +1233,7 @@ mod core_tests { tags: Default::default(), }, agg_func: AggFunc::Sum, + phase: ParameterPhase::Before, metrics: vec![ Metric::Parameter(ParameterReference { name: "p1".to_string(), @@ -1262,6 +1263,7 @@ mod core_tests { tags: Default::default(), }, agg_func: AggFunc::Sum, + phase: ParameterPhase::Before, metrics: vec![ Metric::Parameter(ParameterReference { name: "p1".to_string(), @@ -1298,6 +1300,7 @@ mod core_tests { tags: Default::default(), }, agg_func: AggFunc::Sum, + phase: ParameterPhase::Before, metrics: vec![ Metric::Parameter(ParameterReference { name: "p1".to_string(), diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 44cc5586..a148426f 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -781,7 +781,7 @@ impl OutputNode { network.parameters().f64(Box::new(deficit_builder)); } - UnresolvedMetricF64::new_parameter_after(deficit_parameter_name) + UnresolvedMetricF64::new_parameter_after_else_initial(deficit_parameter_name) } }; diff --git a/pywr-schema/src/nodes/piecewise_storage.rs b/pywr-schema/src/nodes/piecewise_storage.rs index 8a5b8829..a4162732 100644 --- a/pywr-schema/src/nodes/piecewise_storage.rs +++ b/pywr-schema/src/nodes/piecewise_storage.rs @@ -160,7 +160,7 @@ impl PiecewiseStorageNode { 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( + let mut prior_max_volume = pywr_core::parameters::AggregatedParameterBuilder::before( prior_max_volume_name.clone(), pywr_core::agg_funcs::AggFuncF64::Sum, ); @@ -234,7 +234,7 @@ impl PiecewiseStorageNode { Some(&self.meta.name), ); - let mut prior_max_volume = pywr_core::parameters::AggregatedParameterBuilder::new( + let mut prior_max_volume = pywr_core::parameters::AggregatedParameterBuilder::before( prior_max_volume_name.clone(), pywr_core::agg_funcs::AggFuncF64::Sum, ); diff --git a/pywr-schema/src/nodes/reservoir.rs b/pywr-schema/src/nodes/reservoir.rs index bc41509f..8d9c642e 100644 --- a/pywr-schema/src/nodes/reservoir.rs +++ b/pywr-schema/src/nodes/reservoir.rs @@ -431,7 +431,7 @@ impl ReservoirNode { let rainfall_metric = rainfall.data.load(network, args, Some(&self.meta().name))?; let rainfall_flow_parameter_name = ParameterName::new("rainfall", Some(self.meta().name.as_str())); - let mut rainfall_flow_parameter = pywr_core::parameters::AggregatedParameterBuilder::new( + let mut rainfall_flow_parameter = pywr_core::parameters::AggregatedParameterBuilder::before( rainfall_flow_parameter_name.clone(), AggFuncF64::Product, ); @@ -464,7 +464,7 @@ impl ReservoirNode { let evaporation_metric = evaporation.data.load(network, args, Some(&self.meta().name))?; let evaporation_flow_parameter_name = ParameterName::new("evaporation", Some(self.meta().name.as_str())); - let mut evaporation_flow_parameter = pywr_core::parameters::AggregatedParameterBuilder::new( + let mut evaporation_flow_parameter = pywr_core::parameters::AggregatedParameterBuilder::before( evaporation_flow_parameter_name.clone(), AggFuncF64::Product, ); diff --git a/pywr-schema/src/parameters/aggregated.rs b/pywr-schema/src/parameters/aggregated.rs index c6cb7dd8..1536d512 100644 --- a/pywr-schema/src/parameters/aggregated.rs +++ b/pywr-schema/src/parameters/aggregated.rs @@ -5,7 +5,7 @@ use crate::error::SchemaError; use crate::metric::{IndexMetric, Metric}; #[cfg(feature = "core")] use crate::network::LoadArgs; -use crate::parameters::{ConversionData, ParameterMeta}; +use crate::parameters::{ConversionData, ParameterMeta, ParameterPhase}; use crate::v1::{TryFromV1, TryIntoV2, try_convert_parameter_attr}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterName; @@ -43,6 +43,7 @@ use std::collections::HashMap; #[serde(deny_unknown_fields)] pub struct AggregatedParameter { pub meta: ParameterMeta, + pub phase: ParameterPhase, pub agg_func: AggFunc, pub metrics: Vec, } @@ -55,10 +56,14 @@ impl AggregatedParameter { args: &LoadArgs, parent: Option<&str>, ) -> Result<(), SchemaError> { - let mut builder = pywr_core::parameters::AggregatedParameterBuilder::new( - ParameterName::new(&self.meta.name, parent), - self.agg_func.load(args.data_path)?, - ); + let name = ParameterName::new(&self.meta.name, parent); + let agg_func = self.agg_func.load(args.data_path)?; + + let mut builder = match self.phase { + ParameterPhase::Before => pywr_core::parameters::AggregatedParameterBuilder::before(name, agg_func), + ParameterPhase::After => pywr_core::parameters::AggregatedParameterBuilder::after(name, agg_func), + ParameterPhase::Both => pywr_core::parameters::AggregatedParameterBuilder::both(name, agg_func), + }; for metric in &self.metrics { let m = metric.load(network, args, parent)?; @@ -91,6 +96,7 @@ impl TryFromV1 for AggregatedParameter { meta, agg_func: v1.agg_func.into(), metrics, + phase: ParameterPhase::Before, }; Ok(p) } @@ -100,6 +106,7 @@ impl TryFromV1 for AggregatedParameter { #[serde(deny_unknown_fields)] pub struct AggregatedIndexParameter { pub meta: ParameterMeta, + pub phase: ParameterPhase, pub agg_func: IndexAggFunc, pub metrics: Vec, } @@ -108,15 +115,6 @@ impl AggregatedIndexParameter { pub fn node_references(&self) -> HashMap<&str, &str> { HashMap::new() } - - // pub fn parameters(&self) -> HashMap<&str, DynamicFloatValueType> { - // let mut attributes = HashMap::new(); - // - // let parameters = &self.parameters; - // attributes.insert("parameters", parameters.into()); - // - // attributes - // } } #[cfg(feature = "core")] @@ -127,10 +125,14 @@ impl AggregatedIndexParameter { args: &LoadArgs, parent: Option<&str>, ) -> Result<(), SchemaError> { - let mut builder = pywr_core::parameters::AggregatedIndexParameterBuilder::new( - ParameterName::new(&self.meta.name, parent), - self.agg_func.load(args.data_path)?, - ); + let name = ParameterName::new(&self.meta.name, parent); + let agg_func = self.agg_func.load(args.data_path)?; + + let mut builder = match self.phase { + ParameterPhase::Before => pywr_core::parameters::AggregatedIndexParameterBuilder::before(name, agg_func), + ParameterPhase::After => pywr_core::parameters::AggregatedIndexParameterBuilder::after(name, agg_func), + ParameterPhase::Both => pywr_core::parameters::AggregatedIndexParameterBuilder::both(name, agg_func), + }; for metric in &self.metrics { let m = metric.load(network, args, parent)?; @@ -161,6 +163,7 @@ impl TryFromV1 for AggregatedIndexParameter { let p = Self { meta, + phase: ParameterPhase::Before, agg_func: v1.agg_func.into(), metrics, }; @@ -184,6 +187,7 @@ mod tests { "agg_func": { "type": "Min" }, + "phase": "Before", "metrics": [ { "type": "Parameter", diff --git a/pywr-schema/src/parameters/asymmetric_switch.rs b/pywr-schema/src/parameters/asymmetric_switch.rs index 3be1a238..135d7861 100644 --- a/pywr-schema/src/parameters/asymmetric_switch.rs +++ b/pywr-schema/src/parameters/asymmetric_switch.rs @@ -32,7 +32,7 @@ impl AsymmetricSwitchIndexParameter { 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::AsymmetricSwitchIndexParameterBuilder::new( + let p = pywr_core::parameters::AsymmetricSwitchIndexParameterBuilder::before( ParameterName::new(&self.meta.name, parent), on_index_parameter, off_index_parameter, diff --git a/pywr-schema/src/parameters/control_curves.rs b/pywr-schema/src/parameters/control_curves.rs index 6354327e..5ac028f0 100644 --- a/pywr-schema/src/parameters/control_curves.rs +++ b/pywr-schema/src/parameters/control_curves.rs @@ -50,7 +50,7 @@ impl ControlCurveInterpolatedParameter { .map(|val| val.load(network, args, None)) .collect::, _>>()?; - let mut p = pywr_core::parameters::ControlCurveInterpolatedParameterBuilder::new( + let mut p = pywr_core::parameters::ControlCurveInterpolatedParameterBuilder::before( ParameterName::new(&self.meta.name, parent), metric, ); @@ -156,7 +156,7 @@ impl ControlCurveIndexParameter { parent: Option<&str>, ) -> Result<(), SchemaError> { let metric = self.storage_metric.load(network, args, parent)?; - let mut builder = pywr_core::parameters::ControlCurveIndexParameterBuilder::new( + let mut builder = pywr_core::parameters::ControlCurveIndexParameterBuilder::before( ParameterName::new(&self.meta.name, parent), metric, ); @@ -275,7 +275,7 @@ impl ControlCurveParameter { ) -> Result<(), SchemaError> { let metric = self.storage_metric.load(network, args, None)?; - let mut builder = pywr_core::parameters::ControlCurveParameterBuilder::new( + let mut builder = pywr_core::parameters::ControlCurveParameterBuilder::before( ParameterName::new(&self.meta.name, parent), metric, ); @@ -376,7 +376,7 @@ impl ControlCurvePiecewiseInterpolatedParameter { ) -> Result<(), SchemaError> { let metric = self.storage_metric.load(network, args, parent)?; - let mut builder = PiecewiseInterpolatedParameterBuilder::new( + let mut builder = PiecewiseInterpolatedParameterBuilder::before( ParameterName::new(&self.meta.name, parent), metric, self.maximum.unwrap_or(1.0), diff --git a/pywr-schema/src/parameters/doc_examples/aggregated_1.json b/pywr-schema/src/parameters/doc_examples/aggregated_1.json index 49175fce..a38d2fe0 100644 --- a/pywr-schema/src/parameters/doc_examples/aggregated_1.json +++ b/pywr-schema/src/parameters/doc_examples/aggregated_1.json @@ -6,6 +6,7 @@ "agg_func": { "type": "Sum" }, + "phase": "Before", "metrics": [ { "type": "Literal", diff --git a/pywr-schema/src/parameters/mod.rs b/pywr-schema/src/parameters/mod.rs index 7e3bbb6f..b48f2d29 100644 --- a/pywr-schema/src/parameters/mod.rs +++ b/pywr-schema/src/parameters/mod.rs @@ -84,6 +84,13 @@ pub struct ParameterMeta { pub tags: HashMap, } +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema, PywrVisitAll)] +pub enum ParameterPhase { + Before, + After, + Both, +} + #[derive(serde::Deserialize, serde::Serialize, Debug, EnumDiscriminants, Clone, JsonSchema, Display)] #[serde(tag = "type")] #[strum_discriminants(derive(Display, IntoStaticStr, EnumString, EnumIter))] @@ -178,6 +185,47 @@ impl Parameter { // Implementation provided by the `EnumDiscriminants` derive macro. self.into() } + + pub fn phase(&self) -> ParameterPhase { + match self { + Self::Aggregated(p) => p.phase.clone(), + Self::AggregatedIndex(p) => p.phase.clone(), + Self::AsymmetricSwitchIndex(_) => ParameterPhase::Before, + Self::Constant(_) => ParameterPhase::Before, + Self::ConstantScenario(_) => ParameterPhase::Before, + Self::ControlCurvePiecewiseInterpolated(_) => ParameterPhase::Before, + Self::ControlCurveInterpolated(_) => ParameterPhase::Before, + Self::ControlCurveIndex(_) => ParameterPhase::Before, + Self::ControlCurve(_) => ParameterPhase::Before, + Self::DailyProfile(_) => ParameterPhase::Before, + Self::IndexedArray(_) => ParameterPhase::Before, + Self::MonthlyProfile(_) => ParameterPhase::Before, + Self::WeeklyProfile(_) => ParameterPhase::Before, + Self::UniformDrawdownProfile(_) => ParameterPhase::Before, + Self::Max(_) => ParameterPhase::Before, + Self::Min(_) => ParameterPhase::Before, + Self::MultiThreshold(_) => ParameterPhase::Before, + Self::Negative(_) => ParameterPhase::Before, + Self::Polynomial1D(_) => ParameterPhase::Before, + Self::Threshold(_) => ParameterPhase::Before, + Self::TablesArray(_) => ParameterPhase::Before, + Self::Python(_) => ParameterPhase::Before, + Self::Delay(_) => ParameterPhase::Before, + Self::DelayIndex(_) => ParameterPhase::Before, + Self::Division(_) => ParameterPhase::Before, + Self::Offset(_) => ParameterPhase::Before, + Self::DiscountFactor(_) => ParameterPhase::Before, + Self::Interpolated(_) => ParameterPhase::Before, + Self::HydropowerTarget(_) => ParameterPhase::Before, + Self::RbfProfile(_) => ParameterPhase::Before, + Self::NegativeMax(_) => ParameterPhase::Before, + Self::NegativeMin(_) => ParameterPhase::Before, + Self::Rolling(_) => ParameterPhase::Before, + Self::RollingIndex(_) => ParameterPhase::Before, + Self::Placeholder(_) => ParameterPhase::Before, + Self::DiurnalProfile(_) => ParameterPhase::Before, + } + } } #[cfg(feature = "core")] diff --git a/pywr-schema/tests/daily-profile1.json b/pywr-schema/tests/daily-profile1.json index fe16e56d..8f2b65a6 100644 --- a/pywr-schema/tests/daily-profile1.json +++ b/pywr-schema/tests/daily-profile1.json @@ -112,6 +112,7 @@ "agg_func": { "type": "Sum" }, + "phase": "Before", "metrics": [ { "type": "Parameter", diff --git a/pywr-schema/tests/deficit-agg1-expected.csv b/pywr-schema/tests/deficit-agg1-expected.csv new file mode 100644 index 00000000..30bf631d --- /dev/null +++ b/pywr-schema/tests/deficit-agg1-expected.csv @@ -0,0 +1,19 @@ +time_start,time_end,simulation_id,label,metric_set,name,attribute,value +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand,before,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,"Total deficit",after,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand,before,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,"Total deficit",after,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand,before,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,"Total deficit",after,5.0 diff --git a/pywr-schema/tests/deficit-agg1.json b/pywr-schema/tests/deficit-agg1.json new file mode 100644 index 00000000..5d925bc0 --- /dev/null +++ b/pywr-schema/tests/deficit-agg1.json @@ -0,0 +1,131 @@ +{ + "metadata": { + "title": "Deficit aggregation 1", + "description": "An example of how to aggregate deficits from multiple nodes.", + "minimum_version": "0.1" + }, + "time": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": { + "type": "Days", + "days": 1 + } + }, + "network": { + "nodes": [ + { + "meta": { + "name": "supply1" + }, + "type": "Input", + "max_flow": { + "type": "Literal", + "value": 15.0 + } + }, + { + "meta": { + "name": "link1" + }, + "type": "Link" + }, + { + "meta": { + "name": "demand11" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -10 + } + }, + { + "meta": { + "name": "demand12" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -5 + } + } + ], + "edges": [ + { + "from_node": "supply1", + "to_node": "link1" + }, + { + "from_node": "link1", + "to_node": "demand11" + }, + { + "from_node": "link1", + "to_node": "demand12" + } + ], + "parameters": [ + { + "meta": { + "name": "demand" + }, + "type": "Constant", + "value": { + "type": "Literal", + "value": 10.0 + } + }, + { + "meta": { + "name": "Total deficit" + }, + "type": "Aggregated", + "phase": "After", + "agg_func": { + "type": "Sum" + }, + "metrics": [ + { + "type": "Node", + "name": "demand11", + "attribute": "Deficit" + }, + { + "type": "Node", + "name": "demand12", + "attribute": "Deficit" + } + ] + } + ], + "metric_sets": [ + { + "name": "all", + "filters": { + "all_nodes": true, + "all_virtual_nodes": true, + "all_parameters": true, + "all_edges": false + } + } + ], + "outputs": [ + { + "name": "nodes", + "type": "CSV", + "format": "Long", + "filename": "deficit-agg1-expected.csv", + "metric_set": "all" + } + ] + } +} diff --git a/pywr-schema/tests/deficit-agg2-expected.csv b/pywr-schema/tests/deficit-agg2-expected.csv new file mode 100644 index 00000000..0763654a --- /dev/null +++ b/pywr-schema/tests/deficit-agg2-expected.csv @@ -0,0 +1,19 @@ +time_start,time_end,simulation_id,label,metric_set,name,attribute,value +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand11,Inflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand12,Inflow,0.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand,before,20.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,"Total deficit",after,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand11,Inflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand12,Inflow,0.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand,before,20.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,"Total deficit",after,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand11,Inflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand12,Inflow,0.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand,before,20.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,"Total deficit",after,15.0 diff --git a/pywr-schema/tests/deficit-agg2.json b/pywr-schema/tests/deficit-agg2.json new file mode 100644 index 00000000..c4747dcd --- /dev/null +++ b/pywr-schema/tests/deficit-agg2.json @@ -0,0 +1,132 @@ +{ + "metadata": { + "title": "Deficit aggregation 1", + "description": "An example of how to aggregate deficits from multiple nodes.", + "minimum_version": "0.1" + }, + "time": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": { + "type": "Days", + "days": 1 + } + }, + "network": { + "nodes": [ + { + "meta": { + "name": "supply1" + }, + "type": "Input", + "max_flow": { + "type": "Literal", + "value": 15.0 + } + }, + { + "meta": { + "name": "link1" + }, + "type": "Link" + }, + { + "meta": { + "name": "demand11" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -10 + } + }, + { + "meta": { + "name": "demand12" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "Total deficit", + "return_value": "AfterOrElseInitial" + }, + "cost": { + "type": "Literal", + "value": -5 + } + } + ], + "edges": [ + { + "from_node": "supply1", + "to_node": "link1" + }, + { + "from_node": "link1", + "to_node": "demand11" + }, + { + "from_node": "link1", + "to_node": "demand12" + } + ], + "parameters": [ + { + "meta": { + "name": "demand" + }, + "type": "Constant", + "value": { + "type": "Literal", + "value": 20.0 + } + }, + { + "meta": { + "name": "Total deficit" + }, + "type": "Aggregated", + "agg_func": { + "type": "Sum" + }, + "phase": "After", + "metrics": [ + { + "type": "Node", + "name": "demand11", + "attribute": "Deficit" + }, + { + "type": "Node", + "name": "demand12", + "attribute": "Deficit" + } + ] + } + ], + "metric_sets": [ + { + "name": "all", + "filters": { + "all_nodes": true, + "all_virtual_nodes": true, + "all_parameters": true, + "all_edges": false + } + } + ], + "outputs": [ + { + "name": "nodes", + "type": "CSV", + "format": "Long", + "filename": "deficit-agg2-expected.csv", + "metric_set": "all" + } + ] + } +} diff --git a/pywr-schema/tests/deficit-agg3-expected.csv b/pywr-schema/tests/deficit-agg3-expected.csv new file mode 100644 index 00000000..11897d16 --- /dev/null +++ b/pywr-schema/tests/deficit-agg3-expected.csv @@ -0,0 +1,22 @@ +time_start,time_end,simulation_id,label,metric_set,name,attribute,value +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand,before,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,"Total deficit",before,0.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,"Total deficit",after,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand,before,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,"Total deficit",before,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,"Total deficit",after,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand,before,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,"Total deficit",before,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,"Total deficit",after,5.0 diff --git a/pywr-schema/tests/deficit-agg3.json b/pywr-schema/tests/deficit-agg3.json new file mode 100644 index 00000000..c1d6d016 --- /dev/null +++ b/pywr-schema/tests/deficit-agg3.json @@ -0,0 +1,131 @@ +{ + "metadata": { + "title": "Deficit aggregation 1", + "description": "An example of how to aggregate deficits from multiple nodes.", + "minimum_version": "0.1" + }, + "time": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": { + "type": "Days", + "days": 1 + } + }, + "network": { + "nodes": [ + { + "meta": { + "name": "supply1" + }, + "type": "Input", + "max_flow": { + "type": "Literal", + "value": 15.0 + } + }, + { + "meta": { + "name": "link1" + }, + "type": "Link" + }, + { + "meta": { + "name": "demand11" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -10 + } + }, + { + "meta": { + "name": "demand12" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -5 + } + } + ], + "edges": [ + { + "from_node": "supply1", + "to_node": "link1" + }, + { + "from_node": "link1", + "to_node": "demand11" + }, + { + "from_node": "link1", + "to_node": "demand12" + } + ], + "parameters": [ + { + "meta": { + "name": "demand" + }, + "type": "Constant", + "value": { + "type": "Literal", + "value": 10.0 + } + }, + { + "meta": { + "name": "Total deficit" + }, + "type": "Aggregated", + "phase": "Both", + "agg_func": { + "type": "Sum" + }, + "metrics": [ + { + "type": "Node", + "name": "demand11", + "attribute": "Deficit" + }, + { + "type": "Node", + "name": "demand12", + "attribute": "Deficit" + } + ] + } + ], + "metric_sets": [ + { + "name": "all", + "filters": { + "all_nodes": true, + "all_virtual_nodes": true, + "all_parameters": true, + "all_edges": false + } + } + ], + "outputs": [ + { + "name": "nodes", + "type": "CSV", + "format": "Long", + "filename": "deficit-agg3-expected.csv", + "metric_set": "all" + } + ] + } +} diff --git a/pywr-schema/tests/flow-agg1-expected.csv b/pywr-schema/tests/flow-agg1-expected.csv new file mode 100644 index 00000000..c8544ffc --- /dev/null +++ b/pywr-schema/tests/flow-agg1-expected.csv @@ -0,0 +1,22 @@ +time_start,time_end,simulation_id,label,metric_set,name,attribute,value +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,demand,before,10.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,"Total flow",before,0.0 +2015-01-01T00:00:00,2015-01-02T00:00:00,0,0,all,"Total flow",after,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,demand,before,10.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,"Total flow",before,15.0 +2015-01-02T00:00:00,2015-01-03T00:00:00,0,0,all,"Total flow",after,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,supply1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,link1,Outflow,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand11,Inflow,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand12,Inflow,5.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,demand,before,10.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,"Total flow",before,15.0 +2015-01-03T00:00:00,2015-01-04T00:00:00,0,0,all,"Total flow",after,15.0 diff --git a/pywr-schema/tests/flow-agg1.json b/pywr-schema/tests/flow-agg1.json new file mode 100644 index 00000000..c74b50d5 --- /dev/null +++ b/pywr-schema/tests/flow-agg1.json @@ -0,0 +1,131 @@ +{ + "metadata": { + "title": "Deficit aggregation 1", + "description": "An example of how to aggregate deficits from multiple nodes.", + "minimum_version": "0.1" + }, + "time": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": { + "type": "Days", + "days": 1 + } + }, + "network": { + "nodes": [ + { + "meta": { + "name": "supply1" + }, + "type": "Input", + "max_flow": { + "type": "Literal", + "value": 15.0 + } + }, + { + "meta": { + "name": "link1" + }, + "type": "Link" + }, + { + "meta": { + "name": "demand11" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -10 + } + }, + { + "meta": { + "name": "demand12" + }, + "type": "Output", + "max_flow": { + "type": "Parameter", + "name": "demand" + }, + "cost": { + "type": "Literal", + "value": -5 + } + } + ], + "edges": [ + { + "from_node": "supply1", + "to_node": "link1" + }, + { + "from_node": "link1", + "to_node": "demand11" + }, + { + "from_node": "link1", + "to_node": "demand12" + } + ], + "parameters": [ + { + "meta": { + "name": "demand" + }, + "type": "Constant", + "value": { + "type": "Literal", + "value": 10.0 + } + }, + { + "meta": { + "name": "Total flow" + }, + "type": "Aggregated", + "phase": "Both", + "agg_func": { + "type": "Sum" + }, + "metrics": [ + { + "type": "Node", + "name": "demand11", + "attribute": "Inflow" + }, + { + "type": "Node", + "name": "demand12", + "attribute": "Inflow" + } + ] + } + ], + "metric_sets": [ + { + "name": "all", + "filters": { + "all_nodes": true, + "all_virtual_nodes": true, + "all_parameters": true, + "all_edges": false + } + } + ], + "outputs": [ + { + "name": "nodes", + "type": "CSV", + "format": "Long", + "filename": "flow-agg1-expected.csv", + "metric_set": "all" + } + ] + } +} diff --git a/pywr-schema/tests/python-agg-func1.json b/pywr-schema/tests/python-agg-func1.json index ac7738eb..b124c098 100644 --- a/pywr-schema/tests/python-agg-func1.json +++ b/pywr-schema/tests/python-agg-func1.json @@ -95,6 +95,7 @@ "multiplier": 1.5 } }, + "phase": "Before", "metrics": [ { "type": "Parameter", diff --git a/pywr-schema/tests/storage_max_volumes.json b/pywr-schema/tests/storage_max_volumes.json index 421e4bcd..186321bb 100644 --- a/pywr-schema/tests/storage_max_volumes.json +++ b/pywr-schema/tests/storage_max_volumes.json @@ -128,6 +128,7 @@ "agg_func": { "type": "Sum" }, + "phase": "Before", "metrics": [ { "type": "Parameter", diff --git a/pywr-schema/tests/test_models.rs b/pywr-schema/tests/test_models.rs index 36d2dbab..db428001 100644 --- a/pywr-schema/tests/test_models.rs +++ b/pywr-schema/tests/test_models.rs @@ -49,6 +49,10 @@ model_tests! { test_csv1: ("csv1.json", vec![("csv1-outputs-long.csv", ResultsShape::Long), ("csv1-outputs-wide.csv", ResultsShape::Wide)], vec![], vec![]), test_csv2: ("csv2.json", vec![("csv2-outputs-long.csv", ResultsShape::Long), ("csv2-outputs-wide.csv", ResultsShape::Wide)], vec![], vec![]), test_csv3: ("csv3.json", vec![("csv3-outputs-long.csv", ResultsShape::Long)], vec![], vec![]), + test_deficit_agg1: ("deficit-agg1.json", vec![("deficit-agg1-expected.csv", ResultsShape::Long)], vec![], vec![]), + test_deficit_agg2: ("deficit-agg2.json", vec![("deficit-agg2-expected.csv", ResultsShape::Long)], vec![], vec![]), + test_deficit_agg3: ("deficit-agg3.json", vec![("deficit-agg3-expected.csv", ResultsShape::Long)], vec![], vec![]), + test_flow_agg1: ("flow-agg1.json", vec![("flow-agg1-expected.csv", ResultsShape::Long)], vec![], vec![]), test_hdf1: ("hdf1.json", vec![], vec![], vec![]), // TODO asserting h5 results not possible with this framework test_memory1: ("memory1.json", vec![], vec![], vec![]), // TODO asserting memory results not possible with this framework test_timeseries: ("timeseries.json", vec![("timeseries-expected.csv", ResultsShape::Long)], vec![], vec![]), diff --git a/pywr-schema/tests/timeseries.json b/pywr-schema/tests/timeseries.json index 7357a43e..096edcda 100644 --- a/pywr-schema/tests/timeseries.json +++ b/pywr-schema/tests/timeseries.json @@ -90,6 +90,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/timeseries2-hourly.json b/pywr-schema/tests/timeseries2-hourly.json index ad671e46..44c8a269 100644 --- a/pywr-schema/tests/timeseries2-hourly.json +++ b/pywr-schema/tests/timeseries2-hourly.json @@ -98,6 +98,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/timeseries2.json b/pywr-schema/tests/timeseries2.json index 0490dfff..afa0fc7e 100644 --- a/pywr-schema/tests/timeseries2.json +++ b/pywr-schema/tests/timeseries2.json @@ -98,6 +98,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/timeseries3.json b/pywr-schema/tests/timeseries3.json index 0650d04b..dc1abe7e 100644 --- a/pywr-schema/tests/timeseries3.json +++ b/pywr-schema/tests/timeseries3.json @@ -103,6 +103,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/timeseries4.json b/pywr-schema/tests/timeseries4.json index a622e51b..1eee1b70 100644 --- a/pywr-schema/tests/timeseries4.json +++ b/pywr-schema/tests/timeseries4.json @@ -127,6 +127,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/timeseries5.json b/pywr-schema/tests/timeseries5.json index 09427004..be5656d1 100644 --- a/pywr-schema/tests/timeseries5.json +++ b/pywr-schema/tests/timeseries5.json @@ -127,6 +127,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/timeseries_pandas.json b/pywr-schema/tests/timeseries_pandas.json index 9c15e595..26cddc2a 100644 --- a/pywr-schema/tests/timeseries_pandas.json +++ b/pywr-schema/tests/timeseries_pandas.json @@ -90,6 +90,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries", diff --git a/pywr-schema/tests/v1/inline-parameter-converted.json b/pywr-schema/tests/v1/inline-parameter-converted.json index d0564eaf..424ade24 100644 --- a/pywr-schema/tests/v1/inline-parameter-converted.json +++ b/pywr-schema/tests/v1/inline-parameter-converted.json @@ -124,6 +124,7 @@ "type": "Timeseries" } ], + "phase": "Before", "type": "Aggregated" } ], diff --git a/pywr-schema/tests/v1/inline-parameter.json b/pywr-schema/tests/v1/inline-parameter.json index ef99a2a8..65e99e57 100644 --- a/pywr-schema/tests/v1/inline-parameter.json +++ b/pywr-schema/tests/v1/inline-parameter.json @@ -41,6 +41,7 @@ "max_flow": { "type": "aggregated", "agg_func": "product", + "phase": "Before", "parameters": [ { "type": "constant", diff --git a/pywr-schema/tests/v1/timeseries-converted.json b/pywr-schema/tests/v1/timeseries-converted.json index cde18e44..869f1283 100644 --- a/pywr-schema/tests/v1/timeseries-converted.json +++ b/pywr-schema/tests/v1/timeseries-converted.json @@ -108,6 +108,7 @@ "agg_func": { "type": "Product" }, + "phase": "Before", "metrics": [ { "type": "Timeseries",