diff --git a/pywr-core/src/lib.rs b/pywr-core/src/lib.rs index 4e2dce7d..fb923dc1 100644 --- a/pywr-core/src/lib.rs +++ b/pywr-core/src/lib.rs @@ -9,6 +9,7 @@ pub mod edge; pub mod metric; pub mod models; pub mod network; +pub mod network_variable_config; pub mod node; pub mod parameters; pub mod recorders; diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 8e2f9fc3..fc57c9ef 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -8,6 +8,7 @@ use crate::parameters::{ GeneralParameterIndex, GeneralParameterType, ParameterCalculationError, ParameterCollection, ParameterCollectionConstCalculationError, ParameterCollectionError, ParameterCollectionSetupError, ParameterCollectionSimpleCalculationError, ParameterIndex, ParameterName, ParameterStates, VariableConfig, + VariableParameterValues, }; use crate::recorders::{ MetricSet, MetricSetIndex, MetricSetSaveError, MetricSetState, RecorderAggregationError, RecorderFinalResult, @@ -2098,18 +2099,19 @@ impl Network { Ok(edge_index) } - /// Set the variable values on the parameter `parameter_index`. + /// Set the variable values on the parameter [`parameter_index`]. /// /// This will update the internal state of the parameter with the new values for all scenarios. pub fn set_f64_parameter_variable_values( &self, parameter_index: ParameterIndex, - values: &[f64], + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, state: &mut NetworkState, ) -> Result<(), NetworkError> { match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_f64_variable() { + Some(parameter) => match parameter.as_variable() { Some(variable) => { // Iterate over all scenarios and set the variable values for parameter_states in state.iter_parameter_states_mut() { @@ -2120,7 +2122,7 @@ impl Network { )?; variable - .set_variables(values, variable_config, internal_state) + .set_variables(values_f64, values_u64, variable_config, internal_state) .map_err(|source| NetworkError::VariableParameterError { name: parameter.name().clone(), source, @@ -2137,19 +2139,20 @@ impl Network { } } - /// Set the variable values on the parameter `parameter_index` and scenario `scenario_index`. + /// Set the variable values on the parameter [`parameter_index`] and scenario [`scenario_index`]. /// /// Only the internal state of the parameter for the given scenario will be updated. pub fn set_f64_parameter_variable_values_for_scenario( &self, parameter_index: ParameterIndex, scenario_index: ScenarioIndex, - values: &[f64], + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, state: &mut NetworkState, ) -> Result<(), NetworkError> { match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_f64_variable() { + Some(parameter) => match parameter.as_variable() { Some(variable) => { let internal_state = state .parameter_states_mut(&scenario_index) @@ -2157,13 +2160,14 @@ impl Network { .ok_or(NetworkError::ParameterStateNotFound { name: parameter.name().clone(), })?; - variable - .set_variables(values, variable_config, internal_state) + .set_variables(values_f64, values_u64, variable_config, internal_state) .map_err(|source| NetworkError::VariableParameterError { name: parameter.name().clone(), source, - }) + })?; + + Ok(()) } None => Err(NetworkError::ParameterTypeNotVariable { name: parameter.name().clone(), @@ -2179,9 +2183,9 @@ impl Network { parameter_index: ParameterIndex, scenario_index: ScenarioIndex, state: &NetworkState, - ) -> Result>, NetworkError> { + ) -> Result, NetworkError> { match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_f64_variable() { + Some(parameter) => match parameter.as_variable() { Some(variable) => { let internal_state = state .parameter_states(&scenario_index) @@ -2204,9 +2208,9 @@ impl Network { &self, parameter_index: ParameterIndex, state: &NetworkState, - ) -> Result>>, NetworkError> { + ) -> Result>, NetworkError> { match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_f64_variable() { + Some(parameter) => match parameter.as_variable() { Some(variable) => { let values = state .iter_parameter_states() @@ -2231,29 +2235,30 @@ impl Network { } } - /// Set the variable values on the parameter `parameter_index`. + /// Set the variable values on the parameter [`parameter_index`]. /// /// This will update the internal state of the parameter with the new values for scenarios. - pub fn set_u32_parameter_variable_values( + pub fn set_u64_parameter_variable_values( &self, - parameter_index: ParameterIndex, - values: &[u32], + parameter_index: ParameterIndex, + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, state: &mut NetworkState, ) -> Result<(), NetworkError> { - match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_u32_variable() { + match self.parameters.get_u64(parameter_index) { + Some(parameter) => match parameter.as_variable() { Some(variable) => { // Iterate over all scenarios and set the variable values for parameter_states in state.iter_parameter_states_mut() { - let internal_state = parameter_states.get_mut_f64_state(parameter_index).ok_or( + let internal_state = parameter_states.get_mut_u64_state(parameter_index).ok_or( NetworkError::ParameterStateNotFound { name: parameter.name().clone(), }, )?; variable - .set_variables(values, variable_config, internal_state) + .set_variables(values_f64, values_u64, variable_config, internal_state) .map_err(|source| NetworkError::VariableParameterError { name: parameter.name().clone(), source, @@ -2266,73 +2271,107 @@ impl Network { name: parameter.name().clone(), }), }, - None => Err(NetworkError::ParameterF64IndexNotFound(parameter_index)), + None => Err(NetworkError::ParameterU64IndexNotFound(parameter_index)), } } - /// Set the variable values on the parameter `parameter_index` and scenario `scenario_index`. + /// Set the variable values on the parameter [`parameter_index`] and scenario [`scenario_index`]. /// /// Only the internal state of the parameter for the given scenario will be updated. - pub fn set_u32_parameter_variable_values_for_scenario( + pub fn set_u64_parameter_variable_values_for_scenario( &self, - parameter_index: ParameterIndex, + parameter_index: ParameterIndex, scenario_index: ScenarioIndex, - values: &[u32], + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, state: &mut NetworkState, ) -> Result<(), NetworkError> { - match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_u32_variable() { + match self.parameters.get_u64(parameter_index) { + Some(parameter) => match parameter.as_variable() { Some(variable) => { let internal_state = state .parameter_states_mut(&scenario_index) - .get_mut_f64_state(parameter_index) + .get_mut_u64_state(parameter_index) .ok_or(NetworkError::ParameterStateNotFound { name: parameter.name().clone(), })?; variable - .set_variables(values, variable_config, internal_state) + .set_variables(values_f64, values_u64, variable_config, internal_state) .map_err(|source| NetworkError::VariableParameterError { name: parameter.name().clone(), source, - }) + })?; + + Ok(()) } None => Err(NetworkError::ParameterTypeNotVariable { name: parameter.name().clone(), }), }, - None => Err(NetworkError::ParameterF64IndexNotFound(parameter_index)), + None => Err(NetworkError::ParameterU64IndexNotFound(parameter_index)), } } /// Return a vector of the current values of active variable parameters. - pub fn get_u32_parameter_variable_values_for_scenario( + pub fn get_u64_parameter_variable_values_for_scenario( &self, - parameter_index: ParameterIndex, + parameter_index: ParameterIndex, scenario_index: ScenarioIndex, state: &NetworkState, - ) -> Result>, NetworkError> { - match self.parameters.get_f64(parameter_index) { - Some(parameter) => match parameter.as_u32_variable() { + ) -> Result, NetworkError> { + match self.parameters.get_u64(parameter_index) { + Some(parameter) => match parameter.as_variable() { Some(variable) => { let internal_state = state .parameter_states(&scenario_index) - .get_f64_state(parameter_index) + .get_u64_state(parameter_index) .ok_or(NetworkError::ParameterStateNotFound { name: parameter.name().clone(), })?; + Ok(variable.get_variables(internal_state)) } None => Err(NetworkError::ParameterTypeNotVariable { name: parameter.name().clone(), }), }, - None => Err(NetworkError::ParameterF64IndexNotFound(parameter_index)), + None => Err(NetworkError::ParameterU64IndexNotFound(parameter_index)), } } -} + pub fn get_u64_parameter_variable_values( + &self, + parameter_index: ParameterIndex, + state: &NetworkState, + ) -> Result>, NetworkError> { + match self.parameters.get_u64(parameter_index) { + Some(parameter) => match parameter.as_variable() { + Some(variable) => { + let values = state + .iter_parameter_states() + .map(|parameter_states| { + let internal_state = parameter_states.get_u64_state(parameter_index).ok_or( + NetworkError::ParameterStateNotFound { + name: parameter.name().clone(), + }, + )?; + + Ok(variable.get_variables(internal_state)) + }) + .collect::>()?; + + Ok(values) + } + None => Err(NetworkError::ParameterTypeNotVariable { + name: parameter.name().clone(), + }), + }, + None => Err(NetworkError::ParameterU64IndexNotFound(parameter_index)), + } + } +} #[cfg(test)] mod tests { use super::*; @@ -2561,7 +2600,7 @@ mod tests { let variable = ActivationFunction::Unit { min: 0.0, max: 10.0 }; let input_max_flow = parameters::ConstantParameter::new("my-constant".into(), 10.0); - assert!(input_max_flow.can_be_f64_variable()); + assert!(input_max_flow.can_be_variable()); let input_max_flow_idx = model .network_mut() @@ -2585,7 +2624,7 @@ mod tests { // Update the variable values model .network_mut() - .set_f64_parameter_variable_values(input_max_flow_idx, &[5.0], &variable, state.network_state_mut()) + .set_f64_parameter_variable_values(input_max_flow_idx, &[5.0], &[], &variable, state.network_state_mut()) .unwrap(); // After update the variable value should match what was set @@ -2594,6 +2633,12 @@ mod tests { .get_f64_parameter_variable_values(input_max_flow_idx, state.network_state()) .unwrap(); - assert_eq!(variable_values, vec![Some(vec![5.0])]); + assert_eq!( + variable_values, + vec![Some(VariableParameterValues { + f64: vec![5.0], + u64: vec![] + })] + ); } } diff --git a/pywr-core/src/network_variable_config.rs b/pywr-core/src/network_variable_config.rs new file mode 100644 index 00000000..f549ee70 --- /dev/null +++ b/pywr-core/src/network_variable_config.rs @@ -0,0 +1,147 @@ +use crate::NetworkError; +use crate::network::{Network, NetworkState}; +use crate::parameters::{ParameterIndex, ParameterName, VariableConfig}; +use thiserror::Error; + +pub struct NetworkVariableConfig { + /// Configuration for f64 parameters. + variable_configs_f64: Vec<(ParameterIndex, Box)>, + /// Configuration for u64 parameters. + variable_configs_u64: Vec<(ParameterIndex, Box)>, +} + +impl NetworkVariableConfig { + /// Iterate over the variable configurations. + pub fn iter<'n>( + &'n self, + network: &'n Network, + ) -> impl Iterator + use<'n> { + self.variable_configs_f64 + .iter() + .map(move |(parameter_index, config)| { + (network.get_parameter(*parameter_index).unwrap().name(), config.as_ref()) + }) + .chain(self.variable_configs_u64.iter().map(move |(parameter_index, config)| { + ( + network.get_index_parameter(*parameter_index).unwrap().name(), + config.as_ref(), + ) + })) + } + /// Apply the values to the network state. + pub fn apply( + &self, + network: &Network, + values_f64: &[f64], + values_u64: &[u64], + state: &mut NetworkState, + ) -> Result<(), NetworkError> { + let mut offset_f64: usize = 0; + let mut offset_u64: usize = 0; + + for (parameter_index, var_config) in &self.variable_configs_f64 { + let parameter = network.get_parameter(*parameter_index).expect("Parameter not found"); + let variable = parameter.as_variable().expect("Parameter cannot be variable"); + + let (size_f64, size_u64) = variable.size(var_config.as_ref()); + + let range_f64 = offset_f64..offset_f64 + size_f64; + let range_u64 = offset_u64..offset_u64 + size_u64; + + for parameter_states in state.iter_parameter_states_mut() { + let internal_state = parameter_states.get_mut_f64_state(*parameter_index).ok_or( + NetworkError::ParameterStateNotFound { + name: parameter.name().clone(), + }, + )?; + + variable + .set_variables( + &values_f64[range_f64.clone()], + &values_u64[range_u64.clone()], + var_config.as_ref(), + internal_state, + ) + .map_err(|source| NetworkError::VariableParameterError { + name: parameter.name().clone(), + source, + })?; + } + + offset_f64 += size_f64; + offset_u64 += size_u64; + } + + for (parameter_index, var_config) in &self.variable_configs_u64 { + let parameter = network + .get_index_parameter(*parameter_index) + .expect("Parameter not found"); + let variable = parameter.as_variable().expect("Parameter cannot be variable"); + + let (size_f64, size_u64) = variable.size(var_config.as_ref()); + let range_f64 = offset_f64..offset_f64 + size_f64; + let range_u64 = offset_u64..offset_u64 + size_u64; + + // TODO handle the case where the range is out of bounds without a panic + network.set_u64_parameter_variable_values( + *parameter_index, + &values_f64[range_f64], + &values_u64[range_u64], + var_config.as_ref(), + state, + )?; + + offset_f64 += size_f64; + offset_u64 += size_u64; + } + + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum NetworkVariableConfigBuilderError { + #[error("Parameter not found: {0}")] + ParameterNotFound(ParameterName), +} + +pub struct NetworkVariableConfigBuilder<'n> { + network: &'n Network, + variable_configs_f64: Vec<(ParameterIndex, Box)>, + variable_configs_u64: Vec<(ParameterIndex, Box)>, +} + +impl<'n> NetworkVariableConfigBuilder<'n> { + pub fn new(network: &'n Network) -> Self { + Self { + network, + variable_configs_f64: Vec::new(), + variable_configs_u64: Vec::new(), + } + } + + pub fn add_variable_config( + mut self, + parameter_name: &ParameterName, + config: Box, + ) -> Result { + if let Some(parameter_index) = self.network.get_parameter_index_by_name(parameter_name) { + self.variable_configs_f64.push((parameter_index, config)); + } else if let Some(parameter_index) = self.network.get_index_parameter_index_by_name(parameter_name) { + self.variable_configs_u64.push((parameter_index, config)); + } else { + return Err(NetworkVariableConfigBuilderError::ParameterNotFound( + parameter_name.clone(), + )); + } + + Ok(self) + } + + pub fn build(self) -> NetworkVariableConfig { + NetworkVariableConfig { + variable_configs_f64: self.variable_configs_f64, + variable_configs_u64: self.variable_configs_u64, + } + } +} diff --git a/pywr-core/src/parameters/activation_function.rs b/pywr-core/src/parameters/activation_function.rs index a05e4a3b..6ce48b86 100644 --- a/pywr-core/src/parameters/activation_function.rs +++ b/pywr-core/src/parameters/activation_function.rs @@ -1,3 +1,6 @@ +use crate::parameters::VariableConfig; +use std::any::Any; + #[derive(Copy, Clone)] pub enum ActivationFunction { Unit { min: f64, max: f64 }, @@ -51,6 +54,16 @@ impl ActivationFunction { } } +impl VariableConfig for ActivationFunction { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + #[cfg(test)] mod tests { use crate::parameters::ActivationFunction; diff --git a/pywr-core/src/parameters/constant.rs b/pywr-core/src/parameters/constant.rs index 8274d9ed..ad2648b9 100644 --- a/pywr-core/src/parameters/constant.rs +++ b/pywr-core/src/parameters/constant.rs @@ -1,8 +1,8 @@ use crate::parameters::errors::{ConstCalculationError, ParameterSetupError}; use crate::parameters::{ ActivationFunction, ConstParameter, Parameter, ParameterMeta, ParameterName, ParameterState, VariableConfig, - VariableParameter, VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, - downcast_variable_config_ref, + VariableParameter, VariableParameterError, VariableParameterValues, downcast_internal_state_mut, + downcast_internal_state_ref, downcast_variable_config_ref, }; use crate::scenario::ScenarioIndex; use crate::state::ConstParameterValues; @@ -49,11 +49,7 @@ impl Parameter for ConstantParameter { let value: Option = None; Ok(Some(Box::new(value))) } - fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { - Some(self) - } - - fn as_f64_variable_mut(&mut self) -> Option<&mut dyn VariableParameter> { + fn as_variable(&self) -> Option<&dyn VariableParameter> { Some(self) } } @@ -76,49 +72,66 @@ impl ConstParameter for ConstantParameter { } } -impl VariableParameter for ConstantParameter { +impl VariableParameter for ConstantParameter { fn meta(&self) -> &ParameterMeta { &self.meta } - fn size(&self, _variable_config: &dyn VariableConfig) -> usize { - 1 + fn size(&self, _variable_config: &dyn VariableConfig) -> (usize, usize) { + (1, 0) } fn set_variables( &self, - values: &[f64], + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, internal_state: &mut Option>, ) -> Result<(), VariableParameterError> { let activation_function = downcast_variable_config_ref::(variable_config); - if values.len() == 1 { + if !values_u64.is_empty() { + return Err(VariableParameterError::IncorrectNumberOfValues { + expected: 0, + received: values_u64.len(), + }); + } + + if values_f64.len() == 1 { let value = downcast_internal_state_mut::(internal_state); - *value = Some(activation_function.apply(values[0])); + *value = Some(activation_function.apply(values_f64[0])); Ok(()) } else { Err(VariableParameterError::IncorrectNumberOfValues { expected: 1, - received: values.len(), + received: values_f64.len(), }) } } - fn get_variables(&self, internal_state: &Option>) -> Option> { + fn get_variables(&self, internal_state: &Option>) -> Option { downcast_internal_state_ref::(internal_state) .as_ref() - .map(|value| vec![*value]) + .map(|value| VariableParameterValues { + f64: vec![*value], + u64: vec![], + }) } - fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option { let activation_function = downcast_variable_config_ref::(variable_config); - Some(vec![activation_function.lower_bound()]) + Some(VariableParameterValues { + f64: vec![activation_function.lower_bound()], + u64: vec![], + }) } - fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option { let activation_function = downcast_variable_config_ref::(variable_config); - Some(vec![activation_function.upper_bound()]) + Some(VariableParameterValues { + f64: vec![activation_function.upper_bound()], + u64: vec![], + }) } } @@ -143,12 +156,14 @@ mod tests { assert_eq!(p.get_variables(&state), None); // Update the value via the variable API - p.set_variables(&[2.0], &var, &mut state).unwrap(); + p.set_variables(&[2.0], &[], &var, &mut state).unwrap(); // Check the parameter returns the new value assert_approx_eq!(f64, p.value(&state), 2.0); - assert_approx_eq!(&[f64], &p.get_variables(&state).unwrap(), &[2.0]); + let values = p.get_variables(&state).unwrap(); + assert_approx_eq!(&[f64], &values.f64, &[2.0]); + assert!(values.u64.is_empty()); } #[test] diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 7504a11d..ce274182 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -73,12 +73,14 @@ pub use negativemin::NegativeMinParameter; pub use offset::OffsetParameter; pub use polynomial::Polynomial1DParameter; pub use profiles::{ - DailyProfileParameter, DiurnalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, RadialBasisFunction, - RbfProfileParameter, RbfProfileVariableConfig, UniformDrawdownProfileParameter, WeeklyInterpDay, - WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues, + DailyProfileParameter, DiurnalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, MonthlyProfileVariableConfig, + RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig, UniformDrawdownProfileParameter, + WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues, }; #[cfg(feature = "pyo3")] pub use py::{ParameterInfo, PyClassParameter, PyFuncParameter}; +#[cfg(feature = "pyo3")] +use pyo3::pyclass; pub use rolling::RollingParameter; use std::fmt; use std::fmt::{Display, Formatter}; @@ -397,6 +399,7 @@ impl ParameterIndex { } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "pyo3", pyclass)] pub struct ParameterName { name: String, // Optional sub-name for parameters that are part of multi-parameter groups @@ -518,6 +521,14 @@ impl ParameterStates { ParameterIndex::General(idx) => self.general.f64.get(*idx.deref()), } } + + pub fn get_u64_state(&self, index: ParameterIndex) -> Option<&Option>> { + match index { + ParameterIndex::Const(idx) => self.constant.u64.get(*idx.deref()), + ParameterIndex::Simple(idx) => self.simple.u64.get(*idx.deref()), + ParameterIndex::General(idx) => self.general.u64.get(*idx.deref()), + } + } pub fn get_general_f64_state(&self, index: GeneralParameterIndex) -> Option<&Option>> { self.general.f64.get(*index.deref()) } @@ -538,6 +549,14 @@ impl ParameterStates { } } + pub fn get_mut_u64_state(&mut self, index: ParameterIndex) -> Option<&mut Option>> { + match index { + ParameterIndex::Const(idx) => self.constant.u64.get_mut(*idx.deref()), + ParameterIndex::Simple(idx) => self.simple.u64.get_mut(*idx.deref()), + ParameterIndex::General(idx) => self.general.u64.get_mut(*idx.deref()), + } + } + pub fn get_general_mut_f64_state( &mut self, index: GeneralParameterIndex, @@ -629,18 +648,6 @@ pub trait VariableConfig: Any + Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any; } -impl VariableConfig for T -where - T: Any + Send + Sync, -{ - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - /// Helper function to downcast to variable config and print a helpful panic message if this fails. pub fn downcast_variable_config_ref(variable_config: &dyn VariableConfig) -> &T { // Downcast the internal state to the correct type @@ -667,34 +674,14 @@ pub trait Parameter: Send + Sync { Ok(None) } - /// Return the parameter as a [`VariableParameter`] if it supports being a variable. - fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { - None - } - - /// Return the parameter as a [`VariableParameter`] if it supports being a variable. - fn as_f64_variable_mut(&mut self) -> Option<&mut dyn VariableParameter> { - None - } - - /// Can this parameter be a variable - fn can_be_f64_variable(&self) -> bool { - self.as_f64_variable().is_some() - } - - /// Return the parameter as a [`VariableParameter`] if it supports being a variable. - fn as_u32_variable(&self) -> Option<&dyn VariableParameter> { - None - } - - /// Return the parameter as a [`VariableParameter`] if it supports being a variable. - fn as_u32_variable_mut(&mut self) -> Option<&mut dyn VariableParameter> { + /// Return the parameter as a [`VariableParameter`] if it supports being a variable. + fn as_variable(&self) -> Option<&dyn VariableParameter> { None } /// Can this parameter be a variable - fn can_be_u32_variable(&self) -> bool { - self.as_u32_variable().is_some() + fn can_be_variable(&self) -> bool { + self.as_variable().is_some() } } @@ -878,33 +865,42 @@ pub enum VariableParameterError { IncorrectNumberOfValues { expected: usize, received: usize }, } +/// Values associated with a variable parameter. +/// +/// These could represent the current values, new values, or lower and upper bounds for the parameter. +#[derive(PartialEq, Debug)] +pub struct VariableParameterValues { + pub f64: Vec, + pub u64: Vec, +} + /// A parameter that can be optimised. /// /// This trait is used to allow parameter's internal values to be accessed and altered by /// external algorithms. It is primarily designed to be used by the optimisation algorithms -/// such as multi-objective evolutionary algorithms. The trait is generic to the type of -/// the variable values being optimised but these will typically by `f64` and `u32`. -pub trait VariableParameter { +/// such as multi-objective evolutionary algorithms. +pub trait VariableParameter { fn meta(&self) -> &ParameterMeta; fn name(&self) -> &ParameterName { &self.meta().name } /// Return the number of variables required - fn size(&self, variable_config: &dyn VariableConfig) -> usize; + fn size(&self, variable_config: &dyn VariableConfig) -> (usize, usize); /// Apply new variable values to the parameter's state fn set_variables( &self, - values: &[T], + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, internal_state: &mut Option>, ) -> Result<(), VariableParameterError>; /// Get the current variable values - fn get_variables(&self, internal_state: &Option>) -> Option>; + fn get_variables(&self, internal_state: &Option>) -> Option; /// Get variable lower bounds - fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option>; + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option; /// Get variable upper bounds - fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option>; + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option; } #[derive(Debug, Clone, Copy)] @@ -1276,10 +1272,26 @@ 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> { @@ -1380,10 +1392,26 @@ 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()) + { + Some(p) + } else if let Some(p) = self + .simple_u64 .iter() .find(|p| p.name() == name) .map(|p| p.as_parameter()) + { + Some(p) + } else { + self.constant_u64 + .iter() + .find(|p| p.name() == name) + .map(|p| p.as_parameter()) + } } pub fn get_u64_index_by_name(&self, name: &ParameterName) -> Option> { diff --git a/pywr-core/src/parameters/offset.rs b/pywr-core/src/parameters/offset.rs index d5a662e8..eecc1469 100644 --- a/pywr-core/src/parameters/offset.rs +++ b/pywr-core/src/parameters/offset.rs @@ -3,8 +3,8 @@ use crate::network::Network; use crate::parameters::errors::ParameterCalculationError; use crate::parameters::{ ActivationFunction, GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState, VariableConfig, - VariableParameter, VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, - downcast_variable_config_ref, + VariableParameter, VariableParameterError, VariableParameterValues, downcast_internal_state_mut, + downcast_internal_state_ref, downcast_variable_config_ref, }; use crate::scenario::ScenarioIndex; use crate::state::State; @@ -44,11 +44,7 @@ impl Parameter for OffsetParameter { &self.meta } - fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { - Some(self) - } - - fn as_f64_variable_mut(&mut self) -> Option<&mut dyn VariableParameter> { + fn as_variable(&self) -> Option<&dyn VariableParameter> { Some(self) } } @@ -75,48 +71,65 @@ impl GeneralParameter for OffsetParameter { } } -impl VariableParameter for OffsetParameter { +impl VariableParameter for OffsetParameter { fn meta(&self) -> &ParameterMeta { &self.meta } - fn size(&self, _variable_config: &dyn VariableConfig) -> usize { - 1 + fn size(&self, _variable_config: &dyn VariableConfig) -> (usize, usize) { + (1, 0) } fn set_variables( &self, - values: &[f64], + values_f64: &[f64], + values_u64: &[u64], variable_config: &dyn VariableConfig, internal_state: &mut Option>, ) -> Result<(), VariableParameterError> { let activation_function = downcast_variable_config_ref::(variable_config); - if values.len() == 1 { + if !values_u64.is_empty() { + return Err(VariableParameterError::IncorrectNumberOfValues { + expected: 0, + received: values_u64.len(), + }); + } + + if values_f64.len() == 1 { let value = downcast_internal_state_mut::(internal_state); - *value = Some(activation_function.apply(values[0])); + *value = Some(activation_function.apply(values_f64[0])); Ok(()) } else { Err(VariableParameterError::IncorrectNumberOfValues { expected: 1, - received: values.len(), + received: values_f64.len(), }) } } - fn get_variables(&self, internal_state: &Option>) -> Option> { + fn get_variables(&self, internal_state: &Option>) -> Option { downcast_internal_state_ref::(internal_state) .as_ref() - .map(|value| vec![*value]) + .map(|value| VariableParameterValues { + f64: vec![*value], + u64: vec![], + }) } - fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option { let activation_function = downcast_variable_config_ref::(variable_config); - Some(vec![activation_function.lower_bound()]) + Some(VariableParameterValues { + f64: vec![activation_function.lower_bound()], + u64: vec![], + }) } - fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option { let activation_function = downcast_variable_config_ref::(variable_config); - Some(vec![activation_function.upper_bound()]) + Some(VariableParameterValues { + f64: vec![activation_function.upper_bound()], + u64: vec![], + }) } } diff --git a/pywr-core/src/parameters/profiles/mod.rs b/pywr-core/src/parameters/profiles/mod.rs index a85b9587..9667ad90 100644 --- a/pywr-core/src/parameters/profiles/mod.rs +++ b/pywr-core/src/parameters/profiles/mod.rs @@ -7,7 +7,7 @@ mod weekly; pub use daily::DailyProfileParameter; pub use diurnal::DiurnalProfileParameter; -pub use monthly::{MonthlyInterpDay, MonthlyProfileParameter}; +pub use monthly::{MonthlyInterpDay, MonthlyProfileParameter, MonthlyProfileVariableConfig}; pub use rbf::{RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig}; pub use uniform_drawdown::UniformDrawdownProfileParameter; pub use weekly::{WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues}; diff --git a/pywr-core/src/parameters/profiles/monthly.rs b/pywr-core/src/parameters/profiles/monthly.rs index 2743f825..0641b989 100644 --- a/pywr-core/src/parameters/profiles/monthly.rs +++ b/pywr-core/src/parameters/profiles/monthly.rs @@ -1,9 +1,14 @@ use crate::parameters::errors::SimpleCalculationError; -use crate::parameters::{Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter}; +use crate::parameters::{ + Parameter, ParameterMeta, ParameterName, ParameterSetupError, ParameterState, SimpleParameter, VariableConfig, + VariableParameter, VariableParameterError, VariableParameterValues, downcast_internal_state_mut, + downcast_internal_state_ref, downcast_variable_config_ref, +}; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; use chrono::{Datelike, NaiveDateTime, Timelike}; +use std::any::Any; #[derive(Copy, Clone)] pub enum MonthlyInterpDay { @@ -11,6 +16,9 @@ pub enum MonthlyInterpDay { Last, } +// We store this internal value as an Option so that it can be updated by the variable API +type InternalValue = Option<[f64; 12]>; + pub struct MonthlyProfileParameter { meta: ParameterMeta, values: [f64; 12], @@ -25,6 +33,17 @@ impl MonthlyProfileParameter { interp_day, } } + + /// Return the current value for a given month0 (0-based index). + /// + /// If the internal state is None, the value is returned directly. Otherwise, the value is + /// taken from the internal state. + fn value_for_month0(&self, month0: usize, internal_state: &Option>) -> f64 { + match downcast_internal_state_ref::(internal_state) { + Some(value) => value[month0], + None => self.values[month0], + } + } } fn days_in_year_month(datetime: &NaiveDateTime) -> u32 { @@ -73,6 +92,17 @@ impl Parameter for MonthlyProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta } + fn setup( + &self, + _timesteps: &[Timestep], + _scenario_index: &ScenarioIndex, + ) -> Result>, ParameterSetupError> { + let value: Option<[f64; 12]> = None; + Ok(Some(Box::new(value))) + } + fn as_variable(&self) -> Option<&dyn VariableParameter> { + Some(self) + } } impl SimpleParameter for MonthlyProfileParameter { fn before( @@ -80,27 +110,27 @@ impl SimpleParameter for MonthlyProfileParameter { timestep: &Timestep, _scenario_index: &ScenarioIndex, _values: &SimpleParameterValues, - _internal_state: &mut Option>, + internal_state: &mut Option>, ) -> Result, SimpleCalculationError> { let v = match &self.interp_day { Some(interp_day) => match interp_day { MonthlyInterpDay::First => { let next_month0 = (timestep.date.month0() + 1) % 12; - let first_value = self.values[timestep.date.month0() as usize]; - let last_value = self.values[next_month0 as usize]; + let first_value = self.value_for_month0(timestep.date.month0() as usize, internal_state); + let last_value = self.value_for_month0(next_month0 as usize, internal_state); interpolate_first(×tep.date, first_value, last_value) } MonthlyInterpDay::Last => { let current_month = timestep.date.month(); let last_month = if current_month == 1 { 12 } else { current_month - 1 }; - let first_value = self.values[last_month as usize - 1]; - let last_value = self.values[timestep.date.month() as usize - 1]; + let first_value = self.value_for_month0(last_month as usize - 1, internal_state); + let last_value = self.value_for_month0(timestep.date.month() as usize - 1, internal_state); interpolate_last(×tep.date, first_value, last_value) } }, - None => self.values[timestep.date.month() as usize - 1], + None => self.value_for_month0(timestep.date.month() as usize - 1, internal_state), }; Ok(Some(v)) } @@ -112,3 +142,103 @@ impl SimpleParameter for MonthlyProfileParameter { self } } + +impl VariableParameter for MonthlyProfileParameter { + fn meta(&self) -> &ParameterMeta { + &self.meta + } + + fn size(&self, _variable_config: &dyn VariableConfig) -> (usize, usize) { + (12, 0) + } + + fn set_variables( + &self, + values_f64: &[f64], + values_u64: &[u64], + variable_config: &dyn VariableConfig, + internal_state: &mut Option>, + ) -> Result<(), VariableParameterError> { + let monthly_profile_config = downcast_variable_config_ref::(variable_config); + + if values_f64.len() != 12 { + return Err(VariableParameterError::IncorrectNumberOfValues { + expected: 12, + received: values_f64.len(), + }); + } + + if !values_u64.is_empty() { + return Err(VariableParameterError::IncorrectNumberOfValues { + expected: 0, + received: values_u64.len(), + }); + } + + let value = downcast_internal_state_mut::(internal_state); + + let new_values: [f64; 12] = (0..12) + .map(|i| { + values_f64[i].clamp( + monthly_profile_config.lower_bounds[i], + monthly_profile_config.upper_bounds[i], + ) + }) + .collect::>() + .try_into() + .unwrap(); + + *value = Some(new_values); + + Ok(()) + } + + fn get_variables(&self, internal_state: &Option>) -> Option { + downcast_internal_state_ref::(internal_state) + .as_ref() + .map(|values| VariableParameterValues { + f64: values.to_vec(), + u64: vec![], + }) + } + + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option { + let monthly_profile_config = downcast_variable_config_ref::(variable_config); + Some(VariableParameterValues { + f64: monthly_profile_config.lower_bounds.to_vec(), + u64: vec![], + }) + } + + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option { + let monthly_profile_config = downcast_variable_config_ref::(variable_config); + Some(VariableParameterValues { + f64: monthly_profile_config.upper_bounds.to_vec(), + u64: vec![], + }) + } +} + +pub struct MonthlyProfileVariableConfig { + upper_bounds: [f64; 12], + lower_bounds: [f64; 12], +} + +impl MonthlyProfileVariableConfig { + pub fn new(upper_bounds: [f64; 12], lower_bounds: [f64; 12]) -> Self { + Self { + upper_bounds, + lower_bounds, + } + } +} + +impl VariableConfig for MonthlyProfileVariableConfig { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} diff --git a/pywr-core/src/parameters/profiles/rbf.rs b/pywr-core/src/parameters/profiles/rbf.rs index c00dac18..5d8dcc24 100644 --- a/pywr-core/src/parameters/profiles/rbf.rs +++ b/pywr-core/src/parameters/profiles/rbf.rs @@ -1,21 +1,23 @@ use crate::parameters::errors::{ParameterSetupError, SimpleCalculationError}; use crate::parameters::{ Parameter, ParameterMeta, ParameterName, ParameterState, SimpleParameter, VariableConfig, VariableParameter, - VariableParameterError, downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, + VariableParameterError, VariableParameterValues, downcast_internal_state_mut, downcast_internal_state_ref, + downcast_variable_config_ref, }; use crate::scenario::ScenarioIndex; use crate::state::SimpleParameterValues; use crate::timestep::Timestep; use nalgebra::DMatrix; +use std::any::Any; pub struct RbfProfileVariableConfig { - days_of_year_range: Option, + days_of_year_range: Option, value_upper_bounds: f64, value_lower_bounds: f64, } impl RbfProfileVariableConfig { - pub fn new(days_of_year_range: Option, value_upper_bounds: f64, value_lower_bounds: f64) -> Self { + pub fn new(days_of_year_range: Option, value_upper_bounds: f64, value_lower_bounds: f64) -> Self { Self { days_of_year_range, value_upper_bounds, @@ -23,7 +25,7 @@ impl RbfProfileVariableConfig { } } - pub fn days_of_year_range(&self) -> Option { + pub fn days_of_year_range(&self) -> Option { self.days_of_year_range } @@ -36,11 +38,21 @@ impl RbfProfileVariableConfig { } } +impl VariableConfig for RbfProfileVariableConfig { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + /// A parameter that interpolates between a set of points using a radial basis function to /// create a daily profile. pub struct RbfProfileParameter { meta: ParameterMeta, - points: Vec<(u32, f64)>, + points: Vec<(u64, f64)>, function: RadialBasisFunction, } @@ -52,13 +64,13 @@ struct RbfProfileInternalState { /// The interpolated profile. profile: [f64; 366], /// Optional updated x values of the points. - points_x: Option>, + points_x: Option>, /// Optional updated y values of the points. points_y: Option>, } impl RbfProfileInternalState { - fn new(points: &[(u32, f64)], function: &RadialBasisFunction) -> Self { + fn new(points: &[(u64, f64)], function: &RadialBasisFunction) -> Self { let profile = interpolate_rbf_profile(points, function); Self { @@ -71,7 +83,7 @@ impl RbfProfileInternalState { /// Update the x values of the points. /// /// This does not update the profile. - fn update_x(&mut self, x: Vec) { + fn update_x(&mut self, x: Vec) { self.points_x = Some(x); } @@ -84,7 +96,7 @@ impl RbfProfileInternalState { /// Update the profile with the given points used as default. Any locally stored x and y values are /// used in preference to the default points when interpolating the profile. - fn update_profile(&mut self, points: &[(u32, f64)], function: &RadialBasisFunction) { + fn update_profile(&mut self, points: &[(u64, f64)], function: &RadialBasisFunction) { let points: Vec<_> = match (&self.points_x, &self.points_y) { (Some(x), Some(y)) => x.iter().zip(y.iter()).map(|(x, y)| (*x, *y)).collect(), (Some(x), None) => x @@ -101,7 +113,7 @@ impl RbfProfileInternalState { } impl RbfProfileParameter { - pub fn new(name: ParameterName, points: Vec<(u32, f64)>, function: RadialBasisFunction) -> Self { + pub fn new(name: ParameterName, points: Vec<(u64, f64)>, function: RadialBasisFunction) -> Self { Self { meta: ParameterMeta::new(name), points, @@ -123,19 +135,7 @@ impl Parameter for RbfProfileParameter { let internal_state = RbfProfileInternalState::new(&self.points, &self.function); Ok(Some(Box::new(internal_state))) } - fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { - Some(self) - } - - fn as_f64_variable_mut(&mut self) -> Option<&mut dyn VariableParameter> { - Some(self) - } - - fn as_u32_variable(&self) -> Option<&dyn VariableParameter> { - Some(self) - } - - fn as_u32_variable_mut(&mut self) -> Option<&mut dyn VariableParameter> { + fn as_variable(&self) -> Option<&dyn VariableParameter> { Some(self) } } @@ -162,14 +162,21 @@ impl SimpleParameter for RbfProfileParameter { } } -impl VariableParameter for RbfProfileParameter { +impl VariableParameter for RbfProfileParameter { fn meta(&self) -> &ParameterMeta { &self.meta } /// The size is the number of points that define the profile. - fn size(&self, _variable_config: &dyn VariableConfig) -> usize { - self.points.len() + /// The size is the number of points that define the profile. + fn size(&self, variable_config: &dyn VariableConfig) -> (usize, usize) { + let config = downcast_variable_config_ref::(variable_config); + let size_u64 = match config.days_of_year_range { + Some(_) => self.points.len(), + None => 0, + }; + + (self.points.len(), size_u64) } /// The f64 values update the profile value of each point. @@ -184,117 +191,81 @@ impl VariableParameter for RbfProfileParameter { /// returns: Result<(), PywrError> fn set_variables( &self, - values: &[f64], + values_f64: &[f64], + values_u64: &[u64], _variable_config: &dyn VariableConfig, internal_state: &mut Option>, ) -> Result<(), VariableParameterError> { - if values.len() == self.points.len() { + if values_f64.len() == self.points.len() && values_u64.len() == self.points.len() { let value = downcast_internal_state_mut::(internal_state); - value.update_y(values.to_vec()); + value.update_y(values_f64.to_vec()); + value.update_x(values_u64.to_vec()); value.update_profile(&self.points, &self.function); Ok(()) } else { Err(VariableParameterError::IncorrectNumberOfValues { expected: self.points.len(), - received: values.len(), + received: values_f64.len(), }) } } /// The f64 values are the profile values of each point. - fn get_variables(&self, internal_state: &Option>) -> Option> { + fn get_variables(&self, internal_state: &Option>) -> Option { let value = downcast_internal_state_ref::(internal_state); - value.points_y.clone() - } - - fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { - let config = downcast_variable_config_ref::(variable_config); - let lb = (0..self.points.len()).map(|_| config.value_lower_bounds).collect(); - Some(lb) - } - - fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { - let config = downcast_variable_config_ref::(variable_config); - let lb = (0..self.points.len()).map(|_| config.value_upper_bounds).collect(); - Some(lb) - } -} - -impl VariableParameter for RbfProfileParameter { - fn meta(&self) -> &ParameterMeta { - &self.meta - } - /// The size is the number of points that define the profile. - fn size(&self, variable_config: &dyn VariableConfig) -> usize { - let config = downcast_variable_config_ref::(variable_config); - match config.days_of_year_range { - Some(_) => self.points.len(), - None => 0, - } - } - /// Sets the day of year for each point. - fn set_variables( - &self, - values: &[u32], - _variable_config: &dyn VariableConfig, - internal_state: &mut Option>, - ) -> Result<(), VariableParameterError> { - if values.len() == self.points.len() { - let value = downcast_internal_state_mut::(internal_state); - - value.update_x(values.to_vec()); - value.update_profile(&self.points, &self.function); - - Ok(()) - } else { - Err(VariableParameterError::IncorrectNumberOfValues { - expected: self.points.len(), - received: values.len(), - }) + match (&value.points_y, &value.points_x) { + (Some(y), Some(x)) => Some(VariableParameterValues { + f64: y.clone(), + u64: x.clone(), + }), + (Some(y), None) => Some(VariableParameterValues { + f64: y.clone(), + u64: vec![], + }), + (None, Some(x)) => Some(VariableParameterValues { + f64: vec![], + u64: x.clone(), + }), + (None, None) => None, } } - /// Returns the day of year for each point. - fn get_variables(&self, internal_state: &Option>) -> Option> { - let value = downcast_internal_state_ref::(internal_state); - value.points_x.clone() - } - - fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Option { let config = downcast_variable_config_ref::(variable_config); - if let Some(days_of_year_range) = &config.days_of_year_range { + let lb_x = if let Some(days_of_year_range) = &config.days_of_year_range { // Make sure the lower bound is not less than 1 and handle integer underflow - let lb = self - .points + self.points .iter() .map(|p| p.0.checked_sub(*days_of_year_range).unwrap_or(1).max(1)) - .collect(); - - Some(lb) + .collect() } else { - None - } + vec![] + }; + + let lb_y = (0..self.points.len()).map(|_| config.value_lower_bounds).collect(); + + Some(VariableParameterValues { f64: lb_y, u64: lb_x }) } - fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option> { + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Option { let config = downcast_variable_config_ref::(variable_config); - if let Some(days_of_year_range) = &config.days_of_year_range { + let lb_x = if let Some(days_of_year_range) = &config.days_of_year_range { // Make sure the upper bound is not greater than 365 and handle integer overflow - let lb = self - .points + self.points .iter() .map(|p| p.0.checked_add(*days_of_year_range).unwrap_or(365).min(365)) - .collect(); - - Some(lb) + .collect() } else { - None - } + vec![] + }; + + let lb_y = (0..self.points.len()).map(|_| config.value_upper_bounds).collect(); + Some(VariableParameterValues { f64: lb_y, u64: lb_x }) } } @@ -363,7 +334,7 @@ fn interpolate_rbf(points: &[(f64, f64)], function: &RadialBasis /// This method repeats the point 365 days before and after the user provided points. This /// helps create a cyclic interpolation suitable for a annual profile. It then repeats the /// value for the 58th day to create a daily profile 366 days long. -fn interpolate_rbf_profile(points: &[(u32, f64)], function: &RadialBasisFunction) -> [f64; 366] { +fn interpolate_rbf_profile(points: &[(u64, f64)], function: &RadialBasisFunction) -> [f64; 366] { // Replicate the points in the year before and after. let year_before = points.iter().map(|p| (p.0 as f64 - 365.0, p.1)); let year_after = points.iter().map(|p| (p.0 as f64 + 365.0, p.1)); @@ -473,7 +444,7 @@ mod tests { /// ``` #[test] fn test_rbf_interpolation_profile() { - let points: Vec<(u32, f64)> = vec![(90, 0.5), (180, 0.3), (270, 0.7)]; + let points: Vec<(u64, f64)> = vec![(90, 0.5), (180, 0.3), (270, 0.7)]; let rbf = RadialBasisFunction::MultiQuadric { epsilon: 1.0 / 50.0 }; let f_interp = interpolate_rbf_profile(&points, &rbf); diff --git a/pywr-schema/src/error.rs b/pywr-schema/src/error.rs index 2b8feaf8..5a64b1e5 100644 --- a/pywr-schema/src/error.rs +++ b/pywr-schema/src/error.rs @@ -109,6 +109,11 @@ pub enum SchemaError { PlaceholderNodeNotAllowed { name: String }, #[error("Placeholder parameter `{name}` cannot be added to a model.")] PlaceholderParameterNotAllowed { name: String }, + #[error("Network variable config builder error: {0}")] + #[cfg(feature = "core")] + CoreNetworkVariableConfigBuilderError( + #[from] pywr_core::network_variable_config::NetworkVariableConfigBuilderError, + ), #[error("Node cannot be used in a flow constraint.")] NodeNotAllowedInFlowConstraint, #[error("Node cannot be used in a storage constraint.")] diff --git a/pywr-schema/src/lib.rs b/pywr-schema/src/lib.rs index 562af9c6..63ab73d4 100644 --- a/pywr-schema/src/lib.rs +++ b/pywr-schema/src/lib.rs @@ -20,6 +20,7 @@ pub mod parameters; mod py_utils; pub mod timeseries; mod v1; +mod variable_config; mod visit; pub use digest::{Checksum, ChecksumError}; @@ -32,4 +33,5 @@ pub use network::{LoadArgs, NetworkSchemaBuildError}; pub use network::{NetworkSchema, NetworkSchemaReadError, NetworkSchemaRef}; pub use py_utils::{PythonSource, PythonSourceType, PythonSourceTypeIter}; pub use v1::{ConversionData, TryFromV1, TryIntoV2}; +pub use variable_config::VariableConfigs; pub use visit::{VisitMetrics, VisitPaths}; diff --git a/pywr-schema/src/outputs/memory.rs b/pywr-schema/src/outputs/memory.rs index 6fae9ab0..061c29c7 100644 --- a/pywr-schema/src/outputs/memory.rs +++ b/pywr-schema/src/outputs/memory.rs @@ -11,6 +11,7 @@ use strum_macros::{Display, EnumIter}; #[skip_serializing_none] #[derive(serde::Deserialize, serde::Serialize, Debug, Default, Clone, JsonSchema, PywrVisitPaths)] +#[serde(deny_unknown_fields)] pub struct MemoryAggregation { pub time: Option, pub scenario: Option, @@ -21,8 +22,8 @@ pub struct MemoryAggregation { impl MemoryAggregation { fn load(&self, data_path: Option<&Path>) -> Result { Ok(pywr_core::recorders::Aggregation::new( - self.time.as_ref().map(|f| f.load(data_path)).transpose()?, self.scenario.as_ref().map(|f| f.load(data_path)).transpose()?, + self.time.as_ref().map(|f| f.load(data_path)).transpose()?, self.metric.as_ref().map(|f| f.load(data_path)).transpose()?, )) } diff --git a/pywr-schema/src/parameters/mod.rs b/pywr-schema/src/parameters/mod.rs index 271d6e4b..44f9d594 100644 --- a/pywr-schema/src/parameters/mod.rs +++ b/pywr-schema/src/parameters/mod.rs @@ -57,8 +57,8 @@ pub use offset::OffsetParameter; pub use placeholder::PlaceholderParameter; pub use polynomial::Polynomial1DParameter; pub use profiles::{ - DailyProfileParameter, DirunalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, RadialBasisFunction, - RbfProfileParameter, RbfProfileVariableSettings, UniformDrawdownProfileParameter, WeeklyProfileParameter, + DailyProfileParameter, DirunalProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, MonthlyProfileVariableConfig, RadialBasisFunction, RbfProfileParameter, + RbfProfileVariableConfig, UniformDrawdownProfileParameter, WeeklyProfileParameter, }; pub use python::{PythonObject, PythonParameter, PythonReturnType}; use pywr_schema_macros::{PywrVisitAll, skip_serializing_none}; diff --git a/pywr-schema/src/parameters/profiles.rs b/pywr-schema/src/parameters/profiles.rs index 07084cdf..7faabbe0 100644 --- a/pywr-schema/src/parameters/profiles.rs +++ b/pywr-schema/src/parameters/profiles.rs @@ -159,6 +159,30 @@ impl TryFromV1 for MonthlyProfileParameter { } } +/// Settings for a variable RBF profile. +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Copy, JsonSchema, PywrVisitAll)] +#[serde(deny_unknown_fields)] +pub struct MonthlyProfileVariableConfig { + /// Is this parameter an active variable? + pub is_active: bool, + /// Optional upper bound for the value of each month. If this is `None` then + /// there is no upper bound. + pub upper_bounds: Option<[f64; 12]>, + /// Optional lower bound for the value of each month. If this is `None` then + /// the lower bound is zero. + pub lower_bounds: Option<[f64; 12]>, +} + +#[cfg(feature = "core")] +impl From for pywr_core::parameters::MonthlyProfileVariableConfig { + fn from(settings: MonthlyProfileVariableConfig) -> Self { + Self::new( + settings.upper_bounds.unwrap_or([f64::INFINITY; 12]), + settings.lower_bounds.unwrap_or([0.0; 12]), + ) + } +} + #[skip_serializing_none] #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema, PywrVisitAll)] #[serde(deny_unknown_fields)] @@ -241,7 +265,7 @@ impl RadialBasisFunction { /// Convert the schema representation of the RBF into `pywr_core` type. /// /// If required this will estimate values of from the provided points. - fn into_core_rbf(self, points: &[(u32, f64)]) -> Result { + fn into_core_rbf(self, points: &[(u64, f64)]) -> Result { let rbf = match self { Self::Linear => pywr_core::parameters::RadialBasisFunction::Linear, Self::Cubic => pywr_core::parameters::RadialBasisFunction::Cubic, @@ -281,7 +305,7 @@ impl RadialBasisFunction { /// /// If there `points` is empty then `None` is returned. #[cfg(feature = "core")] -fn estimate_epsilon(points: &[(u32, f64)]) -> Option { +fn estimate_epsilon(points: &[(u64, f64)]) -> Option { if points.is_empty() { return None; } @@ -307,13 +331,13 @@ fn estimate_epsilon(points: &[(u32, f64)]) -> Option { /// Settings for a variable RBF profile. #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Copy, JsonSchema, PywrVisitAll)] #[serde(deny_unknown_fields)] -pub struct RbfProfileVariableSettings { +pub struct RbfProfileVariableConfig { /// Is this parameter an active variable? pub is_active: bool, /// Optional maximum number of days that the interpolation points can be moved from their /// original position. If this is `None` then the points can not be moved from their /// original day of the year. - pub days_of_year_range: Option, + pub days_of_year_range: Option, /// Optional upper bound for the value of each interpolation point. If this is `None` then /// there is no upper bound. pub value_upper_bounds: Option, @@ -323,8 +347,8 @@ pub struct RbfProfileVariableSettings { } #[cfg(feature = "core")] -impl From for pywr_core::parameters::RbfProfileVariableConfig { - fn from(settings: RbfProfileVariableSettings) -> Self { +impl From for pywr_core::parameters::RbfProfileVariableConfig { + fn from(settings: RbfProfileVariableConfig) -> Self { Self::new( settings.days_of_year_range, settings.value_upper_bounds.unwrap_or(f64::INFINITY), @@ -358,12 +382,12 @@ pub struct RbfProfileParameter { pub meta: ParameterMeta, /// The points are the profile positions defined by an ordinal day of the year and a value. /// Radial basis function interpolation is used to create a daily profile from these points. - pub points: Vec<(u32, f64)>, + pub points: Vec<(u64, f64)>, /// The distance function used for interpolation. pub function: RadialBasisFunction, /// Optional settings for configuring how the value of this parameter can be varied. This /// is used by, for example, external algorithms to optimise the value of the parameter. - pub variable: Option, + pub variable: Option, } #[cfg(feature = "core")] @@ -394,7 +418,12 @@ impl TryFromV1 for RbfProfileParameter { ) -> Result { let meta: ParameterMeta = v1.meta.try_into_v2(parent_node, conversion_data)?; - let points = v1.days_of_year.into_iter().zip(v1.values).collect(); + let points = v1 + .days_of_year + .into_iter() + .map(|doy| doy as u64) + .zip(v1.values) + .collect(); if v1.rbf_kwargs.contains_key("smooth") { return Err(Box::new(ComponentConversionError::Parameter { diff --git a/pywr-schema/src/timeseries/pandas_load.py b/pywr-schema/src/timeseries/pandas_load.py index 2a0f1851..f74082ff 100644 --- a/pywr-schema/src/timeseries/pandas_load.py +++ b/pywr-schema/src/timeseries/pandas_load.py @@ -9,9 +9,9 @@ def load_pandas(path: str, index_col: Union[str, int], **kwargs) -> pl.DataFrame This function is used by the `load` function of the `PandasDataset` in the Rust extension. """ - suffix = Path(path).suffix.lower() + suffix = "".join(Path(path).suffixes).lower() match suffix: - case ".csv": + case ".csv" | ".csv.gz": df = pd.read_csv(path, index_col=index_col, parse_dates=True, **kwargs) case ".xlsx": df = pd.read_excel(path, index_col=index_col, **kwargs) diff --git a/pywr-schema/src/variable_config.rs b/pywr-schema/src/variable_config.rs new file mode 100644 index 00000000..cc31c43e --- /dev/null +++ b/pywr-schema/src/variable_config.rs @@ -0,0 +1,92 @@ +#[cfg(feature = "core")] +use crate::SchemaError; +use crate::parameters::{ActivationFunction, MonthlyProfileVariableConfig, RbfProfileVariableConfig}; +#[cfg(feature = "core")] +use pywr_core::parameters::ParameterName; +use schemars::JsonSchema; +use std::path::{Path, PathBuf}; +use strum_macros::{Display, EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; +use thiserror::Error; + +/// Configuration of a variable parameter. +/// +/// The variant of this enum determines must match the type of the parameter it is +/// applied to. +#[derive(serde::Deserialize, serde::Serialize, Debug, EnumDiscriminants, Clone, JsonSchema, Display)] +#[serde(tag = "type")] +#[strum_discriminants(derive(Display, IntoStaticStr, EnumString, VariantNames))] +// This creates a separate enum called `ParameterType` that is available in this module. +#[strum_discriminants(name(ParameterType))] +pub enum VariableConfig { + ActivationFunction(ActivationFunction), + RbfProfile(RbfProfileVariableConfig), + MonthlyProfile(MonthlyProfileVariableConfig), +} + +#[cfg(feature = "core")] +impl VariableConfig { + fn load(&self) -> Box { + match self { + VariableConfig::ActivationFunction(activation_function) => { + Box::::new((*activation_function).into()) + } + VariableConfig::RbfProfile(rbf_profile) => { + Box::::new((*rbf_profile).into()) + } + VariableConfig::MonthlyProfile(monthly_profile) => { + Box::::new((*monthly_profile).into()) + } + } + } +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, JsonSchema)] +pub struct NamedVariableConfig { + pub name: String, + pub config: VariableConfig, +} + +#[derive(Error, Debug)] +pub enum VariableConfigsReadError { + #[error("IO error on path `{path}`: {error}")] + IO { path: PathBuf, error: std::io::Error }, + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, Default, JsonSchema)] +pub struct VariableConfigs { + pub configs: Vec, +} + +impl VariableConfigs { + pub fn from_path>(path: P) -> Result { + let data = std::fs::read_to_string(&path).map_err(|error| VariableConfigsReadError::IO { + path: path.as_ref().to_path_buf(), + error, + })?; + Ok(serde_json::from_str(data.as_str())?) + } +} + +#[cfg(feature = "core")] +impl VariableConfigs { + /// Convert the variable configuration to a "core" network variable configuration. + /// + /// This is required in order to apply the built configuration to the built core network. + pub fn build_config( + &self, + network: &pywr_core::network::Network, + ) -> Result { + let mut builder = pywr_core::network_variable_config::NetworkVariableConfigBuilder::new(network); + + for config in &self.configs { + let core_config = config.config.load(); + let name: ParameterName = config.name.as_str().into(); + + builder = builder.add_variable_config(&name, core_config)?; + } + + Ok(builder.build()) + } +} diff --git a/pywr-schema/tests/memory1.json b/pywr-schema/tests/memory1.json index 306bf4e3..42467798 100644 --- a/pywr-schema/tests/memory1.json +++ b/pywr-schema/tests/memory1.json @@ -104,7 +104,7 @@ "time": { "type": "CountNonZero" }, - "metrics": { + "metric": { "type": "Max" }, "scenario": {