From 100a9fd1a7b7d52984dd807fe1400e74452c694a Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:49:51 +0100 Subject: [PATCH] feat: Add division parameter (#40) --------- Co-authored-by: James Tomlinson --- src/lib.rs | 2 + src/parameters/division.rs | 55 +++++++++++++++++++++++++ src/parameters/mod.rs | 2 + src/schema/parameters/core.rs | 75 ++++++++++++++++++++++++++++++++++- src/schema/parameters/mod.rs | 8 +++- 5 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/parameters/division.rs diff --git a/src/lib.rs b/src/lib.rs index fb0c417d..f1250dcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,8 @@ pub enum PywrError { MetricNotDefinedForNode, #[error("invalid metric type: {0}")] InvalidMetricType(String), + #[error("invalid metric value: {0}")] + InvalidMetricValue(String), #[error("recorder not initialised")] RecorderNotInitialised, #[error("hdf5 error: {0}")] diff --git a/src/parameters/division.rs b/src/parameters/division.rs new file mode 100644 index 00000000..f3652fba --- /dev/null +++ b/src/parameters/division.rs @@ -0,0 +1,55 @@ +use super::PywrError; +use crate::metric::Metric; +use crate::model::Model; +use crate::parameters::{Parameter, ParameterMeta}; +use crate::scenario::ScenarioIndex; +use crate::state::State; +use crate::timestep::Timestep; +use crate::PywrError::InvalidMetricValue; +use std::any::Any; + +pub struct DivisionParameter { + meta: ParameterMeta, + numerator: Metric, + denominator: Metric, +} + +impl DivisionParameter { + pub fn new(name: &str, numerator: Metric, denominator: Metric) -> Self { + Self { + meta: ParameterMeta::new(name), + numerator, + denominator, + } + } +} + +impl Parameter for DivisionParameter { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn meta(&self) -> &ParameterMeta { + &self.meta + } + fn compute( + &self, + _timestep: &Timestep, + _scenario_index: &ScenarioIndex, + model: &Model, + state: &State, + _internal_state: &mut Option>, + ) -> Result { + // TODO handle scenarios + let denominator = self.denominator.get_value(model, state)?; + + if denominator == 0.0 { + return Err(InvalidMetricValue(format!( + "Division by zero creates a NaN in {}.", + self.name() + ))); + } + + let numerator = self.numerator.get_value(model, state)?; + Ok(numerator / denominator) + } +} diff --git a/src/parameters/mod.rs b/src/parameters/mod.rs index b74030bc..d6cdd38e 100644 --- a/src/parameters/mod.rs +++ b/src/parameters/mod.rs @@ -4,6 +4,7 @@ mod array; mod asymmetric; mod control_curves; mod delay; +mod division; mod indexed_array; mod max; mod min; @@ -32,6 +33,7 @@ pub use control_curves::{ PiecewiseInterpolatedParameter, }; pub use delay::DelayParameter; +pub use division::DivisionParameter; pub use indexed_array::IndexedArrayParameter; pub use max::MaxParameter; pub use min::MinParameter; diff --git a/src/schema/parameters/core.rs b/src/schema/parameters/core.rs index b84669b6..d93bf8e4 100644 --- a/src/schema/parameters/core.rs +++ b/src/schema/parameters/core.rs @@ -6,8 +6,8 @@ use crate::schema::parameters::{ }; use crate::{ParameterIndex, PywrError}; use pywr_schema::parameters::{ - ConstantParameter as ConstantParameterV1, MaxParameter as MaxParameterV1, MinParameter as MinParameterV1, - NegativeParameter as NegativeParameterV1, + ConstantParameter as ConstantParameterV1, DivisionParameter as DivisionParameterV1, MaxParameter as MaxParameterV1, + MinParameter as MinParameterV1, NegativeParameter as NegativeParameterV1, }; use std::collections::HashMap; use std::path::Path; @@ -115,6 +115,77 @@ impl TryFromV1Parameter for MaxParameter { } } +/// This parameter divides one Parameter by another. +/// +/// # Arguments +/// +/// * `numerator` - The parameter to use as the numerator (or dividend). +/// * `denominator` - The parameter to use as the denominator (or divisor). +/// +/// # Examples +/// +/// ```json +/// { +/// "type": "Division", +/// "numerator": { +/// "type": "MonthlyProfile", +/// "values": [1, 4, 5, 9, 1, 5, 10, 8, 11, 9, 11 ,12] +/// }, +/// "denominator": { +/// "type": "Constant", +/// "value": 0.3 +/// } +/// } +/// ``` +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct DivisionParameter { + #[serde(flatten)] + pub meta: ParameterMeta, + pub numerator: DynamicFloatValue, + pub denominator: DynamicFloatValue, +} + +impl DivisionParameter { + pub fn node_references(&self) -> HashMap<&str, &str> { + HashMap::new() + } + + pub fn add_to_model( + &self, + model: &mut crate::model::Model, + tables: &LoadedTableCollection, + data_path: Option<&Path>, + ) -> Result { + let n = self.numerator.load(model, tables, data_path)?; + let d = self.denominator.load(model, tables, data_path)?; + + let p = crate::parameters::DivisionParameter::new(&self.meta.name, n, d); + model.add_parameter(Box::new(p)) + } +} + +impl TryFromV1Parameter for DivisionParameter { + type Error = ConversionError; + + fn try_from_v1_parameter( + v1: DivisionParameterV1, + parent_node: Option<&str>, + unnamed_count: &mut usize, + ) -> Result { + let meta: ParameterMeta = v1.meta.into_v2_parameter(parent_node, unnamed_count); + + let numerator = v1.numerator.try_into_v2_parameter(Some(&meta.name), unnamed_count)?; + let denominator = v1.denominator.try_into_v2_parameter(Some(&meta.name), unnamed_count)?; + + let p = Self { + meta, + numerator, + denominator, + }; + Ok(p) + } +} + /// This parameter takes the minimum of another Parameter and a constant value (threshold). /// /// # Arguments diff --git a/src/schema/parameters/mod.rs b/src/schema/parameters/mod.rs index 9d152ded..ef779491 100644 --- a/src/schema/parameters/mod.rs +++ b/src/schema/parameters/mod.rs @@ -31,6 +31,7 @@ pub use super::parameters::thresholds::ParameterThresholdParameter; use crate::metric::Metric; use crate::parameters::{IndexValue, ParameterType}; use crate::schema::error::ConversionError; +use crate::schema::parameters::core::DivisionParameter; pub use crate::schema::parameters::data_frame::DataFrameParameter; use crate::{IndexParameterIndex, NodeIndex, PywrError}; use pywr_schema::parameters::{ @@ -143,6 +144,7 @@ pub enum Parameter { Python(PythonParameter), DataFrame(DataFrameParameter), Delay(DelayParameter), + Division(DivisionParameter), } impl Parameter { @@ -168,6 +170,7 @@ impl Parameter { Self::TablesArray(p) => p.meta.name.as_str(), Self::Python(p) => p.meta.name.as_str(), Self::DataFrame(p) => p.meta.name.as_str(), + Self::Division(p) => p.meta.name.as_str(), Parameter::Delay(p) => p.meta.name.as_str(), } } @@ -197,6 +200,7 @@ impl Parameter { Self::Python(p) => p.node_references(), Self::DataFrame(p) => p.node_references(), Self::Delay(p) => p.node_references(), + Self::Division(p) => p.node_references(), } } @@ -242,6 +246,7 @@ impl Parameter { Self::Python(_) => "Python", Self::DataFrame(_) => "DataFrame", Self::Delay(_) => "Delay", + Self::Division(_) => "Division", } } @@ -275,6 +280,7 @@ impl Parameter { Self::Python(p) => p.add_to_model(model, tables, data_path)?, Self::DataFrame(p) => ParameterType::Parameter(p.add_to_model(model, data_path)?), Self::Delay(p) => ParameterType::Parameter(p.add_to_model(model, tables, data_path)?), + Self::Division(p) => ParameterType::Parameter(p.add_to_model(model, tables, data_path)?), }; Ok(ty) @@ -338,7 +344,7 @@ impl TryFromV1Parameter for Parameter { Parameter::TablesArray(p.try_into_v2_parameter(parent_node, unnamed_count)?) } CoreParameter::Min(p) => Parameter::Min(p.try_into_v2_parameter(parent_node, unnamed_count)?), - CoreParameter::Division(_) => todo!(), + CoreParameter::Division(p) => Parameter::Division(p.try_into_v2_parameter(parent_node, unnamed_count)?), }, ParameterV1::Custom(p) => { println!("Custom parameter: {:?} ({})", p.meta.name, p.ty);