From b3d743ef85d4ad14711164be94c0ab568d41f097 Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:06:50 +0100 Subject: [PATCH] feat: Implement NegativeMaxParameter and NegativeMinParameter (#79) --- Cargo.toml | 2 +- pywr-core/src/parameters/mod.rs | 4 + pywr-core/src/parameters/negativemax.rs | 40 +++++++ pywr-core/src/parameters/negativemin.rs | 40 +++++++ pywr-schema/src/parameters/core.rs | 135 +++++++++++++++++++++++- pywr-schema/src/parameters/mod.rs | 17 ++- 6 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 pywr-core/src/parameters/negativemax.rs create mode 100644 pywr-core/src/parameters/negativemin.rs diff --git a/Cargo.toml b/Cargo.toml index da6b96ff..07e2a22e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,4 +47,4 @@ tracing = { version = "0.1", features = ["log"] } csv = "1.1" hdf5 = { git = "https://github.com/aldanor/hdf5-rust.git", package = "hdf5", features = ["static", "zlib"] } pywr-v1-schema = { git = "https://github.com/pywr/pywr-schema/", tag = "v0.11.0", package = "pywr-schema" } -chrono = { version = "0.4.34" } +chrono = { version = "0.4.34" } \ No newline at end of file diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 7bbcc9e5..0c0ce37f 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -14,6 +14,8 @@ mod interpolated; mod max; mod min; mod negative; +mod negativemax; +mod negativemin; mod offset; mod polynomial; mod profiles; @@ -49,6 +51,8 @@ pub use interpolated::InterpolatedParameter; pub use max::MaxParameter; pub use min::MinParameter; pub use negative::NegativeParameter; +pub use negativemax::NegativeMaxParameter; +pub use negativemin::NegativeMinParameter; pub use offset::OffsetParameter; pub use polynomial::Polynomial1DParameter; pub use profiles::{ diff --git a/pywr-core/src/parameters/negativemax.rs b/pywr-core/src/parameters/negativemax.rs new file mode 100644 index 00000000..b2e9620d --- /dev/null +++ b/pywr-core/src/parameters/negativemax.rs @@ -0,0 +1,40 @@ +use crate::metric::MetricF64; +use crate::network::Network; +use crate::parameters::{Parameter, ParameterMeta}; +use crate::scenario::ScenarioIndex; +use crate::state::{ParameterState, State}; +use crate::timestep::Timestep; +use crate::PywrError; + +pub struct NegativeMaxParameter { + meta: ParameterMeta, + metric: MetricF64, + threshold: f64, +} + +impl NegativeMaxParameter { + pub fn new(name: &str, metric: MetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl Parameter for NegativeMaxParameter { + fn meta(&self) -> &ParameterMeta { + &self.meta + } + fn compute( + &self, + _timestep: &Timestep, + _scenario_index: &ScenarioIndex, + network: &Network, + state: &State, + _internal_state: &mut Option>, + ) -> Result { + let x = -self.metric.get_value(network, state)?; + Ok(x.max(self.threshold)) + } +} diff --git a/pywr-core/src/parameters/negativemin.rs b/pywr-core/src/parameters/negativemin.rs new file mode 100644 index 00000000..a92fc614 --- /dev/null +++ b/pywr-core/src/parameters/negativemin.rs @@ -0,0 +1,40 @@ +use crate::metric::MetricF64; +use crate::network::Network; +use crate::parameters::{Parameter, ParameterMeta}; +use crate::scenario::ScenarioIndex; +use crate::state::{ParameterState, State}; +use crate::timestep::Timestep; +use crate::PywrError; + +pub struct NegativeMinParameter { + meta: ParameterMeta, + metric: MetricF64, + threshold: f64, +} + +impl NegativeMinParameter { + pub fn new(name: &str, metric: MetricF64, threshold: f64) -> Self { + Self { + meta: ParameterMeta::new(name), + metric, + threshold, + } + } +} + +impl Parameter for NegativeMinParameter { + fn meta(&self) -> &ParameterMeta { + &self.meta + } + fn compute( + &self, + _timestep: &Timestep, + _scenario_index: &ScenarioIndex, + network: &Network, + state: &State, + _internal_state: &mut Option>, + ) -> Result { + let x = -self.metric.get_value(network, state)?; + Ok(x.min(self.threshold)) + } +} diff --git a/pywr-schema/src/parameters/core.rs b/pywr-schema/src/parameters/core.rs index a5dde37e..e65c4ff8 100644 --- a/pywr-schema/src/parameters/core.rs +++ b/pywr-schema/src/parameters/core.rs @@ -7,7 +7,8 @@ use crate::parameters::{ use pywr_core::parameters::ParameterIndex; use pywr_v1_schema::parameters::{ ConstantParameter as ConstantParameterV1, DivisionParameter as DivisionParameterV1, MaxParameter as MaxParameterV1, - MinParameter as MinParameterV1, NegativeParameter as NegativeParameterV1, + MinParameter as MinParameterV1, NegativeMaxParameter as NegativeMaxParameterV1, + NegativeMinParameter as NegativeMinParameterV1, NegativeParameter as NegativeParameterV1, }; use std::collections::HashMap; @@ -430,3 +431,135 @@ impl TryFromV1Parameter for NegativeParameter { Ok(p) } } + +/// This parameter takes the maximum of the negative of a metric and a constant value (threshold). +/// +/// # Arguments +/// +/// * `metric` - The metric value to compare with the float. +/// * `threshold` - The threshold value to compare against the given parameter. Default to 0.0. +/// +/// # Examples +/// +/// ```json +/// { +/// "type": "NegativeMax", +/// "metric": { +/// "type": "MonthlyProfile", +/// "values": [-1, -4, 5, 9, 1, 5, 10, 8, 11, 9, 11 ,12] +/// }, +/// "threshold": 2 +/// } +/// ``` +/// In January this parameter returns 2, in February 4. +/// +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct NegativeMaxParameter { + #[serde(flatten)] + pub meta: ParameterMeta, + pub metric: Metric, + pub threshold: Option, +} + +impl NegativeMaxParameter { + pub fn node_references(&self) -> HashMap<&str, &str> { + HashMap::new() + } + + pub fn add_to_model( + &self, + network: &mut pywr_core::network::Network, + args: &LoadArgs, + ) -> Result, SchemaError> { + let idx = self.metric.load(network, args)?; + let threshold = self.threshold.unwrap_or(0.0); + + let p = pywr_core::parameters::NegativeMaxParameter::new(&self.meta.name, idx, threshold); + Ok(network.add_parameter(Box::new(p))?) + } +} + +impl TryFromV1Parameter for NegativeMaxParameter { + type Error = ConversionError; + + fn try_from_v1_parameter( + v1: NegativeMaxParameterV1, + parent_node: Option<&str>, + unnamed_count: &mut usize, + ) -> Result { + let meta: ParameterMeta = v1.meta.into_v2_parameter(parent_node, unnamed_count); + let parameter = v1.parameter.try_into_v2_parameter(Some(&meta.name), unnamed_count)?; + let p = Self { + meta, + metric: parameter, + threshold: v1.threshold, + }; + Ok(p) + } +} + +/// This parameter takes the minimum of the negative of a metric and a constant value (threshold). +/// +/// # Arguments +/// +/// * `metric` - The metric value to compare with the float. +/// * `threshold` - The threshold value to compare against the given parameter. Default to 0.0. +/// +/// # Examples +/// +/// ```json +/// { +/// "type": "NegativeMin", +/// "metric": { +/// "type": "MonthlyProfile", +/// "values": [-1, -4, 5, 9, 1, 5, 10, 8, 11, 9, 11 ,12] +/// }, +/// "threshold": 2 +/// } +/// ``` +/// In January this parameter returns 1, in February 2. +/// +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct NegativeMinParameter { + #[serde(flatten)] + pub meta: ParameterMeta, + pub metric: Metric, + pub threshold: Option, +} + +impl NegativeMinParameter { + pub fn node_references(&self) -> HashMap<&str, &str> { + HashMap::new() + } + + pub fn add_to_model( + &self, + network: &mut pywr_core::network::Network, + args: &LoadArgs, + ) -> Result, SchemaError> { + let idx = self.metric.load(network, args)?; + let threshold = self.threshold.unwrap_or(0.0); + + let p = pywr_core::parameters::NegativeMinParameter::new(&self.meta.name, idx, threshold); + Ok(network.add_parameter(Box::new(p))?) + } +} + +impl TryFromV1Parameter for NegativeMinParameter { + type Error = ConversionError; + + fn try_from_v1_parameter( + v1: NegativeMinParameterV1, + parent_node: Option<&str>, + unnamed_count: &mut usize, + ) -> Result { + let meta: ParameterMeta = v1.meta.into_v2_parameter(parent_node, unnamed_count); + let parameter = v1.parameter.try_into_v2_parameter(Some(&meta.name), unnamed_count)?; + let p = Self { + meta, + metric: parameter, + threshold: v1.threshold, + }; + Ok(p) + } +} diff --git a/pywr-schema/src/parameters/mod.rs b/pywr-schema/src/parameters/mod.rs index e528b391..ff1d9e27 100644 --- a/pywr-schema/src/parameters/mod.rs +++ b/pywr-schema/src/parameters/mod.rs @@ -30,7 +30,8 @@ pub use super::parameters::control_curves::{ ControlCurvePiecewiseInterpolatedParameter, }; pub use super::parameters::core::{ - ActivationFunction, ConstantParameter, MaxParameter, MinParameter, NegativeParameter, VariableSettings, + ActivationFunction, ConstantParameter, MaxParameter, MinParameter, NegativeMaxParameter, NegativeMinParameter, + NegativeParameter, VariableSettings, }; pub use super::parameters::delay::DelayParameter; pub use super::parameters::discount_factor::DiscountFactorParameter; @@ -160,6 +161,8 @@ pub enum Parameter { Max(MaxParameter), Min(MinParameter), Negative(NegativeParameter), + NegativeMax(NegativeMaxParameter), + NegativeMin(NegativeMinParameter), Polynomial1D(Polynomial1DParameter), ParameterThreshold(ParameterThresholdParameter), TablesArray(TablesArrayParameter), @@ -201,6 +204,8 @@ impl Parameter { Self::DiscountFactor(p) => p.meta.name.as_str(), Self::Interpolated(p) => p.meta.name.as_str(), Self::RbfProfile(p) => p.meta.name.as_str(), + Self::NegativeMax(p) => p.meta.name.as_str(), + Self::NegativeMin(p) => p.meta.name.as_str(), } } @@ -249,6 +254,8 @@ impl Parameter { Self::DiscountFactor(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args)?), Self::Interpolated(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args)?), Self::RbfProfile(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network)?), + Self::NegativeMax(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args)?), + Self::NegativeMin(p) => pywr_core::parameters::ParameterType::Parameter(p.add_to_model(network, args)?), }; Ok(ty) @@ -435,8 +442,6 @@ impl TryFromV1Parameter for ParameterOrTimeseries { CoreParameter::InterpolatedFlow(p) => { Parameter::Interpolated(p.try_into_v2_parameter(parent_node, unnamed_count)?).into() } - CoreParameter::NegativeMax(_) => todo!("Implement NegativeMaxParameter"), - CoreParameter::NegativeMin(_) => todo!("Implement NegativeMinParameter"), CoreParameter::HydropowerTarget(_) => todo!("Implement HydropowerTargetParameter"), CoreParameter::WeeklyProfile(p) => { Parameter::WeeklyProfile(p.try_into_v2_parameter(parent_node, unnamed_count)?).into() @@ -460,6 +465,12 @@ impl TryFromV1Parameter for ParameterOrTimeseries { CoreParameter::RbfProfile(p) => { Parameter::RbfProfile(p.try_into_v2_parameter(parent_node, unnamed_count)?).into() } + CoreParameter::NegativeMax(p) => { + Parameter::NegativeMax(p.try_into_v2_parameter(parent_node, unnamed_count)?).into() + } + CoreParameter::NegativeMin(p) => { + Parameter::NegativeMin(p.try_into_v2_parameter(parent_node, unnamed_count)?).into() + } }, ParameterV1::Custom(p) => { println!("Custom parameter: {:?} ({})", p.meta.name, p.ty);