From 74b6ee4bcbbae87e259a520fcd741fae02173b93 Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:09:05 +0100 Subject: [PATCH 1/7] Added soft_min and soft_max fields to LinkNode --- pywr-schema/src/nodes/core.rs | 125 +++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 6a5fd6c9..c17d20f3 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -131,23 +131,102 @@ impl TryFrom for InputNode { } } +/// Cost and flow metric for soft node's constraints #[derive(serde::Deserialize, serde::Serialize, Clone, Default, Debug, JsonSchema, PywrVisitAll)] +pub struct SoftConstraint { + pub cost: Option, + pub flow: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, Default, Debug, JsonSchema, PywrVisitAll)] +#[doc = svgbobdoc::transform!( +/// A node with cost, and min and max flow constraints. The node `L`, when connected to an upstream +/// node `U` and downstream node `D`, will look like this on the model schematic: +/// +/// ```svgbob +/// +/// U L D +/// - - ->*-------->*--------->*- - - +/// ``` +/// +/// # Soft constraints +/// This node allows setting optional maximum and minimum soft constraints which means the node's `min_flow` +/// and `max_flow` properties may be breached, as specified by the user in the `soft_max` and `soft_min` +/// properties. When the two attributes are provided, the internal representation of the link will +/// look like this: +/// +/// ```svgbob +/// .soft_max +/// .------>L_max -----. +/// | | +/// U | | D +/// - - -*--|-------->L --------|--->*- - - +/// | | +/// | | +/// '------>L_min -----' +/// .soft_min +/// ``` +/// +/// Link soft constraints may be used in the following scenarios: +/// 1) If the link represents a works and its `max_flow` is constrained by a reservoir rule curve, +/// there may be certain circumstances when over-abstracting may be required in a few occasions to +/// ensure that demand is always met. By setting a high tuned cost via [`SoftConstraint`], this will +/// ensure that the abstraction is breached only when needed. +/// 2) If the link represents a works and a minimum flow must be guaranteed, `soft_min` may be set +/// with a negative cost to allow the minimum flow requirement. However when this cannot be met (for +/// example when the abstraction license or the source runs out), the minimum flow will not +/// be honoured and the solver will find a solution. +)] pub struct LinkNode { #[serde(flatten)] pub meta: NodeMeta, + /// The optional maximum flow through the node. pub max_flow: Option, + /// The optional minimum flow through the node. pub min_flow: Option, + /// The cost. pub cost: Option, + /// The minimum soft constraints. + pub soft_min: Option, + /// The maximum soft constraints. + pub soft_max: Option, } impl LinkNode { const DEFAULT_ATTRIBUTE: NodeAttribute = NodeAttribute::Outflow; pub fn input_connectors(&self) -> Vec<(&str, Option)> { - vec![(self.meta.name.as_str(), None)] + let mut connectors = vec![(self.meta.name.as_str(), None)]; + if self.soft_min.is_some() { + connectors.push(( + self.meta.name.as_str(), + Self::soft_min_node_sub_name().map(|s| s.to_string()), + )); + } + if self.soft_max.is_some() { + connectors.push(( + self.meta.name.as_str(), + Self::soft_max_node_sub_name().map(|s| s.to_string()), + )); + } + connectors } + pub fn output_connectors(&self) -> Vec<(&str, Option)> { - vec![(self.meta.name.as_str(), None)] + let mut connectors = vec![(self.meta.name.as_str(), None)]; + if self.soft_min.is_some() { + connectors.push(( + self.meta.name.as_str(), + Self::soft_min_node_sub_name().map(|s| s.to_string()), + )); + } + if self.soft_max.is_some() { + connectors.push(( + self.meta.name.as_str(), + Self::soft_max_node_sub_name().map(|s| s.to_string()), + )); + } + connectors } pub fn default_metric(&self) -> NodeAttribute { @@ -157,8 +236,23 @@ impl LinkNode { #[cfg(feature = "core")] impl LinkNode { + fn soft_min_node_sub_name() -> Option<&'static str> { + Some("soft_min_node") + } + + fn soft_max_node_sub_name() -> Option<&'static str> { + Some("soft_max_node") + } + pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { network.add_link_node(self.meta.name.as_str(), None)?; + + if self.soft_min.is_some() { + network.add_link_node(self.meta.name.as_str(), Self::soft_min_node_sub_name())?; + } + if self.soft_max.is_some() { + network.add_link_node(self.meta.name.as_str(), Self::soft_max_node_sub_name())?; + } Ok(()) } @@ -182,6 +276,28 @@ impl LinkNode { network.set_node_min_flow(self.meta.name.as_str(), None, value.into())?; } + if let Some(soft_min) = &self.soft_min { + if let Some(soft_min_flow) = &soft_min.flow { + let value = soft_min_flow.load(network, args)?; + network.set_node_min_flow(self.meta.name.as_str(), Self::soft_min_node_sub_name(), value.into())?; + } + if let Some(soft_min_cost) = &soft_min.cost { + let value = soft_min_cost.load(network, args)?; + network.set_node_cost(self.meta.name.as_str(), Self::soft_min_node_sub_name(), value.into())?; + } + } + + if let Some(soft_max) = &self.soft_max { + if let Some(soft_max_flow) = &soft_max.flow { + let value = soft_max_flow.load(network, args)?; + network.set_node_max_flow(self.meta.name.as_str(), Self::soft_max_node_sub_name(), value.into())?; + } + if let Some(soft_max_cost) = &soft_max.cost { + let value = soft_max_cost.load(network, args)?; + network.set_node_cost(self.meta.name.as_str(), Self::soft_max_node_sub_name(), value.into())?; + } + } + Ok(()) } @@ -230,11 +346,16 @@ impl TryFrom for LinkNode { .cost .map(|v| v.try_into_v2_parameter(Some(&meta.name), &mut unnamed_count)) .transpose()?; + // not supported in V1 + let soft_min = None; + let soft_max = None; let n = Self { meta, max_flow, min_flow, + soft_min, + soft_max, cost, }; Ok(n) From 781b970b4611a864974addf0d2060381aad55adb Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:13:59 +0100 Subject: [PATCH 2/7] Fixed v1 conversion test with soft link missing attributes --- pywr-schema/src/test_models/v1/timeseries-converted.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pywr-schema/src/test_models/v1/timeseries-converted.json b/pywr-schema/src/test_models/v1/timeseries-converted.json index 8fae7648..9ea6f714 100644 --- a/pywr-schema/src/test_models/v1/timeseries-converted.json +++ b/pywr-schema/src/test_models/v1/timeseries-converted.json @@ -42,6 +42,8 @@ "name": "link1", "max_flow": null, "min_flow": null, + "soft_max": null, + "soft_min": null, "cost": null }, { From 1ee85369bb1d1f1c7c053c582c958e2404db1723 Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:17:42 +0100 Subject: [PATCH 3/7] Made sub-node name methods available outside core feature --- pywr-schema/src/nodes/core.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index c17d20f3..f5cd60eb 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -195,6 +195,14 @@ pub struct LinkNode { impl LinkNode { const DEFAULT_ATTRIBUTE: NodeAttribute = NodeAttribute::Outflow; + fn soft_min_node_sub_name() -> Option<&'static str> { + Some("soft_min_node") + } + + fn soft_max_node_sub_name() -> Option<&'static str> { + Some("soft_max_node") + } + pub fn input_connectors(&self) -> Vec<(&str, Option)> { let mut connectors = vec![(self.meta.name.as_str(), None)]; if self.soft_min.is_some() { @@ -236,14 +244,6 @@ impl LinkNode { #[cfg(feature = "core")] impl LinkNode { - fn soft_min_node_sub_name() -> Option<&'static str> { - Some("soft_min_node") - } - - fn soft_max_node_sub_name() -> Option<&'static str> { - Some("soft_max_node") - } - pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { network.add_link_node(self.meta.name.as_str(), None)?; From 8a92f5e3e12fa2b9686aaff316aaf82f1249b493 Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:30:43 +0100 Subject: [PATCH 4/7] Added new soft constraints and aggregated node Improved documentation Added tests --- pywr-schema/src/nodes/core.rs | 331 +++++++++++++++--- .../src/test_models/link_with_soft_max.json | 68 ++++ .../src/test_models/link_with_soft_min.json | 76 ++++ 3 files changed, 432 insertions(+), 43 deletions(-) create mode 100644 pywr-schema/src/test_models/link_with_soft_max.json create mode 100644 pywr-schema/src/test_models/link_with_soft_min.json diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 5c1fe2da..ba11d439 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -164,15 +164,81 @@ pub struct SoftConstraint { /// '------>L_min -----' /// .soft_min /// ``` +/// ## Implementation /// +/// +/// ### Only `soft_min` is defined +/// Normally the minimum flow is delivered through `L_min` depending on the cost `soft_min.cost`. Any +/// additional flow goes through `L`. Depending on the network demand and the value of `soft_min.cost`, +/// the delivered flow via `L_min` may go below `soft_min.flow`. +/// ```svgbob +/// U D +/// - - -*----------->L ------------>*- - - +/// | | +/// | | () .aggregated_node +/// '------>L_min -----' [ L_min, L ] +/// .soft_min +/// ``` +/// +/// The network is set up as follows: +/// - `L_max` is not added to the network +/// - `L_min` is added with `soft_min` data +/// - `L` is added with `cost`, `min_flow` is set to 0 and `max_flow` is unconstrained. +/// - An aggregated node is added to ensure that combined flow in `L_min` and `L` never exceeds +/// the hard constraints `min_flow` and `max_flow`. +/// +/// ### Only `soft_max` is defined +/// Normally the maximum flow `soft_max.max` is delivered through the `L_max` node and no flow +/// goes through `L`. When needed, based on the value of `soft_max.cost`, the maximum `soft_max.max` +/// value can be breached up to a combined flow of `max_flow`. +/// ```svgbob +/// U D +/// - - -*----------->L ------------>*- - - +/// | | +/// | | () .aggregated_node +/// '------>L_max -----' [ L_max, L ] +/// .soft_max +/// ``` +/// +/// The network is set up as follows: +/// - `L_min` is not added to the network. +/// - `L` is added with the cost in `soft_max.cost` (i.e. cost of going above soft max). +/// - `L_max` is added with max flow of `soft_max.max` and cost of `cost`. +/// - An aggregated node is added to ensure that combined flow in `L_max` and `L` never exceeds +/// the hard constraints `min_flow` and `max_flow`. +/// +/// ### Both `soft_min` and `soft_max` are defined +/// +/// ```svgbob +/// .soft_max +/// .------>L_max -----. +/// | | () .aggregated_node +/// U | | D [ L_max, L_min, L ] +/// - - -*--|-------->L --------|--->*- - - +/// | | () .aggregate_node_l_l_min +/// | | [ L_min, L ] +/// '------>L_min -----' +/// .soft_min +/// ``` +/// +/// The network is set up as follows: +/// - `L_max`'s flow is unconstrained with a cost equal to `soft_max.cost`. +/// - `L`'s flow is unconstrained with a cost equal to `cost`. +/// - `L_min`'s max flow is constrained to `soft_min.flow` with a cost equal to `soft_min.cost`. +/// - An aggregated node is added with `L` and `L_min` to ensure the max flow does not exceed +/// `soft_max.flow`. +/// - An aggregated node is added with `L`, `L_max` and `L_min` to ensure the flow is between +/// `min_flow` and `max_flow`. +/// +/// ## Examples /// Link soft constraints may be used in the following scenarios: /// 1) If the link represents a works and its `max_flow` is constrained by a reservoir rule curve, /// there may be certain circumstances when over-abstracting may be required in a few occasions to /// ensure that demand is always met. By setting a high tuned cost via [`SoftConstraint`], this will /// ensure that the abstraction is breached only when needed. /// 2) If the link represents a works and a minimum flow must be guaranteed, `soft_min` may be set -/// with a negative cost to allow the minimum flow requirement. However when this cannot be met (for -/// example when the abstraction license or the source runs out), the minimum flow will not +/// with a negative cost to allow the minimum flow requirement. However, when this cannot be met +/// (for example when the abstraction license or the source runs out), the minimum flow will not /// be honoured and the solver will find a solution. )] pub struct LinkNode { @@ -201,6 +267,15 @@ impl LinkNode { Some("soft_max_node") } + fn aggregated_node_sub_name() -> Option<&'static str> { + Some("aggregate_node") + } + + /// The aggregated node name of `L` and `L_min` when both soft constraints are provided. + fn aggregated_node_l_l_min_sub_name() -> Option<&'static str> { + Some("aggregate_node_l_l_min") + } + pub fn input_connectors(&self) -> Vec<(&str, Option)> { let mut connectors = vec![(self.meta.name.as_str(), None)]; if self.soft_min.is_some() { @@ -243,14 +318,49 @@ impl LinkNode { #[cfg(feature = "core")] impl LinkNode { pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result<(), SchemaError> { - network.add_link_node(self.meta.name.as_str(), None)?; - - if self.soft_min.is_some() { - network.add_link_node(self.meta.name.as_str(), Self::soft_min_node_sub_name())?; - } - if self.soft_max.is_some() { - network.add_link_node(self.meta.name.as_str(), Self::soft_max_node_sub_name())?; - } + let node_name = self.meta.name.as_str(); + let link = network.add_link_node(node_name, None)?; + // add soft constrained nodes and aggregated node + match (&self.soft_min, &self.soft_max) { + (Some(_), None) => { + // add L_min and aggregated node for L and L_min + let soft_min_node = network.add_link_node(node_name, Self::soft_min_node_sub_name())?; + network.add_aggregated_node( + node_name, + Self::aggregated_node_sub_name(), + &[link, soft_min_node], + None, + )?; + } + (None, Some(_)) => { + // add L_max and aggregated node for L and L_max + let soft_max_node = network.add_link_node(node_name, Self::soft_max_node_sub_name())?; + network.add_aggregated_node( + node_name, + Self::aggregated_node_sub_name(), + &[link, soft_max_node], + None, + )?; + } + (Some(_), Some(_)) => { + // add L_min and L_max, and aggregated node for L, L_min and L_max + let soft_min_node = network.add_link_node(node_name, Self::soft_min_node_sub_name())?; + let soft_max_node = network.add_link_node(node_name, Self::soft_max_node_sub_name())?; + network.add_aggregated_node( + node_name, + Self::aggregated_node_sub_name(), + &[link, soft_min_node, soft_max_node], + None, + )?; + network.add_aggregated_node( + node_name, + Self::aggregated_node_l_l_min_sub_name(), + &[link, soft_min_node], + None, + )?; + } + _ => {} + }; Ok(()) } @@ -259,42 +369,121 @@ impl LinkNode { network: &mut pywr_core::network::Network, args: &LoadArgs, ) -> Result<(), SchemaError> { - if let Some(cost) = &self.cost { - let value = cost.load(network, args)?; - network.set_node_cost(self.meta.name.as_str(), None, value.into())?; - } - - if let Some(max_flow) = &self.max_flow { - let value = max_flow.load(network, args)?; - network.set_node_max_flow(self.meta.name.as_str(), None, value.into())?; - } + let node_name = self.meta.name.as_str(); + match (&self.soft_min, &self.soft_max) { + (None, None) => { + // soft constraints not added. Set constraints for L only + if let Some(cost) = &self.cost { + let value = cost.load(network, args)?; + network.set_node_cost(node_name, None, value.into())?; + } - if let Some(min_flow) = &self.min_flow { - let value = min_flow.load(network, args)?; - network.set_node_min_flow(self.meta.name.as_str(), None, value.into())?; - } + if let Some(max_flow) = &self.max_flow { + let value = max_flow.load(network, args)?; + network.set_node_max_flow(node_name, None, value.into())?; + } - if let Some(soft_min) = &self.soft_min { - if let Some(soft_min_flow) = &soft_min.flow { - let value = soft_min_flow.load(network, args)?; - network.set_node_min_flow(self.meta.name.as_str(), Self::soft_min_node_sub_name(), value.into())?; + if let Some(min_flow) = &self.min_flow { + let value = min_flow.load(network, args)?; + network.set_node_min_flow(node_name, None, value.into())?; + } } - if let Some(soft_min_cost) = &soft_min.cost { - let value = soft_min_cost.load(network, args)?; - network.set_node_cost(self.meta.name.as_str(), Self::soft_min_node_sub_name(), value.into())?; + (Some(soft_min), None) => { + // add L_min constraints + if let Some(soft_min_flow) = &soft_min.flow { + let value = soft_min_flow.load(network, args)?; + network.set_node_max_flow(node_name, Self::soft_min_node_sub_name(), value.into())?; + } + if let Some(soft_min_cost) = &soft_min.cost { + let value = soft_min_cost.load(network, args)?; + network.set_node_cost(node_name, Self::soft_min_node_sub_name(), value.into())?; + } + + // add cost on L + if let Some(cost) = &self.cost { + let value = cost.load(network, args)?; + network.set_node_cost(node_name, None, value.into())?; + } + + // add constraints on aggregated node + if let Some(max_flow) = &self.max_flow { + let value = max_flow.load(network, args)?; + network.set_aggregated_node_max_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + } + if let Some(min_flow) = &self.min_flow { + let value = min_flow.load(network, args)?; + network.set_aggregated_node_min_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + } } - } + (None, Some(soft_max)) => { + // add L_max constraints + if let Some(cost) = &self.cost { + let value = cost.load(network, args)?; + network.set_node_cost(node_name, Self::soft_max_node_sub_name(), value.into())?; + } + if let Some(soft_max_flow) = &soft_max.flow { + let value = soft_max_flow.load(network, args)?; + network.set_node_max_flow(node_name, Self::soft_max_node_sub_name(), value.into())?; + } - if let Some(soft_max) = &self.soft_max { - if let Some(soft_max_flow) = &soft_max.flow { - let value = soft_max_flow.load(network, args)?; - network.set_node_max_flow(self.meta.name.as_str(), Self::soft_max_node_sub_name(), value.into())?; + // add constraints on L + if let Some(soft_max_cost) = &soft_max.cost { + let value = soft_max_cost.load(network, args)?; + network.set_node_cost(node_name, None, value.into())?; + } + + // add constraints on aggregated node + if let Some(max_flow) = &self.max_flow { + let value = max_flow.load(network, args)?; + network.set_aggregated_node_max_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + } + if let Some(min_flow) = &self.min_flow { + let value = min_flow.load(network, args)?; + network.set_aggregated_node_min_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + } } - if let Some(soft_max_cost) = &soft_max.cost { - let value = soft_max_cost.load(network, args)?; - network.set_node_cost(self.meta.name.as_str(), Self::soft_max_node_sub_name(), value.into())?; + (Some(soft_min), Some(soft_max)) => { + // set L_max constraint + if let Some(soft_max_cost) = &soft_max.cost { + let value = soft_max_cost.load(network, args)?; + network.set_node_cost(node_name, Self::soft_max_node_sub_name(), value.into())?; + } + // set L constraint + if let Some(cost) = &self.cost { + let value = cost.load(network, args)?; + network.set_node_cost(node_name, None, value.into())?; + } + // set L_min constraints + if let Some(soft_min_flow) = &soft_min.flow { + let value = soft_min_flow.load(network, args)?; + network.set_node_max_flow(node_name, Self::soft_min_node_sub_name(), value.into())?; + } + if let Some(soft_min_cost) = &soft_min.cost { + let value = soft_min_cost.load(network, args)?; + network.set_node_cost(node_name, Self::soft_min_node_sub_name(), value.into())?; + } + + // add constraints on node aggregating all three nodes + if let Some(max_flow) = &self.max_flow { + let value = max_flow.load(network, args)?; + network.set_aggregated_node_max_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + } + if let Some(min_flow) = &self.min_flow { + let value = min_flow.load(network, args)?; + network.set_aggregated_node_min_flow(node_name, Self::aggregated_node_sub_name(), value.into())?; + } + + // add constraints on node aggregating `L` and `L_min` + if let Some(soft_max_flow) = &soft_max.flow { + let value = soft_max_flow.load(network, args)?; + network.set_aggregated_node_max_flow( + node_name, + Self::aggregated_node_l_l_min_sub_name(), + value.into(), + )?; + } } - } + }; Ok(()) } @@ -306,12 +495,36 @@ impl LinkNode { ) -> Result { // Use the default attribute if none is specified let attr = attribute.unwrap_or(Self::DEFAULT_ATTRIBUTE); - - let idx = network.get_node_index_by_name(self.meta.name.as_str(), None)?; + let node_name = self.meta.name.as_str(); + let link_node = network.get_node_index_by_name(node_name, None)?; + + // combine the flow through the nodes + let indices = match (&self.soft_min, &self.soft_max) { + (Some(_), None) => { + let soft_min_node = network.get_node_index_by_name(node_name, Self::soft_min_node_sub_name())?; + vec![link_node, soft_min_node] + } + (None, Some(_)) => { + let soft_max_node = network.get_node_index_by_name(node_name, Self::soft_max_node_sub_name())?; + vec![link_node, soft_max_node] + } + (Some(_), Some(_)) => { + let soft_min_node = network.get_node_index_by_name(node_name, Self::soft_min_node_sub_name())?; + let soft_max_node = network.get_node_index_by_name(node_name, Self::soft_max_node_sub_name())?; + vec![link_node, soft_min_node, soft_max_node] + } + (None, None) => vec![link_node], + }; let metric = match attr { - NodeAttribute::Outflow => MetricF64::NodeOutFlow(idx), - NodeAttribute::Inflow => MetricF64::NodeInFlow(idx), + NodeAttribute::Outflow => MetricF64::MultiNodeInFlow { + indices, + name: self.meta.name.to_string(), + }, + NodeAttribute::Inflow => MetricF64::MultiNodeOutFlow { + indices, + name: self.meta.name.to_string(), + }, _ => { return Err(SchemaError::NodeAttributeNotSupported { ty: "LinkNode".to_string(), @@ -1105,4 +1318,36 @@ mod tests { // Test all solvers run_all_solvers(&model, &[], &[]); } + + #[cfg(feature = "core")] + fn link_with_soft_min_str() -> &'static str { + include_str!("../test_models/link_with_soft_min.json") + } + + #[test] + #[cfg(feature = "core")] + fn test_link_with_soft_min() { + let data = link_with_soft_min_str(); + let schema = PywrModel::from_str(data).unwrap(); + let model: pywr_core::models::Model = schema.build_model(None, None).unwrap(); + + // Test all solvers + run_all_solvers(&model, &[], &[]); + } + + #[cfg(feature = "core")] + fn link_with_soft_max_str() -> &'static str { + include_str!("../test_models/link_with_soft_max.json") + } + + #[test] + #[cfg(feature = "core")] + fn test_link_with_soft_max() { + let data = link_with_soft_max_str(); + let schema = PywrModel::from_str(data).unwrap(); + let model: pywr_core::models::Model = schema.build_model(None, None).unwrap(); + + // Test all solvers + run_all_solvers(&model, &[], &[]); + } } diff --git a/pywr-schema/src/test_models/link_with_soft_max.json b/pywr-schema/src/test_models/link_with_soft_max.json new file mode 100644 index 00000000..f1ec3fd0 --- /dev/null +++ b/pywr-schema/src/test_models/link_with_soft_max.json @@ -0,0 +1,68 @@ +{ + "metadata": { + "title": "Link with a soft max constraint", + "description": "Test LinkNode with soft_max", + "minimum_version": "0.1" + }, + "timestepper": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": 1 + }, + "network": { + "nodes": [ + { + "name": "input", + "type": "Input" + }, + { + "name": "link", + "type": "Link", + "soft_max": { + "cost": { + "type": "Constant", + "value": 500 + }, + "flow": { + "type": "Constant", + "value": 30 + } + } + }, + { + "name": "demand", + "type": "Output", + "max_flow": { + "type": "Constant", + "value": 10.0 + }, + "cost": { + "type": "Constant", + "value": -10 + } + } + ], + "edges": [ + { + "from_node": "input", + "to_node": "link" + }, + { + "from_node": "link", + "to_node": "demand" + } + ] + }, + "metric_sets": [ + { + "name": "nodes", + "metrics": [ + { + "type": "Node", + "name": "link", + "attribute": "Inflow" + } + ] + } + ] +} diff --git a/pywr-schema/src/test_models/link_with_soft_min.json b/pywr-schema/src/test_models/link_with_soft_min.json new file mode 100644 index 00000000..b597e00d --- /dev/null +++ b/pywr-schema/src/test_models/link_with_soft_min.json @@ -0,0 +1,76 @@ +{ + "metadata": { + "title": "Link with a soft min constraint", + "description": "Test LinkNode with soft_min", + "minimum_version": "0.1" + }, + "timestepper": { + "start": "2015-01-01", + "end": "2015-01-03", + "timestep": 1 + }, + "network": { + "nodes": [ + { + "name": "input", + "type": "Input" + }, + { + "name": "link", + "type": "Link", + "max_flow": { + "type": "Constant", + "value": 20.0 + }, + "cost": { + "type": "Constant", + "value": -50 + }, + "soft_min": { + "cost": { + "type": "Constant", + "value": 50 + }, + "flow": { + "type": "Constant", + "value": 5 + } + } + }, + { + "name": "demand", + "type": "Output", + "max_flow": { + "type": "Constant", + "value": 10.0 + }, + "cost": { + "type": "Constant", + "value": -10 + } + } + ], + "edges": [ + { + "from_node": "input", + "to_node": "link" + }, + { + "from_node": "link", + "to_node": "demand" + } + ] + }, + "metric_sets": [ + { + "name": "nodes", + "metrics": [ + { + "type": "Node", + "name": "link", + "attribute": "Inflow" + } + ] + } + ] +} From 2e3876975a0192355e60467bd202cf60f9bd29db Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:51:32 +0100 Subject: [PATCH 5/7] Fixed add_aggregated_node call in LinkNode due to function signature change --- pywr-schema/src/nodes/core.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 02146ed3..91259710 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -342,7 +342,7 @@ impl LinkNode { network.add_aggregated_node( node_name, Self::aggregated_node_sub_name(), - &[link, soft_min_node], + &[vec![link], vec![soft_min_node]], None, )?; } @@ -352,7 +352,7 @@ impl LinkNode { network.add_aggregated_node( node_name, Self::aggregated_node_sub_name(), - &[link, soft_max_node], + &[vec![link], vec![soft_max_node]], None, )?; } @@ -363,13 +363,13 @@ impl LinkNode { network.add_aggregated_node( node_name, Self::aggregated_node_sub_name(), - &[link, soft_min_node, soft_max_node], + &[vec![link], vec![soft_min_node], vec![soft_max_node]], None, )?; network.add_aggregated_node( node_name, Self::aggregated_node_l_l_min_sub_name(), - &[link, soft_min_node], + &[vec![link], vec![soft_min_node]], None, )?; } From 03565dfccfd794dcad438ca21d410b12cbe950bc Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:54:35 +0100 Subject: [PATCH 6/7] Fixed Clippy warnings --- pywr-schema/src/nodes/core.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 91259710..7311502d 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -274,15 +274,6 @@ impl LinkNode { Some("soft_max_node") } - fn aggregated_node_sub_name() -> Option<&'static str> { - Some("aggregate_node") - } - - /// The aggregated node name of `L` and `L_min` when both soft constraints are provided. - fn aggregated_node_l_l_min_sub_name() -> Option<&'static str> { - Some("aggregate_node_l_l_min") - } - pub fn input_connectors(&self) -> Vec<(&str, Option)> { let mut connectors = vec![(self.meta.name.as_str(), None)]; if self.soft_min.is_some() { @@ -324,6 +315,15 @@ impl LinkNode { #[cfg(feature = "core")] impl LinkNode { + fn aggregated_node_sub_name() -> Option<&'static str> { + Some("aggregate_node") + } + + /// The aggregated node name of `L` and `L_min` when both soft constraints are provided. + fn aggregated_node_l_l_min_sub_name() -> Option<&'static str> { + Some("aggregate_node_l_l_min") + } + pub fn node_indices_for_constraints( &self, network: &pywr_core::network::Network, From 9fb98cffc695d6b7d55ce2c39476cb954e0980c9 Mon Sep 17 00:00:00 2001 From: Stefano Simonelli <16114781+s-simoncelli@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:57:32 +0100 Subject: [PATCH 7/7] Fixed description of link node with soft constraints --- pywr-schema/src/nodes/core.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 7311502d..c6e341a2 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -154,10 +154,11 @@ pub struct SoftConstraint { /// ``` /// /// # Soft constraints -/// This node allows setting optional maximum and minimum soft constraints which means the node's `min_flow` -/// and `max_flow` properties may be breached, as specified by the user in the `soft_max` and `soft_min` -/// properties. When the two attributes are provided, the internal representation of the link will -/// look like this: +/// This node allows setting optional maximum and minimum soft constraints via the `soft_min.flow` +/// and `soft_max.flow` properties. These may be breached depending on the costs set on the +/// optional nodes. However, the combined flow through the internal nodes will always be bound +/// between the `min_flow` and `max_flow` attributes. When the two attributes are provided, the +/// internal representation of the link will look like this: /// /// ```svgbob /// .soft_max @@ -373,7 +374,7 @@ impl LinkNode { None, )?; } - _ => {} + (None, None) => {} }; Ok(()) }