From 9f95f817ce50e95b2cdb2770384afe52ae5e4038 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 22:26:17 +0200 Subject: [PATCH 01/19] Rename `graph::test_types` to `test_utils` Also move the code for the module from `graph.rs` to `graph/test_utils`. This is to make room for a component graph builder for tests, that will be added in the next commit. Signed-off-by: Sahas Subramanian --- src/graph.rs | 51 +--------------------- src/graph/creation.rs | 2 +- src/graph/meter_roles.rs | 2 +- src/graph/retrieval.rs | 2 +- src/graph/test_utils.rs | 51 ++++++++++++++++++++++ src/graph/validation/validate_graph.rs | 2 +- src/graph/validation/validate_neighbors.rs | 2 +- 7 files changed, 57 insertions(+), 55 deletions(-) create mode 100644 src/graph/test_utils.rs diff --git a/src/graph.rs b/src/graph.rs index f7f3d74..28b9c0a 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -42,53 +42,4 @@ where } #[cfg(test)] -mod test_types { - //! This module contains the `TestComponent` and `TestConnection` types, - //! which implement the `Node` and `Edge` traits respectively. - //! - //! They are shared by all the test modules in the `graph` module. - - use crate::{ComponentCategory, Edge, Node}; - - #[derive(Clone, Debug, PartialEq)] - pub(super) struct TestComponent(u64, ComponentCategory); - - impl TestComponent { - pub(super) fn new(id: u64, category: ComponentCategory) -> Self { - TestComponent(id, category) - } - } - - impl Node for TestComponent { - fn component_id(&self) -> u64 { - self.0 - } - - fn category(&self) -> ComponentCategory { - self.1.clone() - } - - fn is_supported(&self) -> bool { - true - } - } - - #[derive(Clone, Debug, PartialEq)] - pub(super) struct TestConnection(u64, u64); - - impl TestConnection { - pub(super) fn new(source: u64, destination: u64) -> Self { - TestConnection(source, destination) - } - } - - impl Edge for TestConnection { - fn source(&self) -> u64 { - self.0 - } - - fn destination(&self) -> u64 { - self.1 - } - } -} +mod test_utils; diff --git a/src/graph/creation.rs b/src/graph/creation.rs index 9cbbf6b..ab79232 100644 --- a/src/graph/creation.rs +++ b/src/graph/creation.rs @@ -118,7 +118,7 @@ where mod tests { use super::*; use crate::component_category::BatteryType; - use crate::graph::test_types::{TestComponent, TestConnection}; + use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::InverterType; diff --git a/src/graph/meter_roles.rs b/src/graph/meter_roles.rs index e541a8c..1eaaa88 100644 --- a/src/graph/meter_roles.rs +++ b/src/graph/meter_roles.rs @@ -134,7 +134,7 @@ mod tests { use crate::component_category::BatteryType; use crate::component_category::EvChargerType; use crate::error::Error; - use crate::graph::test_types::{TestComponent, TestConnection}; + use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::InverterType; diff --git a/src/graph/retrieval.rs b/src/graph/retrieval.rs index 11d8a5d..eca50d0 100644 --- a/src/graph/retrieval.rs +++ b/src/graph/retrieval.rs @@ -80,7 +80,7 @@ mod tests { use crate::component_category::BatteryType; use crate::component_category::CategoryPredicates; use crate::error::Error; - use crate::graph::test_types::{TestComponent, TestConnection}; + use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::InverterType; diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs new file mode 100644 index 0000000..9ae7a53 --- /dev/null +++ b/src/graph/test_utils.rs @@ -0,0 +1,51 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the `TestComponent` and `TestConnection` types, +//! which implement the `Node` and `Edge` traits respectively. +//! +//! They are shared by all the test modules in the `graph` module. + +use crate::{ComponentCategory, Edge, Node}; + +#[derive(Clone, Debug, PartialEq)] +pub(super) struct TestComponent(u64, ComponentCategory); + +impl TestComponent { + pub(super) fn new(id: u64, category: ComponentCategory) -> Self { + TestComponent(id, category) + } +} + +impl Node for TestComponent { + fn component_id(&self) -> u64 { + self.0 + } + + fn category(&self) -> ComponentCategory { + self.1.clone() + } + + fn is_supported(&self) -> bool { + true + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(super) struct TestConnection(u64, u64); + +impl TestConnection { + pub(super) fn new(source: u64, destination: u64) -> Self { + TestConnection(source, destination) + } +} + +impl Edge for TestConnection { + fn source(&self) -> u64 { + self.0 + } + + fn destination(&self) -> u64 { + self.1 + } +} diff --git a/src/graph/validation/validate_graph.rs b/src/graph/validation/validate_graph.rs index 3fd84ee..eeba470 100644 --- a/src/graph/validation/validate_graph.rs +++ b/src/graph/validation/validate_graph.rs @@ -83,7 +83,7 @@ where mod tests { use super::*; use crate::component_category::BatteryType; - use crate::graph::test_types::{TestComponent, TestConnection}; + use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::ComponentGraph; use crate::InverterType; diff --git a/src/graph/validation/validate_neighbors.rs b/src/graph/validation/validate_neighbors.rs index 2710512..7d5af90 100644 --- a/src/graph/validation/validate_neighbors.rs +++ b/src/graph/validation/validate_neighbors.rs @@ -141,7 +141,7 @@ mod tests { use super::*; use crate::component_category::BatteryType; use crate::component_category::EvChargerType; - use crate::graph::test_types::{TestComponent, TestConnection}; + use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::ComponentGraph; use crate::InverterType; From 5d7599784c1bb09e4da8f15e1d132a4d77330442 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 22:36:10 +0200 Subject: [PATCH 02/19] Add the `ComponentGraphBuilder` type in the `test_utils` module Signed-off-by: Sahas Subramanian --- src/graph/test_utils.rs | 171 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 4 deletions(-) diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs index 9ae7a53..c5cacb0 100644 --- a/src/graph/test_utils.rs +++ b/src/graph/test_utils.rs @@ -1,12 +1,17 @@ // License: MIT // Copyright © 2024 Frequenz Energy-as-a-Service GmbH -//! This module contains the `TestComponent` and `TestConnection` types, -//! which implement the `Node` and `Edge` traits respectively. +//! This module is only compiled when running unit tests and contains features +//! that are shared by all tests of the `graph` modue. //! -//! They are shared by all the test modules in the `graph` module. +//! - the `TestComponent` and `TestConnection` types, which implement the `Node` +//! and `Edge` traits respectively. +//! - the `TestGraphBuilder`, which can declaratively build complex component +//! graph configurations for use in tests. -use crate::{ComponentCategory, Edge, Node}; +use crate::{ + BatteryType, ComponentCategory, ComponentGraph, Edge, Error, EvChargerType, InverterType, Node, +}; #[derive(Clone, Debug, PartialEq)] pub(super) struct TestComponent(u64, ComponentCategory); @@ -49,3 +54,161 @@ impl Edge for TestConnection { self.1 } } + +/// Represents a component added to the `ComponentGraphBuilder`. +#[derive(Eq, Hash, PartialEq, Copy, Clone)] +pub(super) struct ComponentHandle(u64); + +impl ComponentHandle { + /// Returns the component ID of the component. + pub(super) fn component_id(&self) -> u64 { + self.0 + } +} + +/// A builder for creating complex component graph configurations easily, for +/// use in tests. +pub(super) struct ComponentGraphBuilder { + components: Vec, + connections: Vec, + next_id: u64, +} + +impl ComponentGraphBuilder { + /// Creates a new `ComponentGraphBuilder`. + pub(super) fn new() -> Self { + let builder = ComponentGraphBuilder { + components: Vec::new(), + connections: Vec::new(), + next_id: 0, + }; + builder + } + + /// Adds a component to the graph and returns its handle. + pub(super) fn add_component(&mut self, category: ComponentCategory) -> ComponentHandle { + let id = self.next_id; + self.next_id += 1; + self.components + .push(TestComponent::new(id, category.clone())); + let handle = ComponentHandle(id); + handle + } + + /// Adds a grid component to the graph and returns its handle. + pub(super) fn grid(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::Grid) + } + + /// Adds a meter to the graph and returns its handle. + pub(super) fn meter(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::Meter) + } + + /// Adds a battery to the graph and returns its handle. + pub(super) fn battery(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::Battery(BatteryType::LiIon)) + } + + /// Adds a battery inverter to the graph and returns its handle. + pub(super) fn battery_inverter(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::Inverter(InverterType::Battery)) + } + + /// Adds a solar inverter to the graph and returns its handle. + pub(super) fn solar_inverter(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::Inverter(InverterType::Solar)) + } + + /// Adds an EV charger to the graph and returns its handle. + pub(super) fn ev_charger(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::EvCharger(EvChargerType::Ac)) + } + + /// Adds a CHP to the graph and returns its handle. + pub(super) fn chp(&mut self) -> ComponentHandle { + self.add_component(ComponentCategory::Chp) + } + + /// Connects two components in the graph. + pub(super) fn connect(&mut self, from: ComponentHandle, to: ComponentHandle) -> &mut Self { + self.connections + .push(TestConnection::new(from.component_id(), to.component_id())); + self + } + + /// Adds a meter, followed by the given number of inverters and batteries, + /// and returns a handle to the meter. + pub(super) fn meter_bat_chain( + &mut self, + num_inverters: usize, + num_batteries: usize, + ) -> ComponentHandle { + let meter = self.meter(); + let mut inverters = vec![]; + for _ in 0..num_inverters { + let inverter = self.battery_inverter(); + self.connect(meter, inverter); + inverters.push(inverter); + } + for _ in 0..num_batteries { + let battery = self.battery(); + for inverter in &inverters { + self.connect(*inverter, battery); + } + } + meter + } + + /// Adds a battery inverter, followed by the given number of batteries, + /// and returns a handle to the battery inverter. + pub(super) fn inv_bat_chain(&mut self, num_batteries: usize) -> ComponentHandle { + let inverter = self.battery_inverter(); + let mut batteries = vec![]; + for _ in 0..num_batteries { + let battery = self.battery(); + self.connect(inverter, battery); + batteries.push(battery); + } + inverter + } + + /// Adds a meter, followed by the given number of PV inverters, and returns a + /// handle to the meter. + pub(super) fn meter_pv_chain(&mut self, num_inverters: usize) -> ComponentHandle { + let meter = self.meter(); + for _ in 0..num_inverters { + let inverter = self.solar_inverter(); + self.connect(meter, inverter); + } + meter + } + + /// Adds a meter, followed by the given number of CHPs, and returns a + /// handle to the meter. + pub(super) fn meter_chp_chain(&mut self, num_chp: usize) -> ComponentHandle { + let meter = self.meter(); + for _ in 0..num_chp { + let chp = self.chp(); + self.connect(meter, chp); + } + meter + } + + /// Adds a meter, followed by the given number of EV chargers, and returns a + /// handle to the meter. + pub(super) fn meter_ev_charger_chain(&mut self, num_ev_chargers: usize) -> ComponentHandle { + let meter = self.meter(); + for _ in 0..num_ev_chargers { + let ev_charger = self.ev_charger(); + self.connect(meter, ev_charger); + } + meter + } + + /// Builds and returns the component graph from the components and + /// connections added to the builder. + pub(super) fn build(&self) -> Result, Error> { + ComponentGraph::try_new(self.components.clone(), self.connections.clone()) + } +} From 29a50c48c4acef68ef00b00b8cc1c0ad5e39b97f Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 22:41:56 +0200 Subject: [PATCH 03/19] Add the `Expr` type for representing formula expressions Signed-off-by: Sahas Subramanian --- src/graph.rs | 1 + src/graph/formulas.rs | 6 + src/graph/formulas/expr.rs | 390 +++++++++++++++++++++++++++++++++++++ 3 files changed, 397 insertions(+) create mode 100644 src/graph/formulas.rs create mode 100644 src/graph/formulas/expr.rs diff --git a/src/graph.rs b/src/graph.rs index 28b9c0a..d77b590 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -9,6 +9,7 @@ mod meter_roles; mod retrieval; mod validation; +mod formulas; pub mod iterators; use crate::{Edge, Node}; diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs new file mode 100644 index 0000000..eb42b0b --- /dev/null +++ b/src/graph/formulas.rs @@ -0,0 +1,6 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! Methods for building formulas for various microgrid metrics. + +mod expr; diff --git a/src/graph/formulas/expr.rs b/src/graph/formulas/expr.rs new file mode 100644 index 0000000..112a6e0 --- /dev/null +++ b/src/graph/formulas/expr.rs @@ -0,0 +1,390 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +use crate::Node; + +#[derive(Debug, Clone)] +pub(crate) enum Expr { + /// A negation of an expression. + Neg { param: Box }, + + /// A numeric constant. + Number { value: f64 }, + + /// A reference to a component. + Component { component_id: u64 }, + + /// An addition of multiple expressions. + Add { params: Vec }, + + /// A subtraction of multiple expressions. + Sub { params: Vec }, + + /// A coalesce function. + Coalesce { params: Vec }, + + /// A min function. + Min { params: Vec }, + + /// A max function. + Max { params: Vec }, +} + +impl std::ops::Add for Expr { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + match (self, rhs) { + // -a + -b = -(a + b) + (Self::Neg { param: lhs }, Self::Neg { param: rhs }) => -(*lhs + *rhs), + // -a + b = b - a + // a + -b = a - b + (other, Self::Neg { param }) | (Self::Neg { param }, other) => other - *param, + // (a + b) + (c + d) = a + b + c + d + (Self::Add { params: mut lhs }, Self::Add { params: mut rhs }) => { + lhs.append(&mut rhs); + Self::Add { params: lhs } + } + // (a + b) + c = a + b + c + (Self::Add { mut params }, rhs) => { + params.push(rhs); + Self::Add { params } + } + // a + (b + c) = a + b + c + (lhs, Self::Add { mut params }) => { + params.insert(0, lhs); + Self::Add { params } + } + // Catch all other cases + (lhs, rhs) => Self::Add { + params: vec![lhs, rhs], + }, + } + } +} + +impl std::ops::Sub for Expr { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + match (self, rhs) { + // (a - b) - -c = a - b + c + (sub @ Self::Sub { .. }, Self::Neg { param }) => sub + *param, + // -a - (b - c) = c - b - a + (Self::Neg { param }, sub @ Self::Sub { .. }) => -sub - *param, + // (a - b) - c = a - b - c + (Self::Sub { mut params }, rhs) => { + params.push(rhs); + Self::Sub { params } + } + // -a - -b = b - a + (Self::Neg { param: lhs }, Self::Neg { param: rhs }) => Self::Sub { + params: vec![*rhs, *lhs], + }, + // -a - b = -(a + b) + (Self::Neg { param }, value) => -(*param + value), + // a - -b = a + b + (lhs, Self::Neg { param }) => lhs + *param, + // Catch all other cases + (lhs, rhs) => Self::Sub { + params: vec![lhs, rhs], + }, + } + } +} + +impl std::ops::Neg for Expr { + type Output = Self; + + fn neg(self) -> Self { + match self { + // -(-a) = a + Expr::Neg { param: inner } => *inner, + // -(a - b) = b - a + // -(a - b - c) = b + c - a + Expr::Sub { mut params } => { + let first = params.remove(0); + Expr::Add { params } - first + } + // Catch all other cases + _ => Expr::Neg { + param: Box::new(self), + }, + } + } +} + +impl From<&N> for Expr { + fn from(node: &N) -> Self { + Self::Component { + component_id: node.component_id(), + } + } +} + +/// Constructors for `FormulaExpression`. +impl Expr { + pub(crate) fn number(value: f64) -> Self { + Self::Number { value } + } + + pub(crate) fn component(component_id: u64) -> Self { + Self::Component { component_id } + } + + pub(crate) fn coalesce(params: Vec) -> Self { + Self::Coalesce { params } + } + + pub(crate) fn min(params: Vec) -> Self { + Self::Min { params } + } + + pub(crate) fn max(params: Vec) -> Self { + Self::Max { params } + } +} + +impl std::fmt::Display for Expr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.generate_string(false)) + } +} + +/// Specifies how to add brackets when generating a string representation of an +/// expression. +/// +/// - `First`: Add brackets around the first component. +/// - `Rest`: Add brackets around all components except the first. +/// - `All`: Add brackets around all components. +/// - `None`: Do not add brackets. +#[derive(PartialEq)] +enum BracketComponents { + First, + Rest, + All, + None, +} + +/// Display helpers for `FormulaExpression`. +impl Expr { + /// Generates a string representation of the expression. + /// + /// If `bracket_whole` is `true` and the expression has more than one + /// component, the whole expression is enclosed in brackets. + fn generate_string(&self, bracket_whole: bool) -> String { + match self { + Self::Neg { param } => format!("-{}", param.generate_string(true)), + Self::Number { value } => { + if value.fract() == 0.0 { + // For whole numbers, format with one decimal place. + format!("{:.1}", value) + } else { + // else format normally. + format!("{}", value) + } + } + Self::Component { component_id } => format!("#{}", component_id), + Self::Add { params } => { + Self::join_params(params, " + ", None, BracketComponents::None, bracket_whole) + } + Self::Sub { params } => { + Self::join_params(params, " - ", None, BracketComponents::Rest, bracket_whole) + } + Self::Coalesce { params } => Self::join_params( + params, + ", ", + Some("COALESCE"), + BracketComponents::None, + false, + ), + Self::Min { params } => { + Self::join_params(params, ", ", Some("MIN"), BracketComponents::None, false) + } + Self::Max { params } => { + Self::join_params(params, ", ", Some("MAX"), BracketComponents::None, false) + } + } + } + + /// Joins a list of expressions into a string, with the specified separator. + /// + /// It also takes an optional prefix, and specifics on how to bracket the + /// components and the whole expression. + fn join_params( + params: &[Expr], + separator: &str, + prefix: Option<&str>, + bracket_components: BracketComponents, + bracket_whole: bool, + ) -> String { + let (mut result, suffix) = match prefix { + Some(prefix) => (format!("{}(", prefix), String::from(")")), + None => (String::new(), String::new()), + }; + let mut num_components = 0; + for expression in params.iter() { + if num_components > 0 { + result.push_str(separator); + } + if (bracket_components == BracketComponents::First && num_components == 0) + || (bracket_components == BracketComponents::Rest && num_components > 0) + || (bracket_components == BracketComponents::All) + { + result.push_str(&expression.generate_string(true)); + } else { + result.push_str(&expression.generate_string(false)); + } + num_components += 1; + } + if bracket_whole && num_components > 1 { + String::from("(") + &result + &suffix + ")" + } else { + result + &suffix + } + } +} + +#[cfg(test)] +mod tests { + use super::Expr; + + #[track_caller] + fn assert_expr(exprs: &[Expr], expected: &str) { + for expr in exprs { + assert_eq!(expr.to_string(), expected); + } + } + + #[test] + fn test_arithmatic() { + let comp = Expr::component; + + assert_expr( + &[ + comp(10) + comp(11) + comp(12) + comp(13), + comp(10) - -comp(11) + (comp(12) + comp(13)), + (comp(10) + comp(11)) - -(comp(12) - -comp(13)), + ], + "#10 + #11 + #12 + #13", + ); + + assert_expr( + &[ + -(comp(10) + comp(11) + comp(12)), + -comp(10) - comp(11) - comp(12), + -comp(10) - (comp(11) + comp(12)), + -(comp(10) + comp(11)) - comp(12), + ], + "-(#10 + #11 + #12)", + ); + + assert_expr( + &[ + comp(11) - comp(10), + comp(11) + -comp(10), + -comp(10) + comp(11), + -comp(10) - -comp(11), + ], + "#11 - #10", + ); + + assert_expr( + &[ + (comp(11) + comp(12)) - comp(10), + (comp(11) + comp(12)) + -comp(10), + -comp(10) + (comp(11) + comp(12)), + -comp(10) - -(comp(11) + comp(12)), + ], + "#11 + #12 - #10", + ); + + assert_expr( + &[ + (comp(11) - comp(12)) - comp(10), + (comp(11) - comp(12)) + -comp(10), + -comp(10) + (comp(11) - comp(12)), + -comp(10) - -(comp(11) - comp(12)), + ], + "#11 - #12 - #10", + ); + + assert_expr( + &[ + comp(11) - comp(12) + comp(10), + (comp(11) - comp(12)) - -comp(10), + (comp(11) - comp(12)) + comp(10), + -(comp(12) - comp(11)) + comp(10), + ], + "#11 - #12 + #10", + ); + + assert_expr( + &[ + (comp(11) + comp(12)) - (comp(10) + comp(13)), + (comp(11) + comp(12)) + -(comp(10) + comp(13)), + -(comp(10) + comp(13)) + (comp(11) + comp(12)), + -(comp(10) + comp(13)) - -(comp(11) + comp(12)), + ], + "#11 + #12 - (#10 + #13)", + ); + + assert_expr( + &[ + (comp(11) - comp(12)) - (comp(10) + comp(13)), + (comp(11) - comp(12)) + -(comp(10) + comp(13)), + -(comp(10) + comp(13)) + (comp(11) - comp(12)), + -(comp(10) + comp(13)) - -(comp(11) - comp(12)), + ], + "#11 - #12 - (#10 + #13)", + ); + + assert_expr( + &[(comp(11) + comp(12)) - (comp(10) - comp(13))], + "#11 + #12 - (#10 - #13)", + ); + assert_expr( + &[(comp(11) + comp(12)) + -(comp(10) - comp(13))], + "#11 + #12 + #13 - #10", + ); + assert_expr( + &[ + -(comp(10) - comp(13)) + (comp(11) + comp(12)), + -(comp(10) - comp(13)) - -(comp(11) + comp(12)), + ], + "#13 - #10 + #11 + #12", + ); + } + + #[test] + fn test_functions() { + let comp = Expr::component; + let coalesce = Expr::coalesce; + let number = Expr::number; + let min = Expr::min; + let max = Expr::max; + + assert_expr( + &[comp(1) + - (coalesce(vec![comp(5), comp(7) + comp(6)]) + coalesce(vec![comp(2), comp(3)])) + + coalesce(vec![ + max(vec![number(0.0), comp(5)]), + max(vec![number(0.0), comp(7)]) + max(vec![number(0.0), comp(6)]), + ])], + concat!( + "#1 - (COALESCE(#5, #7 + #6) + COALESCE(#2, #3)) + ", + "COALESCE(MAX(0.0, #5), MAX(0.0, #7) + MAX(0.0, #6))" + ), + ); + + assert_expr( + &[min(vec![number(0.0), comp(5), comp(7) + comp(6)]) + - max(vec![ + coalesce(vec![comp(5), comp(7) + comp(6)]), + comp(7), + number(22.44), + ])], + "MIN(0.0, #5, #7 + #6) - MAX(COALESCE(#5, #7 + #6), #7, 22.44)", + ) + } +} From cce38d5891ce2b656f5419466eda010cb83fc06b Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:18:15 +0200 Subject: [PATCH 04/19] Add methods for iterating over the siblings of components Signed-off-by: Sahas Subramanian --- src/graph/iterators.rs | 42 +++++++++++++++ src/graph/retrieval.rs | 119 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/src/graph/iterators.rs b/src/graph/iterators.rs index 0d2fe8d..6fecb3c 100644 --- a/src/graph/iterators.rs +++ b/src/graph/iterators.rs @@ -3,6 +3,8 @@ //! Iterators over components and connections in a `ComponentGraph`. +use std::{collections::HashSet, iter::Flatten, vec::IntoIter}; + use petgraph::graph::DiGraph; use crate::{ComponentGraph, Edge, Node}; @@ -69,3 +71,43 @@ where self.iter.next().map(|i| &self.graph[i]) } } + +/// An iterator over the siblings of a component in a `ComponentGraph`. +pub struct Siblings<'a, N> +where + N: Node, +{ + pub(crate) component_id: u64, + pub(crate) iter: Flatten>>, + visited: HashSet, +} + +impl<'a, N> Siblings<'a, N> +where + N: Node, +{ + pub(crate) fn new(component_id: u64, iter: Flatten>>) -> Self { + Siblings { + component_id, + iter, + visited: HashSet::new(), + } + } +} + +impl<'a, N> Iterator for Siblings<'a, N> +where + N: Node, +{ + type Item = &'a N; + + fn next(&mut self) -> Option { + for i in self.iter.by_ref() { + if i.component_id() == self.component_id || !self.visited.insert(i.component_id()) { + continue; + } + return Some(i); + } + None + } +} diff --git a/src/graph/retrieval.rs b/src/graph/retrieval.rs index eca50d0..07eb77f 100644 --- a/src/graph/retrieval.rs +++ b/src/graph/retrieval.rs @@ -3,7 +3,7 @@ //! Methods for retrieving components and connections from a [`ComponentGraph`]. -use crate::iterators::{Components, Connections, Neighbors}; +use crate::iterators::{Components, Connections, Neighbors, Siblings}; use crate::{ComponentGraph, Edge, Error, Node}; /// `Component` and `Connection` retrieval. @@ -72,6 +72,39 @@ where Error::component_not_found(format!("Component with id {} not found.", component_id)) }) } + + /// Returns an iterator over the *siblings* of the component with the + /// given `component_id`, that have shared predecessors. + /// + /// Returns an error if the given `component_id` does not exist. + pub(crate) fn siblings_from_predecessors( + &self, + component_id: u64, + ) -> Result, Error> { + Ok(Siblings::new( + component_id, + self.predecessors(component_id)? + .map(|x| self.successors(x.component_id())) + .collect::, _>>()? + .into_iter() + .flatten(), + )) + } + + /// Returns an iterator over the *siblings* of the component with the + /// given `component_id`, that have shared successors. + /// + /// Returns an error if the given `component_id` does not exist. + pub(crate) fn siblings_from_successors(&self, component_id: u64) -> Result, Error> { + Ok(Siblings::new( + component_id, + self.successors(component_id)? + .map(|x| self.predecessors(x.component_id())) + .collect::, _>>()? + .into_iter() + .flatten(), + )) + } } #[cfg(test)] @@ -80,6 +113,7 @@ mod tests { use crate::component_category::BatteryType; use crate::component_category::CategoryPredicates; use crate::error::Error; + use crate::graph::test_utils::ComponentGraphBuilder; use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::InverterType; @@ -194,4 +228,87 @@ mod tests { Ok(()) } + + #[test] + fn test_siblings() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a grid meter to the grid, with no successors. + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + assert_eq!(grid_meter.component_id(), 1); + + // Add a battery chain with three inverters and two battery. + let meter_bat_chain = builder.meter_bat_chain(3, 2); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 2); + + let graph = builder.build()?; + assert_eq!( + graph + .siblings_from_predecessors(3) + .unwrap() + .collect::>(), + [ + &TestComponent::new(5, ComponentCategory::Inverter(InverterType::Battery)), + &TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery)) + ] + ); + + assert_eq!( + graph + .siblings_from_successors(3) + .unwrap() + .collect::>(), + [ + &TestComponent::new(5, ComponentCategory::Inverter(InverterType::Battery)), + &TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery)) + ] + ); + + assert_eq!( + graph + .siblings_from_successors(6) + .unwrap() + .collect::>(), + Vec::<&TestComponent>::new() + ); + + assert_eq!( + graph + .siblings_from_predecessors(6) + .unwrap() + .collect::>(), + [&TestComponent::new( + 7, + ComponentCategory::Battery(BatteryType::LiIon) + )] + ); + + // Add two dangling meter to the grid meter + let dangling_meter = builder.meter(); + builder.connect(grid_meter, dangling_meter); + assert_eq!(dangling_meter.component_id(), 8); + + let dangling_meter = builder.meter(); + builder.connect(grid_meter, dangling_meter); + assert_eq!(dangling_meter.component_id(), 9); + + let graph = builder.build()?; + assert_eq!( + graph + .siblings_from_predecessors(8) + .unwrap() + .collect::>(), + [ + &TestComponent::new(9, ComponentCategory::Meter), + &TestComponent::new(2, ComponentCategory::Meter), + ] + ); + + Ok(()) + } } From 4554c5095c8652062182e65d06203b59ee064244 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 22:50:36 +0200 Subject: [PATCH 05/19] Support generating formulas with fallback expressions Fallback expressions may be used when the primary expressions are missing values. Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 2 + src/graph/formulas/fallback.rs | 232 ++++++++++++++++++++++++++++++++ src/graph/formulas/traversal.rs | 24 ++++ src/graph/meter_roles.rs | 14 ++ 4 files changed, 272 insertions(+) create mode 100644 src/graph/formulas/fallback.rs create mode 100644 src/graph/formulas/traversal.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index eb42b0b..67f0718 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -4,3 +4,5 @@ //! Methods for building formulas for various microgrid metrics. mod expr; +mod fallback; +mod traversal; diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs new file mode 100644 index 0000000..9527b4e --- /dev/null +++ b/src/graph/formulas/fallback.rs @@ -0,0 +1,232 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! Fallback expression generator for components and meters. + +use crate::component_category::CategoryPredicates; +use crate::{ComponentGraph, Edge, Error, Node}; +use std::collections::BTreeSet; + +use super::expr::Expr; + +impl ComponentGraph +where + N: Node, + E: Edge, +{ + /// Returns a formula expression with fallbacks where possible for the `sum` + /// of the given component ids. + pub(super) fn fallback_expr( + &self, + component_ids: impl IntoIterator, + prefer_meters: bool, + ) -> Result { + FallbackExpr { + prefer_meters, + graph: self, + } + .generate(BTreeSet::from_iter(component_ids)) + } +} + +struct FallbackExpr<'a, N, E> +where + N: Node, + E: Edge, +{ + pub(crate) prefer_meters: bool, + pub(crate) graph: &'a ComponentGraph, +} + +impl<'a, N, E> FallbackExpr<'a, N, E> +where + N: Node, + E: Edge, +{ + fn generate(&self, mut component_ids: BTreeSet) -> Result { + let mut formula = None::; + while let Some(component_id) = component_ids.pop_first() { + if let Some(expr) = self.meter_fallback(component_id)? { + formula = Self::add_to_option(formula, expr); + } else if let Some(expr) = self.component_fallback(&mut component_ids, component_id)? { + formula = Self::add_to_option(formula, expr); + } else { + formula = Self::add_to_option(formula, Expr::component(component_id)); + } + } + + formula.ok_or(Error::internal("Search for fallback components failed.")) + } + + /// Returns a fallback expression for a meter component. + fn meter_fallback(&self, component_id: u64) -> Result, Error> { + let component = self.graph.component(component_id)?; + if !component.is_meter() || self.graph.has_meter_successors(component_id)? { + return Ok(None); + } + + if !self.graph.has_successors(component_id)? + || !self + .graph + .successors(component_id)? + .all(|x| x.is_supported()) + { + return Ok(Some(Expr::component(component_id))); + } + + let successor_expr = self + .graph + .successors(component_id)? + .map(Expr::from) + .reduce(|a, b| a + b) + .ok_or(Error::internal( + "Can't find successors of components with successors.", + ))?; + + let exprs = if self.prefer_meters { + vec![Expr::component(component_id), successor_expr] + } else { + vec![successor_expr, Expr::component(component_id)] + }; + + Ok(Some(Expr::coalesce(exprs))) + } + + /// Returns a fallback expression for components with the following categories: + /// + /// - CHP + /// - Battery Inverter + /// - PV Inverter + /// - EV Charger + fn component_fallback( + &self, + component_ids: &mut BTreeSet, + component_id: u64, + ) -> Result, Error> { + let component = self.graph.component(component_id)?; + if !component.is_battery_inverter() + && !component.is_chp() + && !component.is_pv_inverter() + && !component.is_ev_charger() + { + return Ok(None); + } + + // If predecessors have other successors that are not in the list of + // component ids, the predecessors can't be used as fallback. + let siblings = self + .graph + .siblings_from_predecessors(component_id)? + .filter(|sibling| sibling.component_id() != component_id) + .collect::>(); + if !siblings + .iter() + .all(|sibling| component_ids.contains(&sibling.component_id())) + { + return Ok(Some(Expr::component(component_id))); + } + + for sibling in siblings { + component_ids.remove(&sibling.component_id()); + } + + let predecessor_ids: BTreeSet = self + .graph + .predecessors(component_id)? + .map(|x| x.component_id()) + .collect(); + + Ok(Some(self.generate(predecessor_ids)?)) + } + + fn add_to_option(expr: Option, other: Expr) -> Option { + if let Some(expr) = expr { + Some(expr + other) + } else { + Some(other) + } + } +} + +#[cfg(test)] +mod tests { + use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + + #[test] + fn test_meter_fallback() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a grid meter. + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + // Add a battery meter with one inverter and one battery. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(grid_meter.component_id(), 1); + assert_eq!(meter_bat_chain.component_id(), 2); + + let graph = builder.build()?; + let expr = graph.fallback_expr(vec![1, 2], false)?; + assert_eq!(expr.to_string(), "#1 + COALESCE(#3, #2)"); + + let expr = graph.fallback_expr(vec![1, 2], true)?; + assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3)"); + + let expr = graph.fallback_expr(vec![3], true)?; + assert_eq!(expr.to_string(), "COALESCE(#2, #3)"); + + // Add a battery meter with three inverter and three batteries + let meter_bat_chain = builder.meter_bat_chain(3, 3); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 5); + + let graph = builder.build()?; + let expr = graph.fallback_expr(vec![3, 5], false)?; + assert_eq!( + expr.to_string(), + "COALESCE(#3, #2) + COALESCE(#8 + #7 + #6, #5)" + ); + + let expr = graph.fallback_expr(vec![2, 5], true)?; + assert_eq!( + expr.to_string(), + "COALESCE(#2, #3) + COALESCE(#5, #8 + #7 + #6)" + ); + + let expr = graph.fallback_expr(vec![2, 6, 7, 8], true)?; + assert_eq!( + expr.to_string(), + "COALESCE(#2, #3) + COALESCE(#5, #8 + #7 + #6)" + ); + + let expr = graph.fallback_expr(vec![2, 7, 8], true)?; + assert_eq!(expr.to_string(), "COALESCE(#2, #3) + #7 + #8"); + + let meter = builder.meter(); + let chp = builder.chp(); + let pv_inverter = builder.solar_inverter(); + builder.connect(grid_meter, meter); + builder.connect(meter, chp); + builder.connect(meter, pv_inverter); + + assert_eq!(meter.component_id(), 12); + assert_eq!(chp.component_id(), 13); + assert_eq!(pv_inverter.component_id(), 14); + + let graph = builder.build()?; + let expr = graph.fallback_expr(vec![5, 12], true)?; + assert_eq!( + expr.to_string(), + "COALESCE(#5, #8 + #7 + #6) + COALESCE(#12, #14 + #13)" + ); + + let expr = graph.fallback_expr(vec![7, 14], false)?; + assert_eq!(expr.to_string(), "#7 + #14"); + + Ok(()) + } +} diff --git a/src/graph/formulas/traversal.rs b/src/graph/formulas/traversal.rs new file mode 100644 index 0000000..7f04c69 --- /dev/null +++ b/src/graph/formulas/traversal.rs @@ -0,0 +1,24 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +use crate::{component_category::CategoryPredicates, ComponentGraph, Edge, Error, Node}; + +impl ComponentGraph +where + N: Node, + E: Edge, +{ + /// Returns `true` if the given component has any successors. + pub(super) fn has_successors(&self, component_id: u64) -> Result { + Ok(self.successors(component_id)?.next().is_some()) + } + + /// Returns `true` if the given component has any meter successors. + pub(super) fn has_meter_successors(&self, component_id: u64) -> Result { + let mut has_successors = false; + Ok(self.successors(component_id)?.any(|x| { + has_successors = true; + x.is_meter() + }) && has_successors) + } +} diff --git a/src/graph/meter_roles.rs b/src/graph/meter_roles.rs index 1eaaa88..55f2ede 100644 --- a/src/graph/meter_roles.rs +++ b/src/graph/meter_roles.rs @@ -126,6 +126,20 @@ where }) && has_successors) } + + /// Returns true if the node is a component meter. + /// + /// A meter is a component meter if it is one of the following: + /// - a PV meter, + /// - a battery meter, + /// - an EV charger meter, + /// - a CHP meter. + pub fn is_component_meter(&self, component_id: u64) -> Result { + Ok(self.is_pv_meter(component_id)? + || self.is_battery_meter(component_id)? + || self.is_ev_charger_meter(component_id)? + || self.is_chp_meter(component_id)?) + } } #[cfg(test)] From 5baa9ba3b99d0fc8436b41b8a3512469d0cc3b22 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:20:08 +0200 Subject: [PATCH 06/19] Support searching the graph for components that match a predicate Signed-off-by: Sahas Subramanian --- src/graph/retrieval.rs | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/graph/retrieval.rs b/src/graph/retrieval.rs index 07eb77f..3609644 100644 --- a/src/graph/retrieval.rs +++ b/src/graph/retrieval.rs @@ -5,6 +5,7 @@ use crate::iterators::{Components, Connections, Neighbors, Siblings}; use crate::{ComponentGraph, Edge, Error, Node}; +use std::collections::BTreeSet; /// `Component` and `Connection` retrieval. impl ComponentGraph @@ -105,6 +106,41 @@ where .flatten(), )) } + + /// Returns a set of all components that match the given predicate, starting + /// from the component with the given `component_id`. + /// + /// If `follow_after_match` is `true`, the search continues deeper beyond + /// the matching components. + pub(crate) fn find_all( + &self, + from: u64, + mut pred: impl FnMut(&N) -> bool, + follow_after_match: bool, + ) -> Result, Error> { + let index = self.node_indices.get(&from).ok_or_else(|| { + Error::component_not_found(format!("Component with id {} not found.", from)) + })?; + let mut stack = vec![*index]; + let mut found = BTreeSet::new(); + + while let Some(index) = stack.pop() { + let node = &self.graph[index]; + if pred(node) { + found.insert(node.component_id()); + if !follow_after_match { + continue; + } + } + + let neighbors = self + .graph + .neighbors_directed(index, petgraph::Direction::Outgoing); + stack.extend(neighbors); + } + + Ok(found) + } } #[cfg(test)] @@ -311,4 +347,45 @@ mod tests { Ok(()) } + + #[test] + fn test_find_all() -> Result<(), Error> { + let (components, connections) = nodes_and_edges(); + let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + + let found = graph.find_all(graph.root_id, |x| x.is_meter(), false)?; + assert_eq!(found, [2].iter().cloned().collect()); + + let found = graph.find_all(graph.root_id, |x| x.is_meter(), true)?; + assert_eq!(found, [2, 3, 6].iter().cloned().collect()); + + let found = graph.find_all( + graph.root_id, + |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false), + true, + )?; + assert_eq!(found, [2, 4, 5, 7, 8].iter().cloned().collect()); + + let found = graph.find_all( + 6, + |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false), + true, + )?; + assert_eq!(found, [7, 8].iter().cloned().collect()); + + let found = graph.find_all( + graph.root_id, + |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false), + false, + )?; + assert_eq!(found, [2].iter().cloned().collect()); + + let found = graph.find_all(graph.root_id, |_| true, false)?; + assert_eq!(found, [1].iter().cloned().collect()); + + let found = graph.find_all(3, |_| true, true)?; + assert_eq!(found, [3, 4, 5].iter().cloned().collect()); + + Ok(()) + } } From b9be4c21b2bae3025e8e8d0b78dcd1e3c9b7b83a Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:30:17 +0200 Subject: [PATCH 07/19] Add the `consumer` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 17 + src/graph/formulas/generators.rs | 6 + src/graph/formulas/generators/consumer.rs | 435 ++++++++++++++++++++++ 3 files changed, 458 insertions(+) create mode 100644 src/graph/formulas/generators.rs create mode 100644 src/graph/formulas/generators/consumer.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 67f0718..561a3a4 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -3,6 +3,23 @@ //! Methods for building formulas for various microgrid metrics. +use crate::ComponentGraph; +use crate::Edge; +use crate::Error; +use crate::Node; + mod expr; mod fallback; +mod generators; mod traversal; + +impl ComponentGraph +where + N: Node, + E: Edge, +{ + /// Returns a string representing the consumer formula for the graph. + pub fn consumer_formula(&self) -> Result { + generators::consumer::ConsumerFormulaBuilder::try_new(self)?.build() + } +} diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs new file mode 100644 index 0000000..e360fda --- /dev/null +++ b/src/graph/formulas/generators.rs @@ -0,0 +1,6 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! Formula generators for standard metrics. + +pub(super) mod consumer; diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs new file mode 100644 index 0000000..46a5dfa --- /dev/null +++ b/src/graph/formulas/generators/consumer.rs @@ -0,0 +1,435 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating consumer formulas. + +use std::collections::{BTreeMap, BTreeSet}; + +use super::super::expr::Expr; +use crate::{component_category::CategoryPredicates, ComponentGraph, Edge, Error, Node}; + +pub(crate) struct ConsumerFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + unvisited_meters: BTreeSet, + graph: &'a ComponentGraph, +} + +impl<'a, N, E> ConsumerFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new(graph: &'a ComponentGraph) -> Result { + Ok(Self { + unvisited_meters: graph.find_all(graph.root_id, |node| node.is_meter(), true)?, + graph, + }) + } + + /// Generates the consumer formula for the given node. + pub fn build(mut self) -> Result { + let mut all_meters = None; + while let Some(meter_id) = self.unvisited_meters.pop_first() { + let consumption = self.component_consumption(meter_id)?; + if let Some(expr) = all_meters { + all_meters = Some(expr + consumption); + } else { + all_meters = Some(consumption); + } + } + + let other_grid_successors = self + .graph + .successors(self.graph.root_id)? + .filter(|s| !s.is_meter() && !s.is_battery_inverter()) + .map(|s| self.component_consumption(s.component_id())) + .reduce(|a, b| Ok(a? + b?)); + + let other_grid_successors = match other_grid_successors { + Some(Ok(expr)) => Some(expr), + Some(Err(err)) => return Err(err), + None => None, + }; + + match (all_meters, other_grid_successors) { + (Some(lhs), Some(rhs)) => Ok((lhs + rhs).to_string()), + (None, Some(expr)) | (Some(expr), None) => Ok(expr.to_string()), + (None, None) => Ok("0.0".to_string()), + } + } + + /// Returns a formula expression for just the consumption part of the given + /// component as a formula expression. + /// + /// This is done by clamping the expression to a maximum of 0.0. + fn max_zero(expr: Expr) -> Expr { + Expr::max(vec![Expr::number(0.0), expr]) + } + + fn component_consumption(&mut self, component_id: u64) -> Result { + let component = self.graph.component(component_id)?; + if component.is_meter() { + self.unvisited_meters.remove(&component_id); + // Create a formula expression from the component. + let mut expr = Expr::from(component); + + // If there are siblings with the same successors as the component, + // then it is a diamond configuration, so we add those siblings to + // the expression. + let mut successors = BTreeMap::from_iter( + self.graph + .successors(component_id)? + .map(|s| (s.component_id(), s)), + ); + for sibling in self.graph.siblings_from_successors(component_id)? { + expr = expr + sibling.into(); + self.unvisited_meters.remove(&sibling.component_id()); + for successor in self.graph.successors(sibling.component_id())? { + successors.insert(successor.component_id(), successor); + } + } + + // Subtract each successor from the expression. + for successor in successors { + let successor_expr = if successor.1.is_meter() { + self.graph.fallback_expr([successor.0], true)? + } else { + Expr::from(successor.1) + }; + expr = expr - successor_expr; + } + + expr = Self::max_zero(expr); + + // If the meter doesn't have any meter successors, its consumption + // can be 0 when it can't be calculated. + if self.graph.has_successors(component_id)? + && !self.graph.has_meter_successors(component_id)? + { + expr = Expr::coalesce(vec![expr, Expr::number(0.0)]); + } + Ok(expr) + } else { + Ok(Self::max_zero(component.into())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::graph::test_utils::ComponentGraphBuilder; + + #[test] + fn test_zero_consumers() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a battery inverter to the grid, without a battery meter. + let inv_bat_chain = builder.inv_bat_chain(1); + builder.connect(grid, inv_bat_chain); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!(formula, "0.0"); + + Ok(()) + } + + #[test] + fn test_consumer_formula_with_grid_meter() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a grid meter to the grid, with no successors. + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!(formula, "MAX(0.0, #1)"); + + // Add a battery meter with one battery inverter and one battery to the + // grid meter. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 2); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + // Formula subtracts the battery meter from the grid meter, and the + // battery inverter from the battery meter. + assert_eq!( + formula, + "MAX(0.0, #1 - COALESCE(#2, #3)) + COALESCE(MAX(0.0, #2 - #3), 0.0)" + ); + + // Add a solar meter with two solar inverters to the grid meter. + let meter_pv_chain = builder.meter_pv_chain(2); + builder.connect(grid_meter, meter_pv_chain); + + assert_eq!(meter_pv_chain.component_id(), 5); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of grid meter from all its suceessors + "MAX(0.0, #1 - COALESCE(#2, #3) - COALESCE(#5, #7 + #6)) + ", + // difference of battery meter from battery inverter and pv + // meter from the two pv inverters. + "COALESCE(MAX(0.0, #2 - #3), 0.0) + COALESCE(MAX(0.0, #5 - #6 - #7), 0.0)", + ) + ); + + // Add a "mixed" meter with a CHP, an ev charger and a solar inverter to + // the grid meter. + let solar_inverter = builder.solar_inverter(); + let chp = builder.chp(); + let ev_charger = builder.ev_charger(); + let meter = builder.meter(); + builder.connect(meter, solar_inverter); + builder.connect(meter, chp); + builder.connect(meter, ev_charger); + builder.connect(grid_meter, meter); + + assert_eq!(solar_inverter.component_id(), 8); + assert_eq!(chp.component_id(), 9); + assert_eq!(ev_charger.component_id(), 10); + assert_eq!(meter.component_id(), 11); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of grid meter from all its suceessors + "MAX(0.0, ", + "#1 - COALESCE(#2, #3) - COALESCE(#5, #7 + #6) - COALESCE(#11, #10 + #9 + #8)) + ", + // difference of battery meter from battery inverter and pv + // meter from the two pv inverters. + "COALESCE(MAX(0.0, #2 - #3), 0.0) + COALESCE(MAX(0.0, #5 - #6 - #7), 0.0) + ", + // difference of "mixed" meter from its successors. + "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0)" + ) + ); + + // add a battery chain to the grid meter and a dangling meter to the grid. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + let dangling_meter = builder.meter(); + builder.connect(grid_meter, meter_bat_chain); + builder.connect(grid, dangling_meter); + + assert_eq!(meter_bat_chain.component_id(), 12); + assert_eq!(dangling_meter.component_id(), 15); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of grid meter from all its suceessors + "MAX(0.0, ", + "#1 - COALESCE(#2, #3) - COALESCE(#5, #7 + #6) - COALESCE(#11, #10 + #9 + #8) - ", + "COALESCE(#12, #13)", + ") + ", + // difference of battery meter from battery inverter and pv + // meter from the two pv inverters. + "COALESCE(MAX(0.0, #2 - #3), 0.0) + COALESCE(MAX(0.0, #5 - #6 - #7), 0.0) + ", + // difference of "mixed" meter from its successors. + "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0) + ", + // difference of second battery meter from inverter. + "COALESCE(MAX(0.0, #12 - #13), 0.0) + ", + // consumption component of the dangling meter. + "MAX(0.0, #15)" + ) + ); + + Ok(()) + } + + #[test] + fn test_consumer_formula_without_grid_meter() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a meter-inverter-battery chain to the grid component. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 1); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + // Formula subtracts inverter from battery meter, or shows zero + // consumption if either of the components have no data. + assert_eq!(formula, "COALESCE(MAX(0.0, #1 - #2), 0.0)"); + + // Add a pv meter with one solar inverter and two dangling meter. + let meter_pv_chain = builder.meter_pv_chain(1); + let dangling_meter_1 = builder.meter(); + let dangling_meter_2 = builder.meter(); + builder.connect(grid, meter_pv_chain); + builder.connect(grid, dangling_meter_1); + builder.connect(grid, dangling_meter_2); + + assert_eq!(meter_pv_chain.component_id(), 4); + assert_eq!(dangling_meter_1.component_id(), 6); + assert_eq!(dangling_meter_2.component_id(), 7); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // subtract meter successors from meters + "COALESCE(MAX(0.0, #1 - #2), 0.0) + COALESCE(MAX(0.0, #4 - #5), 0.0) + ", + // dangling meters + "MAX(0.0, #6) + MAX(0.0, #7)" + ) + ); + + // Add a battery inverter to the grid, without a battery meter. + // + // This shouldn't show up in the formula, because battery inverter + // consumption is charging, not site consumption. + let inv_bat_chain = builder.inv_bat_chain(1); + builder.connect(grid, inv_bat_chain); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // subtract meter successors from meters + "COALESCE(MAX(0.0, #1 - #2), 0.0) + COALESCE(MAX(0.0, #4 - #5), 0.0) + ", + // dangling meters + "MAX(0.0, #6) + MAX(0.0, #7)" + ) + ); + + // Add a PV inverter and a CHP to the grid, without a meter. + // + // Their consumption is counted as site consumption, because they can't + // be taken out, by discharging the batteries, for example. + let pv_inv = builder.solar_inverter(); + let chp = builder.chp(); + builder.connect(grid, pv_inv); + builder.connect(grid, chp); + + assert_eq!(pv_inv.component_id(), 10); + assert_eq!(chp.component_id(), 11); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // subtract meter successors from meters + "COALESCE(MAX(0.0, #1 - #2), 0.0) + COALESCE(MAX(0.0, #4 - #5), 0.0) + ", + // dangling meters + "MAX(0.0, #6) + MAX(0.0, #7) + ", + // PV inverter and CHP + "MAX(0.0, #11) + MAX(0.0, #10)", + ) + ); + + Ok(()) + } + + #[test] + fn test_consumer_formula_diamond_meters() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add three meters to the grid + let grid_meter_1 = builder.meter(); + let grid_meter_2 = builder.meter(); + let grid_meter_3 = builder.meter(); + builder.connect(grid, grid_meter_1); + builder.connect(grid, grid_meter_2); + builder.connect(grid, grid_meter_3); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!(formula, "MAX(0.0, #1) + MAX(0.0, #2) + MAX(0.0, #3)"); + + // Add two solar inverters with two grid meters as predecessors. + let meter_pv_chain_1 = builder.meter_pv_chain(1); + let meter_pv_chain_2 = builder.meter_pv_chain(1); + builder.connect(grid_meter_1, meter_pv_chain_1); + builder.connect(grid_meter_1, meter_pv_chain_2); + builder.connect(grid_meter_2, meter_pv_chain_1); + builder.connect(grid_meter_2, meter_pv_chain_2); + + assert_eq!(meter_pv_chain_1.component_id(), 4); + assert_eq!(meter_pv_chain_2.component_id(), 6); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of pv powers from first two grid meters + "MAX(0.0, #1 + #2 - COALESCE(#4, #5) - COALESCE(#6, #7)) + ", + // third grid meter still dangling + "MAX(0.0, #3) + ", + // difference of solar inverters from their meters + "COALESCE(MAX(0.0, #4 - #5), 0.0) + COALESCE(MAX(0.0, #6 - #7), 0.0)" + ) + ); + + // Add a meter to grid meter 3, and then add the two solar inverters to + // that meter. + let meter = builder.meter(); + builder.connect(grid_meter_3, meter); + builder.connect(meter, meter_pv_chain_1); + builder.connect(meter, meter_pv_chain_2); + + assert_eq!(meter.component_id(), 8); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of pv powers from first two grid meters and meter#8 + "MAX(0.0, #1 + #8 + #2 - COALESCE(#4, #5) - COALESCE(#6, #7)) + ", + // difference of meter#8 from third grid meter + "MAX(0.0, #3 - #8) + ", + // difference of solar inverters from their meters + "COALESCE(MAX(0.0, #4 - #5), 0.0) + COALESCE(MAX(0.0, #6 - #7), 0.0)" + ) + ); + + // Add a battery inverter to the first grid meter. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid_meter_1, meter_bat_chain); + + let graph = builder.build()?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of pv and battery powers from first two grid + // meters and meter#8 + "MAX(0.0, ", + "#1 + #8 + #2 - COALESCE(#4, #5) - COALESCE(#6, #7) - COALESCE(#9, #10)", + ") + ", + // difference of meter#8 from third grid meter + "MAX(0.0, #3 - #8) + ", + // difference of solar inverters from their meters + "COALESCE(MAX(0.0, #4 - #5), 0.0) + COALESCE(MAX(0.0, #6 - #7), 0.0) + ", + // difference of battery inverter from battery meter + "COALESCE(MAX(0.0, #9 - #10), 0.0)" + ) + ); + + Ok(()) + } +} From 3750ea04e570dc63e4afc995ae953c38e13a54d3 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:33:32 +0200 Subject: [PATCH 08/19] Add the `grid` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 5 ++ src/graph/formulas/generators.rs | 1 + src/graph/formulas/generators/grid.rs | 97 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/graph/formulas/generators/grid.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 561a3a4..f41c739 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -22,4 +22,9 @@ where pub fn consumer_formula(&self) -> Result { generators::consumer::ConsumerFormulaBuilder::try_new(self)?.build() } + + /// Returns a string representing the grid formula for the graph. + pub fn grid_formula(&self) -> Result { + generators::grid::GridFormulaBuilder::try_new(self)?.build() + } } diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index e360fda..57d3033 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -4,3 +4,4 @@ //! Formula generators for standard metrics. pub(super) mod consumer; +pub(super) mod grid; diff --git a/src/graph/formulas/generators/grid.rs b/src/graph/formulas/generators/grid.rs new file mode 100644 index 0000000..b8ddedb --- /dev/null +++ b/src/graph/formulas/generators/grid.rs @@ -0,0 +1,97 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating grid formulas. + +use crate::{ComponentGraph, Edge, Error, Node}; + +pub(crate) struct GridFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + graph: &'a ComponentGraph, +} + +impl<'a, N, E> GridFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new(graph: &'a ComponentGraph) -> Result { + Ok(Self { graph }) + } + + /// Generates the grid formula for the given node. + /// + /// The grid formula is the sum of all components connected to the grid. + /// This formula can be used for calculating power or current metrics at the + /// grid connection point. + pub fn build(self) -> Result { + let mut expr = None; + for comp in self.graph.successors(self.graph.root_id)? { + let comp = self.graph.fallback_expr([comp.component_id()], true)?; + expr = match expr { + None => Some(comp), + Some(e) => Some(comp + e), + }; + } + Ok(expr + .map(|e| e.to_string()) + .unwrap_or_else(|| "0.0".to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::graph::test_utils::ComponentGraphBuilder; + + #[test] + fn test_grid_formula() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a grid meter and a battery chain behind it. + let grid_meter = builder.meter(); + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid, grid_meter); + builder.connect(grid_meter, meter_bat_chain); + + let graph = builder.build()?; + let formula = graph.grid_formula()?; + assert_eq!(formula, "#1"); + + // Add an additional dangling meter, and a PV chain and a battery chain + // to the grid + let dangling_meter = builder.meter(); + let meter_bat_chain = builder.meter_bat_chain(1, 1); + let meter_pv_chain = builder.meter_pv_chain(1); + builder.connect(grid, dangling_meter); + builder.connect(grid, meter_bat_chain); + builder.connect(grid, meter_pv_chain); + + assert_eq!(dangling_meter.component_id(), 5); + assert_eq!(meter_bat_chain.component_id(), 6); + assert_eq!(meter_pv_chain.component_id(), 9); + + let graph = builder.build()?; + let formula = graph.grid_formula()?; + assert_eq!(formula, "#1 + #5 + COALESCE(#6, #7) + COALESCE(#9, #10)"); + + // Add a PV inverter to the grid, without a meter. + let pv_inverter = builder.solar_inverter(); + builder.connect(grid, pv_inverter); + + assert_eq!(pv_inverter.component_id(), 11); + + let graph = builder.build()?; + let formula = graph.grid_formula()?; + assert_eq!( + formula, + "#1 + #5 + COALESCE(#6, #7) + COALESCE(#9, #10) + #11" + ); + + Ok(()) + } +} From 786b42f69d81fc8186601d3b28029b728d99dc5a Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:36:38 +0200 Subject: [PATCH 09/19] Add the `producer` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 5 + src/graph/formulas/generators.rs | 1 + src/graph/formulas/generators/producer.rs | 160 ++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 src/graph/formulas/generators/producer.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index f41c739..74d27a2 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -27,4 +27,9 @@ where pub fn grid_formula(&self) -> Result { generators::grid::GridFormulaBuilder::try_new(self)?.build() } + + /// Returns a string representing the producer formula for the graph. + pub fn producer_formula(&self) -> Result { + generators::producer::ProducerFormulaBuilder::try_new(self)?.build() + } } diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index 57d3033..aea44dd 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -5,3 +5,4 @@ pub(super) mod consumer; pub(super) mod grid; +pub(super) mod producer; diff --git a/src/graph/formulas/generators/producer.rs b/src/graph/formulas/generators/producer.rs new file mode 100644 index 0000000..a2494bd --- /dev/null +++ b/src/graph/formulas/generators/producer.rs @@ -0,0 +1,160 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating producer formulas. + +use super::super::expr::Expr; +use crate::component_category::CategoryPredicates; +use crate::{ComponentGraph, Edge, Error, Node}; + +pub(crate) struct ProducerFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + graph: &'a ComponentGraph, +} + +impl<'a, N, E> ProducerFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new(graph: &'a ComponentGraph) -> Result { + Ok(Self { graph }) + } + + /// Generates the production formula. + /// + /// The production formula is the sum of all the PV and CHP components in + /// the graph. + pub fn build(self) -> Result { + let mut expr = None; + for component_id in self.graph.find_all( + self.graph.root_id, + |node| { + self.graph.is_pv_meter(node.component_id()).unwrap_or(false) + || self + .graph + .is_chp_meter(node.component_id()) + .unwrap_or(false) + || node.is_pv_inverter() + || node.is_chp() + }, + false, + )? { + let comp_expr = Self::min_zero(self.graph.fallback_expr([component_id], false)?); + expr = match expr { + None => Some(comp_expr), + Some(e) => Some(e + comp_expr), + }; + } + Ok(expr + .map(|e| e.to_string()) + .unwrap_or_else(|| "0.0".to_string())) + } + + /// Returns a formula expression for just the production part of the given + /// component as a formula expression. + /// + /// This is done by clamping the expression to a minimum of 0.0. + fn min_zero(expr: Expr) -> Expr { + Expr::min(vec![Expr::number(0.0), expr]) + } +} + +#[cfg(test)] +mod tests { + use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + + #[test] + fn test_producer_formula() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + // Add a grid meter and a PV meter with two PV inverters behind it. + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!(formula, "0.0"); + + let meter_pv_chain = builder.meter_pv_chain(2); + builder.connect(grid_meter, meter_pv_chain); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!(formula, "MIN(0.0, COALESCE(#4 + #3, #2))"); + + // Add a CHP meter to the grid with a CHP behind it. + let meter_chp_chain = builder.meter_chp_chain(1); + builder.connect(grid, meter_chp_chain); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!( + formula, + "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5))" + ); + + // Add a CHP to the grid, without a meter. + let chp = builder.chp(); + builder.connect(grid, chp); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!( + formula, + "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + MIN(0.0, #7)" + ); + + // Add a PV inverter to the grid_meter. + let pv_inverter = builder.solar_inverter(); + builder.connect(grid_meter, pv_inverter); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!( + formula, + concat!( + "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + ", + "MIN(0.0, #7) + MIN(0.0, #8)" + ) + ); + + // Add a battery chain to the grid meter. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid_meter, meter_bat_chain); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!( + formula, + concat!( + "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + ", + "MIN(0.0, #7) + MIN(0.0, #8)" + ) + ); + + // Add a meter to the grid meter, that has a PV inverter and a CHP behind it. + let meter = builder.meter(); + let pv_inverter = builder.solar_inverter(); + let chp = builder.chp(); + builder.connect(meter, pv_inverter); + builder.connect(meter, chp); + builder.connect(grid_meter, meter); + + let graph = builder.build()?; + let formula = graph.producer_formula()?; + assert_eq!( + formula, + concat!( + "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + ", + "MIN(0.0, #7) + MIN(0.0, #8) + MIN(0.0, #13) + MIN(0.0, #14)" + ) + ); + + Ok(()) + } +} From 399cce7c15be76c390d417dec293e4ad752cf966 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:40:35 +0200 Subject: [PATCH 10/19] Add the `battery` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 7 + src/graph/formulas/generators.rs | 1 + src/graph/formulas/generators/battery.rs | 221 +++++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/graph/formulas/generators/battery.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 74d27a2..74564fb 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -3,6 +3,8 @@ //! Methods for building formulas for various microgrid metrics. +use std::collections::BTreeSet; + use crate::ComponentGraph; use crate::Edge; use crate::Error; @@ -32,4 +34,9 @@ where pub fn producer_formula(&self) -> Result { generators::producer::ProducerFormulaBuilder::try_new(self)?.build() } + + /// Returns a string representing the battery formula for the graph. + pub fn battery_formula(&self, battery_ids: Option>) -> Result { + generators::battery::BatteryFormulaBuilder::try_new(self, battery_ids)?.build() + } } diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index aea44dd..0a3fe35 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -3,6 +3,7 @@ //! Formula generators for standard metrics. +pub(super) mod battery; pub(super) mod consumer; pub(super) mod grid; pub(super) mod producer; diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs new file mode 100644 index 0000000..75885da --- /dev/null +++ b/src/graph/formulas/generators/battery.rs @@ -0,0 +1,221 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating producer formulas. + +use std::collections::BTreeSet; + +use crate::component_category::CategoryPredicates; +use crate::{ComponentGraph, Edge, Error, Node}; + +pub(crate) struct BatteryFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + graph: &'a ComponentGraph, + inverter_ids: BTreeSet, +} + +impl<'a, N, E> BatteryFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new( + graph: &'a ComponentGraph, + battery_ids: Option>, + ) -> Result { + let inverter_ids = if let Some(battery_ids) = battery_ids { + Self::find_inverter_ids(graph, &battery_ids)? + } else { + graph.find_all(graph.root_id, |node| node.is_battery_inverter(), false)? + }; + Ok(Self { + graph, + inverter_ids, + }) + } + + /// Generates the battery formula. + /// + /// This is the sum of all battery_inverters in the graph. If the + /// battery_ids are provided, only the batteries with the given ids are + /// included in the formula. + pub fn build(self) -> Result { + if self.inverter_ids.is_empty() { + return Ok("0.0".to_string()); + } + + self.graph + .fallback_expr(self.inverter_ids, false) + .map(|expr| expr.to_string()) + } + + fn find_inverter_ids( + graph: &ComponentGraph, + battery_ids: &BTreeSet, + ) -> Result, Error> { + let mut inverter_ids = BTreeSet::new(); + for battery_id in battery_ids { + if !graph.component(*battery_id)?.is_battery() { + return Err(Error::invalid_component(format!( + "Component with id {} is not a battery.", + battery_id + ))); + } + for sibling in graph.siblings_from_predecessors(*battery_id)? { + if !battery_ids.contains(&sibling.component_id()) { + return Err(Error::invalid_component(format!( + "Battery {} can't be in a formula without all its siblings: {:?}.", + battery_id, + graph + .siblings_from_predecessors(*battery_id)? + .map(|x| x.component_id()) + .collect::>() + ))); + } + } + inverter_ids.extend(graph.predecessors(*battery_id)?.map(|x| x.component_id())); + } + Ok(inverter_ids) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + + #[test] + fn test_battery_formula() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + let graph = builder.build()?; + let formula = graph.battery_formula(None)?; + assert_eq!(formula, "0.0"); + + // Add a battery meter with one inverter and one battery. + let meter_bat_chain = builder.meter_bat_chain(1, 1); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(grid_meter.component_id(), 1); + assert_eq!(meter_bat_chain.component_id(), 2); + + let graph = builder.build()?; + let formula = graph.battery_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a second battery meter with one inverter and two batteries. + let meter_bat_chain = builder.meter_bat_chain(1, 2); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 5); + + let graph = builder.build()?; + let formula = graph.battery_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#6, #5)"); + + let formula = graph.battery_formula(Some(BTreeSet::from([4])))?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + let formula = graph.battery_formula(Some(BTreeSet::from([7, 8])))?; + assert_eq!(formula, "COALESCE(#6, #5)"); + + let formula = graph + .battery_formula(Some(BTreeSet::from([4, 8, 7]))) + .unwrap(); + assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#6, #5)"); + + // Add a third battery meter with two inverters with two connected batteries. + let meter_bat_chain = builder.meter_bat_chain(2, 2); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 9); + + let graph = builder.build()?; + let formula = graph.battery_formula(None)?; + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#6, #5) + COALESCE(#11 + #10, #9)" + ); + + let formula = graph + .battery_formula(Some(BTreeSet::from([12, 13]))) + .unwrap(); + assert_eq!(formula, "COALESCE(#11 + #10, #9)"); + + // add a PV meter with two PV inverters. + let meter_pv_chain = builder.meter_pv_chain(2); + builder.connect(grid_meter, meter_pv_chain); + + assert_eq!(meter_pv_chain.component_id(), 14); + + let graph = builder.build()?; + let formula = graph.battery_formula(None)?; + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#6, #5) + COALESCE(#11 + #10, #9)" + ); + + // add a battery meter with two inverters that have their own batteries. + let meter = builder.meter(); + builder.connect(grid, meter); + let inv_bat_chain = builder.inv_bat_chain(1); + builder.connect(meter, inv_bat_chain); + + assert_eq!(meter.component_id(), 17); + assert_eq!(inv_bat_chain.component_id(), 18); + + let inv_bat_chain = builder.inv_bat_chain(1); + builder.connect(meter, inv_bat_chain); + + assert_eq!(inv_bat_chain.component_id(), 20); + + let graph = builder.build()?; + let formula = graph.battery_formula(None)?; + assert_eq!( + formula, + concat!( + "COALESCE(#3, #2) + COALESCE(#6, #5) + ", + "COALESCE(#11 + #10, #9) + COALESCE(#20 + #18, #17)" + ) + ); + + let formula = graph + .battery_formula(Some(BTreeSet::from([19, 21]))) + .unwrap(); + assert_eq!(formula, "COALESCE(#20 + #18, #17)"); + + let formula = graph.battery_formula(Some(BTreeSet::from([19]))).unwrap(); + assert_eq!(formula, "#18"); + + let formula = graph.battery_formula(Some(BTreeSet::from([21]))).unwrap(); + assert_eq!(formula, "#20"); + + let formula = graph + .battery_formula(Some(BTreeSet::from([4, 12, 13, 19]))) + .unwrap(); + assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#11 + #10, #9) + #18"); + + // Failure cases: + let formula = graph.battery_formula(Some(BTreeSet::from([17]))); + assert_eq!( + formula.unwrap_err().to_string(), + "InvalidComponent: Component with id 17 is not a battery." + ); + + let formula = graph.battery_formula(Some(BTreeSet::from([12]))); + assert_eq!( + formula.unwrap_err().to_string(), + "InvalidComponent: Battery 12 can't be in a formula without all its siblings: [13]." + ); + + Ok(()) + } +} From 13af13105ca188dfecb2e35b0380a2d5bc2416d5 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:41:56 +0200 Subject: [PATCH 11/19] Add the `chp` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 5 + src/graph/formulas/generators.rs | 1 + src/graph/formulas/generators/chp.rs | 154 +++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/graph/formulas/generators/chp.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 74564fb..699fcaf 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -39,4 +39,9 @@ where pub fn battery_formula(&self, battery_ids: Option>) -> Result { generators::battery::BatteryFormulaBuilder::try_new(self, battery_ids)?.build() } + + /// Returns a string representing the CHP formula for the graph. + pub fn chp_formula(&self, chp_ids: Option>) -> Result { + generators::chp::CHPFormulaBuilder::try_new(self, chp_ids)?.build() + } } diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index 0a3fe35..d5961ed 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -4,6 +4,7 @@ //! Formula generators for standard metrics. pub(super) mod battery; +pub(super) mod chp; pub(super) mod consumer; pub(super) mod grid; pub(super) mod producer; diff --git a/src/graph/formulas/generators/chp.rs b/src/graph/formulas/generators/chp.rs new file mode 100644 index 0000000..8ef52ac --- /dev/null +++ b/src/graph/formulas/generators/chp.rs @@ -0,0 +1,154 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating producer formulas. + +use std::collections::BTreeSet; + +use crate::component_category::CategoryPredicates; +use crate::{ComponentGraph, Edge, Error, Node}; + +pub(crate) struct CHPFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + graph: &'a ComponentGraph, + chp_ids: BTreeSet, +} + +impl<'a, N, E> CHPFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new( + graph: &'a ComponentGraph, + chp_ids: Option>, + ) -> Result { + let chp_ids = if let Some(chp_ids) = chp_ids { + chp_ids + } else { + graph.find_all(graph.root_id, |node| node.is_chp(), false)? + }; + Ok(Self { graph, chp_ids }) + } + + /// Generates the chp formula. + /// + /// This is the sum of all CHPs in the graph. If the chp_ids are provided, + /// only the CHPs with the given ids are included in the formula. + pub fn build(self) -> Result { + if self.chp_ids.is_empty() { + return Ok("0.0".to_string()); + } + + for id in &self.chp_ids { + if !self.graph.component(*id)?.is_chp() { + return Err(Error::invalid_component(format!( + "Component with id {} is not a CHP.", + id + ))); + } + } + + self.graph + .fallback_expr(self.chp_ids, false) + .map(|expr| expr.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + + #[test] + fn test_chp_formula() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + let graph = builder.build()?; + let formula = graph.chp_formula(None)?; + assert_eq!(formula, "0.0"); + + // Add a chp meter with one chp + let meter_chp_chain = builder.meter_chp_chain(1); + builder.connect(grid_meter, meter_chp_chain); + + assert_eq!(grid_meter.component_id(), 1); + assert_eq!(meter_chp_chain.component_id(), 2); + + let graph = builder.build()?; + let formula = graph.chp_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a battery meter with one inverter and two batteries. + let meter_bat_chain = builder.meter_bat_chain(1, 2); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 4); + + let graph = builder.build()?; + let formula = graph.chp_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a chp meter with two CHPs. + let meter_chp_chain = builder.meter_chp_chain(2); + builder.connect(grid_meter, meter_chp_chain); + + assert_eq!(meter_chp_chain.component_id(), 8); + + let graph = builder.build()?; + let formula = graph.chp_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#10 + #9, #8)"); + + let formula = graph.chp_formula(Some(BTreeSet::from([10, 3]))).unwrap(); + assert_eq!(formula, "COALESCE(#3, #2) + #10"); + + // add a meter direct to the grid with three CHPs + let meter_chp_chain = builder.meter_chp_chain(3); + builder.connect(grid, meter_chp_chain); + + assert_eq!(meter_chp_chain.component_id(), 11); + + let graph = builder.build()?; + let formula = graph.chp_formula(None)?; + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)", + ); + + let formula = graph + .chp_formula(Some(BTreeSet::from([3, 9, 10, 12, 13]))) + .unwrap(); + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + #12 + #13" + ); + + let formula = graph + .chp_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14]))) + .unwrap(); + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)" + ); + + let formula = graph.chp_formula(Some(BTreeSet::from([10, 14]))).unwrap(); + assert_eq!(formula, "#10 + #14"); + + // Failure cases: + let formula = graph.chp_formula(Some(BTreeSet::from([8]))); + assert_eq!( + formula.unwrap_err().to_string(), + "InvalidComponent: Component with id 8 is not a CHP." + ); + + Ok(()) + } +} From 6da0f7b8c84c9317972f8e9f1566b9db8412e4e0 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:45:25 +0200 Subject: [PATCH 12/19] Add the `PV` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 5 + src/graph/formulas/generators.rs | 1 + src/graph/formulas/generators/pv.rs | 157 ++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/graph/formulas/generators/pv.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 699fcaf..8ef70bf 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -44,4 +44,9 @@ where pub fn chp_formula(&self, chp_ids: Option>) -> Result { generators::chp::CHPFormulaBuilder::try_new(self, chp_ids)?.build() } + + /// Returns a string representing the PV formula for the graph. + pub fn pv_formula(&self, pv_inverter_ids: Option>) -> Result { + generators::pv::PVFormulaBuilder::try_new(self, pv_inverter_ids)?.build() + } } diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index d5961ed..533b040 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -8,3 +8,4 @@ pub(super) mod chp; pub(super) mod consumer; pub(super) mod grid; pub(super) mod producer; +pub(super) mod pv; diff --git a/src/graph/formulas/generators/pv.rs b/src/graph/formulas/generators/pv.rs new file mode 100644 index 0000000..97cf7ef --- /dev/null +++ b/src/graph/formulas/generators/pv.rs @@ -0,0 +1,157 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating producer formulas. + +use std::collections::BTreeSet; + +use crate::component_category::CategoryPredicates; +use crate::{ComponentGraph, Edge, Error, Node}; + +pub(crate) struct PVFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + graph: &'a ComponentGraph, + pv_inverter_ids: BTreeSet, +} + +impl<'a, N, E> PVFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new( + graph: &'a ComponentGraph, + pv_inverter_ids: Option>, + ) -> Result { + let pv_inverter_ids = if let Some(pv_inverter_ids) = pv_inverter_ids { + pv_inverter_ids + } else { + graph.find_all(graph.root_id, |node| node.is_pv_inverter(), false)? + }; + Ok(Self { + graph, + pv_inverter_ids, + }) + } + + /// Generates the PV formula. + /// + /// This is the sum of all PV inverters in the graph. If the pv_inverter_ids are provided, + /// only the PV inverters with the given ids are included in the formula. + pub fn build(self) -> Result { + if self.pv_inverter_ids.is_empty() { + return Ok("0.0".to_string()); + } + + for id in &self.pv_inverter_ids { + if !self.graph.component(*id)?.is_pv_inverter() { + return Err(Error::invalid_component(format!( + "Component with id {} is not a PV inverter.", + id + ))); + } + } + + self.graph + .fallback_expr(self.pv_inverter_ids, false) + .map(|expr| expr.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + + #[test] + fn test_pv_formula() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + let graph = builder.build()?; + let formula = graph.pv_formula(None)?; + assert_eq!(formula, "0.0"); + + // Add a PV meter with one PV inverter. + let meter_pv_chain = builder.meter_pv_chain(1); + builder.connect(grid_meter, meter_pv_chain); + + assert_eq!(grid_meter.component_id(), 1); + assert_eq!(meter_pv_chain.component_id(), 2); + + let graph = builder.build()?; + let formula = graph.pv_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a battery meter with one inverter and two batteries. + let meter_bat_chain = builder.meter_bat_chain(1, 2); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 4); + + let graph = builder.build()?; + let formula = graph.pv_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a PV meter with two PV inverters. + let meter_pv_chain = builder.meter_pv_chain(2); + builder.connect(grid_meter, meter_pv_chain); + + assert_eq!(meter_pv_chain.component_id(), 8); + + let graph = builder.build()?; + let formula = graph.pv_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#10 + #9, #8)"); + + let formula = graph.pv_formula(Some(BTreeSet::from([10, 3]))).unwrap(); + assert_eq!(formula, "COALESCE(#3, #2) + #10"); + + // add a meter direct to the grid with three PV inverters + let meter_pv_chain = builder.meter_pv_chain(3); + builder.connect(grid, meter_pv_chain); + + assert_eq!(meter_pv_chain.component_id(), 11); + + let graph = builder.build()?; + let formula = graph.pv_formula(None)?; + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)", + ); + + let formula = graph + .pv_formula(Some(BTreeSet::from([3, 9, 10, 12, 13]))) + .unwrap(); + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + #12 + #13" + ); + + let formula = graph + .pv_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14]))) + .unwrap(); + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)" + ); + + let formula = graph.pv_formula(Some(BTreeSet::from([10, 14]))).unwrap(); + assert_eq!(formula, "#10 + #14"); + + // Failure cases: + let formula = graph.pv_formula(Some(BTreeSet::from([8]))); + assert_eq!( + formula.unwrap_err().to_string(), + "InvalidComponent: Component with id 8 is not a PV inverter." + ); + + Ok(()) + } +} From 30e4572ab8a0cb88e83b567abfa6fea266d46c4b Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 23:46:22 +0200 Subject: [PATCH 13/19] Add the `EV charger` formula generator Signed-off-by: Sahas Subramanian --- src/graph/formulas.rs | 8 + src/graph/formulas/generators.rs | 1 + src/graph/formulas/generators/ev_charger.rs | 161 ++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 src/graph/formulas/generators/ev_charger.rs diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 8ef70bf..9f89736 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -49,4 +49,12 @@ where pub fn pv_formula(&self, pv_inverter_ids: Option>) -> Result { generators::pv::PVFormulaBuilder::try_new(self, pv_inverter_ids)?.build() } + + /// Returns a string representing the EV charger formula for the graph. + pub fn ev_charger_formula( + &self, + ev_charger_ids: Option>, + ) -> Result { + generators::ev_charger::EVChargerFormulaBuilder::try_new(self, ev_charger_ids)?.build() + } } diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index 533b040..54bfd58 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -6,6 +6,7 @@ pub(super) mod battery; pub(super) mod chp; pub(super) mod consumer; +pub(super) mod ev_charger; pub(super) mod grid; pub(super) mod producer; pub(super) mod pv; diff --git a/src/graph/formulas/generators/ev_charger.rs b/src/graph/formulas/generators/ev_charger.rs new file mode 100644 index 0000000..b15dc7d --- /dev/null +++ b/src/graph/formulas/generators/ev_charger.rs @@ -0,0 +1,161 @@ +// License: MIT +// Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +//! This module contains the methods for generating producer formulas. + +use std::collections::BTreeSet; + +use crate::component_category::CategoryPredicates; +use crate::{ComponentGraph, Edge, Error, Node}; + +pub(crate) struct EVChargerFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + graph: &'a ComponentGraph, + ev_charger_ids: BTreeSet, +} + +impl<'a, N, E> EVChargerFormulaBuilder<'a, N, E> +where + N: Node, + E: Edge, +{ + pub fn try_new( + graph: &'a ComponentGraph, + ev_charger_ids: Option>, + ) -> Result { + let ev_charger_ids = if let Some(ev_charger_ids) = ev_charger_ids { + ev_charger_ids + } else { + graph.find_all(graph.root_id, |node| node.is_ev_charger(), false)? + }; + Ok(Self { + graph, + ev_charger_ids, + }) + } + + /// Generates the EV charger formula. + /// + /// This is the sum of all EV chargers in the graph. If the ev_charger_ids are provided, + /// only the EV chargers with the given ids are included in the formula. + pub fn build(self) -> Result { + if self.ev_charger_ids.is_empty() { + return Ok("0.0".to_string()); + } + + for id in &self.ev_charger_ids { + if !self.graph.component(*id)?.is_ev_charger() { + return Err(Error::invalid_component(format!( + "Component with id {} is not an EV charger.", + id + ))); + } + } + + self.graph + .fallback_expr(self.ev_charger_ids, false) + .map(|expr| expr.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + + #[test] + fn test_ev_charger_formula() -> Result<(), Error> { + let mut builder = ComponentGraphBuilder::new(); + let grid = builder.grid(); + + let grid_meter = builder.meter(); + builder.connect(grid, grid_meter); + + let graph = builder.build()?; + let formula = graph.ev_charger_formula(None)?; + assert_eq!(formula, "0.0"); + + // Add a EV charger meter with one EV charger. + let meter_ev_charger_chain = builder.meter_ev_charger_chain(1); + builder.connect(grid_meter, meter_ev_charger_chain); + + assert_eq!(grid_meter.component_id(), 1); + assert_eq!(meter_ev_charger_chain.component_id(), 2); + + let graph = builder.build()?; + let formula = graph.ev_charger_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a battery meter with one inverter and two batteries. + let meter_bat_chain = builder.meter_bat_chain(1, 2); + builder.connect(grid_meter, meter_bat_chain); + + assert_eq!(meter_bat_chain.component_id(), 4); + + let graph = builder.build()?; + let formula = graph.ev_charger_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2)"); + + // Add a EV charger meter with two EV chargers. + let meter_ev_charger_chain = builder.meter_ev_charger_chain(2); + builder.connect(grid_meter, meter_ev_charger_chain); + + assert_eq!(meter_ev_charger_chain.component_id(), 8); + + let graph = builder.build()?; + let formula = graph.ev_charger_formula(None)?; + assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#10 + #9, #8)"); + + let formula = graph + .ev_charger_formula(Some(BTreeSet::from([10, 3]))) + .unwrap(); + assert_eq!(formula, "COALESCE(#3, #2) + #10"); + + // add a meter direct to the grid with three EV chargers + let meter_ev_charger_chain = builder.meter_ev_charger_chain(3); + builder.connect(grid, meter_ev_charger_chain); + + assert_eq!(meter_ev_charger_chain.component_id(), 11); + + let graph = builder.build()?; + let formula = graph.ev_charger_formula(None)?; + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)", + ); + + let formula = graph + .ev_charger_formula(Some(BTreeSet::from([3, 9, 10, 12, 13]))) + .unwrap(); + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + #12 + #13" + ); + + let formula = graph + .ev_charger_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14]))) + .unwrap(); + assert_eq!( + formula, + "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)" + ); + + let formula = graph + .ev_charger_formula(Some(BTreeSet::from([10, 14]))) + .unwrap(); + assert_eq!(formula, "#10 + #14"); + + // Failure cases: + let formula = graph.ev_charger_formula(Some(BTreeSet::from([8]))); + assert_eq!( + formula.unwrap_err().to_string(), + "InvalidComponent: Component with id 8 is not an EV charger." + ); + + Ok(()) + } +} From 8fd3b4639d5f41aaa441d6384bcc7746e2f57e63 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 21 Oct 2024 03:42:28 +0200 Subject: [PATCH 14/19] Use `gh-action-cargo-test` in CI Signed-off-by: Sahas Subramanian --- .github/workflows/ci.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 112c6d8..0e85354 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,13 +16,7 @@ jobs: - name: Fetch sources uses: actions/checkout@v4 with: - submodules: true - - - name: Check formatting - run: cargo fmt --check - - - name: Run linter - run: cargo clippy -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic + submodules: recursive - name: Run tests - run: cargo test + uses: frequenz-floss/gh-action-cargo-test@v1.0.0 From 157e1c70f94fe71975a7ddff8537f0344d522e62 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 21 Oct 2024 04:21:50 +0200 Subject: [PATCH 15/19] Update documentation Signed-off-by: Sahas Subramanian --- README.md | 6 +++--- src/graph/formulas.rs | 1 + src/lib.rs | 13 +++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12082a7..6cf3789 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ This is a library for representing the components of a microgrid and the connections between them as a Directed Acyclic Graph (DAG). Current features: - - validating the graph - - traversing the graph + - Validating the graph + - Traversing the graph + - Formula generation Upcoming features: - - Formula generation - Python bindings diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 9f89736..53e0b86 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -15,6 +15,7 @@ mod fallback; mod generators; mod traversal; +/// Formulas for various microgrid metrics. impl ComponentGraph where N: Node, diff --git a/src/lib.rs b/src/lib.rs index d92953a..1140d4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,19 @@ including checking that: If any of the validation steps fail, the method will return an [`Error`], and a [`ComponentGraph`] instance otherwise. + +## Formula generation + +The component graph library has methods for generating formulas for various +metrics of the microgrid. The following formulas are supported: + +- [`grid_formula`][ComponentGraph::grid_formula] +- [`producer_formula`][ComponentGraph::producer_formula] +- [`consumer_formula`][ComponentGraph::consumer_formula] +- [`pv_formula`][ComponentGraph::pv_formula] +- [`battery_formula`][ComponentGraph::battery_formula] +- [`ev_charger_formula`][ComponentGraph::ev_charger_formula] +- [`chp_formula`][ComponentGraph::chp_formula] */ mod component_category; From a3bdb636057bf8e098da4d387899e0705efb9bd5 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 21 Oct 2024 10:55:00 +0200 Subject: [PATCH 16/19] Remove `is_grid_meter` implementation With the new consumer power formula, it is no longer necessary to identify meters as grid meters. So this method can go away. If the SDK's traversal algorithms need it, they can use their own temporary implementation, until they can migrate to the new formulas. Signed-off-by: Sahas Subramanian --- src/graph/meter_roles.rs | 79 ---------------------------------------- 1 file changed, 79 deletions(-) diff --git a/src/graph/meter_roles.rs b/src/graph/meter_roles.rs index 55f2ede..046797f 100644 --- a/src/graph/meter_roles.rs +++ b/src/graph/meter_roles.rs @@ -11,62 +11,6 @@ where N: Node, E: Edge, { - /// Returns true if a node is a grid meter. - /// - /// A meter is identified as a grid meter if: - /// - it is a successor of the grid component, - /// - all its siblings are meters, - /// - if there are siblings, the successors of it and the successors of - /// its siblings are meters. - pub fn is_grid_meter(&self, component_id: u64) -> Result { - let component = self.component(component_id)?; - - // Component must be a meter. - if !component.is_meter() { - return Ok(false); - } - - let mut predecessors = self.predecessors(component_id)?; - - // The meter must have a grid as a predecessor. - let Some(grid) = predecessors.next() else { - return Ok(false); - }; - - let has_multiple_predecessors = predecessors.next().is_some(); - - if !grid.is_grid() || has_multiple_predecessors { - return Ok(false); - } - - // All siblings must be meters. - let mut num_grid_successors = 0; - let mut non_meter_successors = false; - for grid_successor in self.successors(grid.component_id())? { - if grid_successor.is_meter() { - num_grid_successors += 1; - } else { - return Ok(false); - } - let mut successors = self.successors(grid_successor.component_id())?; - if successors.any(|n| !n.is_meter()) { - non_meter_successors = true; - } - } - - // If there are no siblings, the meter is a grid meter. - if num_grid_successors == 1 { - return Ok(true); - } - - // If there are siblings, the meter is a grid meter if the successors of - // it and the successors of the siblings are meters. - if non_meter_successors { - return Ok(false); - } - Ok(true) - } - /// Returns true if the node is a PV meter. /// /// A meter is identified as a PV meter if: @@ -273,29 +217,6 @@ mod tests { Ok(found_meters) } - #[test] - fn test_is_grid_meter() -> Result<(), Error> { - let (components, connections) = nodes_and_edges(); - assert_eq!( - find_matching_components(components, connections, ComponentGraph::is_grid_meter)?, - vec![2], - ); - - let (components, connections) = with_multiple_grid_meters(); - assert_eq!( - find_matching_components(components, connections, ComponentGraph::is_grid_meter)?, - vec![2, 19, 20], - ); - - let (components, connections) = without_grid_meters(); - assert_eq!( - find_matching_components(components, connections, ComponentGraph::is_grid_meter)?, - vec![], - ); - - Ok(()) - } - #[test] fn test_is_pv_meter() -> Result<(), Error> { let (components, connections) = nodes_and_edges(); From b19af8f55cd8595c3a64e82b9abf83fc683c6e19 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 1 Nov 2024 16:11:05 +0100 Subject: [PATCH 17/19] Add additional per-component fallback to 0.0 Signed-off-by: Sahas Subramanian --- src/graph/formulas/fallback.rs | 81 ++++++++++++++++----- src/graph/formulas/generators/battery.rs | 51 +++++++++---- src/graph/formulas/generators/chp.rs | 42 +++++++++-- src/graph/formulas/generators/consumer.rs | 26 +++++-- src/graph/formulas/generators/ev_charger.rs | 43 +++++++++-- src/graph/formulas/generators/grid.rs | 7 +- src/graph/formulas/generators/producer.rs | 36 ++++++--- src/graph/formulas/generators/pv.rs | 43 +++++++++-- 8 files changed, 253 insertions(+), 76 deletions(-) diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs index 9527b4e..1b679cc 100644 --- a/src/graph/formulas/fallback.rs +++ b/src/graph/formulas/fallback.rs @@ -38,7 +38,7 @@ where pub(crate) graph: &'a ComponentGraph, } -impl<'a, N, E> FallbackExpr<'a, N, E> +impl FallbackExpr<'_, N, E> where N: Node, E: Edge, @@ -74,22 +74,43 @@ where return Ok(Some(Expr::component(component_id))); } - let successor_expr = self + let (sum_of_successors, sum_of_coalesced_successors) = self .graph .successors(component_id)? - .map(Expr::from) - .reduce(|a, b| a + b) + .map(|node| { + ( + Expr::from(node), + Expr::coalesce(vec![Expr::from(node), Expr::number(0.0)]), + ) + }) + .reduce(|a, b| (a.0 + b.0, a.1 + b.1)) .ok_or(Error::internal( "Can't find successors of components with successors.", ))?; - let exprs = if self.prefer_meters { - vec![Expr::component(component_id), successor_expr] + let has_multiple_successors = matches!(sum_of_successors, Expr::Add { .. }); + + let mut to_be_coalesced: Vec = vec![]; + + if !self.prefer_meters { + to_be_coalesced.push(sum_of_successors.clone()); + } + to_be_coalesced.push(Expr::component(component_id)); + + if self.prefer_meters { + if has_multiple_successors { + to_be_coalesced.push(sum_of_coalesced_successors); + } else { + to_be_coalesced.push(sum_of_successors); + to_be_coalesced.push(Expr::number(0.0)); + } + } else if has_multiple_successors { + to_be_coalesced.push(sum_of_coalesced_successors); } else { - vec![successor_expr, Expr::component(component_id)] - }; + to_be_coalesced.push(Expr::number(0.0)); + } - Ok(Some(Expr::coalesce(exprs))) + Ok(Some(Expr::coalesce(to_be_coalesced))) } /// Returns a fallback expression for components with the following categories: @@ -123,7 +144,10 @@ where .iter() .all(|sibling| component_ids.contains(&sibling.component_id())) { - return Ok(Some(Expr::component(component_id))); + return Ok(Some(Expr::coalesce(vec![ + Expr::component(component_id), + Expr::number(0.0), + ]))); } for sibling in siblings { @@ -170,13 +194,13 @@ mod tests { let graph = builder.build()?; let expr = graph.fallback_expr(vec![1, 2], false)?; - assert_eq!(expr.to_string(), "#1 + COALESCE(#3, #2)"); + assert_eq!(expr.to_string(), "#1 + COALESCE(#3, #2, 0.0)"); let expr = graph.fallback_expr(vec![1, 2], true)?; - assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3)"); + assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3, 0.0)"); let expr = graph.fallback_expr(vec![3], true)?; - assert_eq!(expr.to_string(), "COALESCE(#2, #3)"); + assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)"); // Add a battery meter with three inverter and three batteries let meter_bat_chain = builder.meter_bat_chain(3, 3); @@ -188,23 +212,39 @@ mod tests { let expr = graph.fallback_expr(vec![3, 5], false)?; assert_eq!( expr.to_string(), - "COALESCE(#3, #2) + COALESCE(#8 + #7 + #6, #5)" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(", + "#8 + #7 + #6, ", + "#5, ", + "COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0)", + ")" + ) ); let expr = graph.fallback_expr(vec![2, 5], true)?; assert_eq!( expr.to_string(), - "COALESCE(#2, #3) + COALESCE(#5, #8 + #7 + #6)" + concat!( + "COALESCE(#2, #3, 0.0) + ", + "COALESCE(#5, COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0))" + ) ); let expr = graph.fallback_expr(vec![2, 6, 7, 8], true)?; assert_eq!( expr.to_string(), - "COALESCE(#2, #3) + COALESCE(#5, #8 + #7 + #6)" + concat!( + "COALESCE(#2, #3, 0.0) + ", + "COALESCE(#5, COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0))" + ) ); let expr = graph.fallback_expr(vec![2, 7, 8], true)?; - assert_eq!(expr.to_string(), "COALESCE(#2, #3) + #7 + #8"); + assert_eq!( + expr.to_string(), + "COALESCE(#2, #3, 0.0) + COALESCE(#7, 0.0) + COALESCE(#8, 0.0)" + ); let meter = builder.meter(); let chp = builder.chp(); @@ -221,11 +261,14 @@ mod tests { let expr = graph.fallback_expr(vec![5, 12], true)?; assert_eq!( expr.to_string(), - "COALESCE(#5, #8 + #7 + #6) + COALESCE(#12, #14 + #13)" + concat!( + "COALESCE(#5, COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0)) + ", + "COALESCE(#12, COALESCE(#14, 0.0) + COALESCE(#13, 0.0))" + ) ); let expr = graph.fallback_expr(vec![7, 14], false)?; - assert_eq!(expr.to_string(), "#7 + #14"); + assert_eq!(expr.to_string(), "COALESCE(#7, 0.0) + COALESCE(#14, 0.0)"); Ok(()) } diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs index 75885da..c778658 100644 --- a/src/graph/formulas/generators/battery.rs +++ b/src/graph/formulas/generators/battery.rs @@ -109,7 +109,7 @@ mod tests { let graph = builder.build()?; let formula = graph.battery_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a second battery meter with one inverter and two batteries. let meter_bat_chain = builder.meter_bat_chain(1, 2); @@ -119,18 +119,18 @@ mod tests { let graph = builder.build()?; let formula = graph.battery_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#6, #5)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#6, #5, 0.0)"); let formula = graph.battery_formula(Some(BTreeSet::from([4])))?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); let formula = graph.battery_formula(Some(BTreeSet::from([7, 8])))?; - assert_eq!(formula, "COALESCE(#6, #5)"); + assert_eq!(formula, "COALESCE(#6, #5, 0.0)"); let formula = graph .battery_formula(Some(BTreeSet::from([4, 8, 7]))) .unwrap(); - assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#6, #5)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#6, #5, 0.0)"); // Add a third battery meter with two inverters with two connected batteries. let meter_bat_chain = builder.meter_bat_chain(2, 2); @@ -142,13 +142,20 @@ mod tests { let formula = graph.battery_formula(None)?; assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#6, #5) + COALESCE(#11 + #10, #9)" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#6, #5, 0.0) + ", + "COALESCE(#11 + #10, #9, COALESCE(#11, 0.0) + COALESCE(#10, 0.0))" + ) ); let formula = graph .battery_formula(Some(BTreeSet::from([12, 13]))) .unwrap(); - assert_eq!(formula, "COALESCE(#11 + #10, #9)"); + assert_eq!( + formula, + "COALESCE(#11 + #10, #9, COALESCE(#11, 0.0) + COALESCE(#10, 0.0))" + ); // add a PV meter with two PV inverters. let meter_pv_chain = builder.meter_pv_chain(2); @@ -160,7 +167,11 @@ mod tests { let formula = graph.battery_formula(None)?; assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#6, #5) + COALESCE(#11 + #10, #9)" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#6, #5, 0.0) + ", + "COALESCE(#11 + #10, #9, COALESCE(#11, 0.0) + COALESCE(#10, 0.0))" + ) ); // add a battery meter with two inverters that have their own batteries. @@ -182,26 +193,38 @@ mod tests { assert_eq!( formula, concat!( - "COALESCE(#3, #2) + COALESCE(#6, #5) + ", - "COALESCE(#11 + #10, #9) + COALESCE(#20 + #18, #17)" + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#6, #5, 0.0) + ", + "COALESCE(#11 + #10, #9, COALESCE(#11, 0.0) + COALESCE(#10, 0.0)) + ", + "COALESCE(#20 + #18, #17, COALESCE(#20, 0.0) + COALESCE(#18, 0.0))" ) ); let formula = graph .battery_formula(Some(BTreeSet::from([19, 21]))) .unwrap(); - assert_eq!(formula, "COALESCE(#20 + #18, #17)"); + assert_eq!( + formula, + "COALESCE(#20 + #18, #17, COALESCE(#20, 0.0) + COALESCE(#18, 0.0))" + ); let formula = graph.battery_formula(Some(BTreeSet::from([19]))).unwrap(); - assert_eq!(formula, "#18"); + assert_eq!(formula, "COALESCE(#18, 0.0)"); let formula = graph.battery_formula(Some(BTreeSet::from([21]))).unwrap(); - assert_eq!(formula, "#20"); + assert_eq!(formula, "COALESCE(#20, 0.0)"); let formula = graph .battery_formula(Some(BTreeSet::from([4, 12, 13, 19]))) .unwrap(); - assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#11 + #10, #9) + #18"); + assert_eq!( + formula, + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#11 + #10, #9, COALESCE(#11, 0.0) + COALESCE(#10, 0.0)) + ", + "COALESCE(#18, 0.0)" + ) + ); // Failure cases: let formula = graph.battery_formula(Some(BTreeSet::from([17]))); diff --git a/src/graph/formulas/generators/chp.rs b/src/graph/formulas/generators/chp.rs index 8ef52ac..26aeb40 100644 --- a/src/graph/formulas/generators/chp.rs +++ b/src/graph/formulas/generators/chp.rs @@ -85,7 +85,7 @@ mod tests { let graph = builder.build()?; let formula = graph.chp_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a battery meter with one inverter and two batteries. let meter_bat_chain = builder.meter_bat_chain(1, 2); @@ -95,7 +95,7 @@ mod tests { let graph = builder.build()?; let formula = graph.chp_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a chp meter with two CHPs. let meter_chp_chain = builder.meter_chp_chain(2); @@ -105,10 +105,16 @@ mod tests { let graph = builder.build()?; let formula = graph.chp_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#10 + #9, #8)"); + assert_eq!( + formula, + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0))" + ) + ); let formula = graph.chp_formula(Some(BTreeSet::from([10, 3]))).unwrap(); - assert_eq!(formula, "COALESCE(#3, #2) + #10"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#10, 0.0)"); // add a meter direct to the grid with three CHPs let meter_chp_chain = builder.meter_chp_chain(3); @@ -120,7 +126,15 @@ mod tests { let formula = graph.chp_formula(None)?; assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)", + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(", + "#14 + #13 + #12, ", + "#11, ", + "COALESCE(#14, 0.0) + COALESCE(#13, 0.0) + COALESCE(#12, 0.0)", + ")" + ), ); let formula = graph @@ -128,7 +142,11 @@ mod tests { .unwrap(); assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + #12 + #13" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(#12, 0.0) + COALESCE(#13, 0.0)" + ) ); let formula = graph @@ -136,11 +154,19 @@ mod tests { .unwrap(); assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(", + "#14 + #13 + #12, ", + "#11, ", + "COALESCE(#14, 0.0) + COALESCE(#13, 0.0) + COALESCE(#12, 0.0)", + ")" + ), ); let formula = graph.chp_formula(Some(BTreeSet::from([10, 14]))).unwrap(); - assert_eq!(formula, "#10 + #14"); + assert_eq!(formula, "COALESCE(#10, 0.0) + COALESCE(#14, 0.0)"); // Failure cases: let formula = graph.chp_formula(Some(BTreeSet::from([8]))); diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index 46a5dfa..3926467 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -165,7 +165,7 @@ mod tests { // battery inverter from the battery meter. assert_eq!( formula, - "MAX(0.0, #1 - COALESCE(#2, #3)) + COALESCE(MAX(0.0, #2 - #3), 0.0)" + "MAX(0.0, #1 - COALESCE(#2, #3, 0.0)) + COALESCE(MAX(0.0, #2 - #3), 0.0)" ); // Add a solar meter with two solar inverters to the grid meter. @@ -180,7 +180,10 @@ mod tests { formula, concat!( // difference of grid meter from all its suceessors - "MAX(0.0, #1 - COALESCE(#2, #3) - COALESCE(#5, #7 + #6)) + ", + "MAX(", + "0.0, ", + "#1 - COALESCE(#2, #3, 0.0) - COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0))", + ") + ", // difference of battery meter from battery inverter and pv // meter from the two pv inverters. "COALESCE(MAX(0.0, #2 - #3), 0.0) + COALESCE(MAX(0.0, #5 - #6 - #7), 0.0)", @@ -210,7 +213,11 @@ mod tests { concat!( // difference of grid meter from all its suceessors "MAX(0.0, ", - "#1 - COALESCE(#2, #3) - COALESCE(#5, #7 + #6) - COALESCE(#11, #10 + #9 + #8)) + ", + "#1 - ", + "COALESCE(#2, #3, 0.0) - ", + "COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)) - ", + "COALESCE(#11, COALESCE(#10, 0.0) + COALESCE(#9, 0.0) + COALESCE(#8, 0.0))", + ") + ", // difference of battery meter from battery inverter and pv // meter from the two pv inverters. "COALESCE(MAX(0.0, #2 - #3), 0.0) + COALESCE(MAX(0.0, #5 - #6 - #7), 0.0) + ", @@ -235,8 +242,11 @@ mod tests { concat!( // difference of grid meter from all its suceessors "MAX(0.0, ", - "#1 - COALESCE(#2, #3) - COALESCE(#5, #7 + #6) - COALESCE(#11, #10 + #9 + #8) - ", - "COALESCE(#12, #13)", + "#1 - ", + "COALESCE(#2, #3, 0.0) - ", + "COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)) - ", + "COALESCE(#11, COALESCE(#10, 0.0) + COALESCE(#9, 0.0) + COALESCE(#8, 0.0)) - ", + "COALESCE(#12, #13, 0.0)", ") + ", // difference of battery meter from battery inverter and pv // meter from the two pv inverters. @@ -376,7 +386,7 @@ mod tests { formula, concat!( // difference of pv powers from first two grid meters - "MAX(0.0, #1 + #2 - COALESCE(#4, #5) - COALESCE(#6, #7)) + ", + "MAX(0.0, #1 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0)) + ", // third grid meter still dangling "MAX(0.0, #3) + ", // difference of solar inverters from their meters @@ -399,7 +409,7 @@ mod tests { formula, concat!( // difference of pv powers from first two grid meters and meter#8 - "MAX(0.0, #1 + #8 + #2 - COALESCE(#4, #5) - COALESCE(#6, #7)) + ", + "MAX(0.0, #1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0)) + ", // difference of meter#8 from third grid meter "MAX(0.0, #3 - #8) + ", // difference of solar inverters from their meters @@ -419,7 +429,7 @@ mod tests { // difference of pv and battery powers from first two grid // meters and meter#8 "MAX(0.0, ", - "#1 + #8 + #2 - COALESCE(#4, #5) - COALESCE(#6, #7) - COALESCE(#9, #10)", + "#1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0) - COALESCE(#9, #10, 0.0)", ") + ", // difference of meter#8 from third grid meter "MAX(0.0, #3 - #8) + ", diff --git a/src/graph/formulas/generators/ev_charger.rs b/src/graph/formulas/generators/ev_charger.rs index b15dc7d..bf3678f 100644 --- a/src/graph/formulas/generators/ev_charger.rs +++ b/src/graph/formulas/generators/ev_charger.rs @@ -88,7 +88,7 @@ mod tests { let graph = builder.build()?; let formula = graph.ev_charger_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a battery meter with one inverter and two batteries. let meter_bat_chain = builder.meter_bat_chain(1, 2); @@ -98,7 +98,7 @@ mod tests { let graph = builder.build()?; let formula = graph.ev_charger_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a EV charger meter with two EV chargers. let meter_ev_charger_chain = builder.meter_ev_charger_chain(2); @@ -108,12 +108,18 @@ mod tests { let graph = builder.build()?; let formula = graph.ev_charger_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#10 + #9, #8)"); + assert_eq!( + formula, + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0))" + ) + ); let formula = graph .ev_charger_formula(Some(BTreeSet::from([10, 3]))) .unwrap(); - assert_eq!(formula, "COALESCE(#3, #2) + #10"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#10, 0.0)"); // add a meter direct to the grid with three EV chargers let meter_ev_charger_chain = builder.meter_ev_charger_chain(3); @@ -125,7 +131,15 @@ mod tests { let formula = graph.ev_charger_formula(None)?; assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)", + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(", + "#14 + #13 + #12, ", + "#11, ", + "COALESCE(#14, 0.0) + COALESCE(#13, 0.0) + COALESCE(#12, 0.0)", + ")" + ), ); let formula = graph @@ -133,7 +147,12 @@ mod tests { .unwrap(); assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + #12 + #13" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(#12, 0.0) + ", + "COALESCE(#13, 0.0)" + ) ); let formula = graph @@ -141,13 +160,21 @@ mod tests { .unwrap(); assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(", + "#14 + #13 + #12, ", + "#11, ", + "COALESCE(#14, 0.0) + COALESCE(#13, 0.0) + COALESCE(#12, 0.0)", + ")" + ) ); let formula = graph .ev_charger_formula(Some(BTreeSet::from([10, 14]))) .unwrap(); - assert_eq!(formula, "#10 + #14"); + assert_eq!(formula, "COALESCE(#10, 0.0) + COALESCE(#14, 0.0)"); // Failure cases: let formula = graph.ev_charger_formula(Some(BTreeSet::from([8]))); diff --git a/src/graph/formulas/generators/grid.rs b/src/graph/formulas/generators/grid.rs index b8ddedb..803e07d 100644 --- a/src/graph/formulas/generators/grid.rs +++ b/src/graph/formulas/generators/grid.rs @@ -77,7 +77,10 @@ mod tests { let graph = builder.build()?; let formula = graph.grid_formula()?; - assert_eq!(formula, "#1 + #5 + COALESCE(#6, #7) + COALESCE(#9, #10)"); + assert_eq!( + formula, + "#1 + #5 + COALESCE(#6, #7, 0.0) + COALESCE(#9, #10, 0.0)" + ); // Add a PV inverter to the grid, without a meter. let pv_inverter = builder.solar_inverter(); @@ -89,7 +92,7 @@ mod tests { let formula = graph.grid_formula()?; assert_eq!( formula, - "#1 + #5 + COALESCE(#6, #7) + COALESCE(#9, #10) + #11" + "#1 + #5 + COALESCE(#6, #7, 0.0) + COALESCE(#9, #10, 0.0) + COALESCE(#11, 0.0)" ); Ok(()) diff --git a/src/graph/formulas/generators/producer.rs b/src/graph/formulas/generators/producer.rs index a2494bd..be37ebb 100644 --- a/src/graph/formulas/generators/producer.rs +++ b/src/graph/formulas/generators/producer.rs @@ -85,7 +85,10 @@ mod tests { let graph = builder.build()?; let formula = graph.producer_formula()?; - assert_eq!(formula, "MIN(0.0, COALESCE(#4 + #3, #2))"); + assert_eq!( + formula, + "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)))" + ); // Add a CHP meter to the grid with a CHP behind it. let meter_chp_chain = builder.meter_chp_chain(1); @@ -95,7 +98,10 @@ mod tests { let formula = graph.producer_formula()?; assert_eq!( formula, - "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5))" + concat!( + "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0))) + ", + "MIN(0.0, COALESCE(#6, #5, 0.0))" + ) ); // Add a CHP to the grid, without a meter. @@ -106,7 +112,11 @@ mod tests { let formula = graph.producer_formula()?; assert_eq!( formula, - "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + MIN(0.0, #7)" + concat!( + "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0))) + ", + "MIN(0.0, COALESCE(#6, #5, 0.0)) + ", + "MIN(0.0, COALESCE(#7, 0.0))" + ) ); // Add a PV inverter to the grid_meter. @@ -118,8 +128,10 @@ mod tests { assert_eq!( formula, concat!( - "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + ", - "MIN(0.0, #7) + MIN(0.0, #8)" + "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0))) + ", + "MIN(0.0, COALESCE(#6, #5, 0.0)) + ", + "MIN(0.0, COALESCE(#7, 0.0)) + ", + "MIN(0.0, COALESCE(#8, 0.0))" ) ); @@ -132,8 +144,10 @@ mod tests { assert_eq!( formula, concat!( - "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + ", - "MIN(0.0, #7) + MIN(0.0, #8)" + "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0))) + ", + "MIN(0.0, COALESCE(#6, #5, 0.0)) + ", + "MIN(0.0, COALESCE(#7, 0.0)) + ", + "MIN(0.0, COALESCE(#8, 0.0))" ) ); @@ -150,8 +164,12 @@ mod tests { assert_eq!( formula, concat!( - "MIN(0.0, COALESCE(#4 + #3, #2)) + MIN(0.0, COALESCE(#6, #5)) + ", - "MIN(0.0, #7) + MIN(0.0, #8) + MIN(0.0, #13) + MIN(0.0, #14)" + "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0))) + ", + "MIN(0.0, COALESCE(#6, #5, 0.0)) + ", + "MIN(0.0, COALESCE(#7, 0.0)) + ", + "MIN(0.0, COALESCE(#8, 0.0)) + ", + "MIN(0.0, COALESCE(#13, 0.0)) + ", + "MIN(0.0, COALESCE(#14, 0.0))" ) ); diff --git a/src/graph/formulas/generators/pv.rs b/src/graph/formulas/generators/pv.rs index 97cf7ef..4bb387e 100644 --- a/src/graph/formulas/generators/pv.rs +++ b/src/graph/formulas/generators/pv.rs @@ -88,7 +88,7 @@ mod tests { let graph = builder.build()?; let formula = graph.pv_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a battery meter with one inverter and two batteries. let meter_bat_chain = builder.meter_bat_chain(1, 2); @@ -98,7 +98,7 @@ mod tests { let graph = builder.build()?; let formula = graph.pv_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2)"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a PV meter with two PV inverters. let meter_pv_chain = builder.meter_pv_chain(2); @@ -108,10 +108,16 @@ mod tests { let graph = builder.build()?; let formula = graph.pv_formula(None)?; - assert_eq!(formula, "COALESCE(#3, #2) + COALESCE(#10 + #9, #8)"); + assert_eq!( + formula, + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0))" + ) + ); let formula = graph.pv_formula(Some(BTreeSet::from([10, 3]))).unwrap(); - assert_eq!(formula, "COALESCE(#3, #2) + #10"); + assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#10, 0.0)"); // add a meter direct to the grid with three PV inverters let meter_pv_chain = builder.meter_pv_chain(3); @@ -123,7 +129,15 @@ mod tests { let formula = graph.pv_formula(None)?; assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)", + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(", + "#14 + #13 + #12, ", + "#11, ", + "COALESCE(#14, 0.0) + COALESCE(#13, 0.0) + COALESCE(#12, 0.0)", + ")" + ), ); let formula = graph @@ -131,7 +145,12 @@ mod tests { .unwrap(); assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + #12 + #13" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(#12, 0.0) + ", + "COALESCE(#13, 0.0)" + ) ); let formula = graph @@ -139,11 +158,19 @@ mod tests { .unwrap(); assert_eq!( formula, - "COALESCE(#3, #2) + COALESCE(#10 + #9, #8) + COALESCE(#14 + #13 + #12, #11)" + concat!( + "COALESCE(#3, #2, 0.0) + ", + "COALESCE(#10 + #9, #8, COALESCE(#10, 0.0) + COALESCE(#9, 0.0)) + ", + "COALESCE(", + "#14 + #13 + #12, ", + "#11, ", + "COALESCE(#14, 0.0) + COALESCE(#13, 0.0) + COALESCE(#12, 0.0)", + ")" + ) ); let formula = graph.pv_formula(Some(BTreeSet::from([10, 14]))).unwrap(); - assert_eq!(formula, "#10 + #14"); + assert_eq!(formula, "COALESCE(#10, 0.0) + COALESCE(#14, 0.0)"); // Failure cases: let formula = graph.pv_formula(Some(BTreeSet::from([8]))); From 6f438285a69e35eec15d1b81d91e785a46305cff Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 1 Nov 2024 17:04:09 +0100 Subject: [PATCH 18/19] Remove `Node::is_supported` trait method All components now have a fallback to 0.0, so this check is no longer necessary. Signed-off-by: Sahas Subramanian --- src/graph/formulas/fallback.rs | 7 +------ src/graph/test_utils.rs | 4 ---- src/graph_traits.rs | 6 ------ 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs index 1b679cc..a0c184d 100644 --- a/src/graph/formulas/fallback.rs +++ b/src/graph/formulas/fallback.rs @@ -65,12 +65,7 @@ where return Ok(None); } - if !self.graph.has_successors(component_id)? - || !self - .graph - .successors(component_id)? - .all(|x| x.is_supported()) - { + if !self.graph.has_successors(component_id)? { return Ok(Some(Expr::component(component_id))); } diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs index c5cacb0..24f0d77 100644 --- a/src/graph/test_utils.rs +++ b/src/graph/test_utils.rs @@ -30,10 +30,6 @@ impl Node for TestComponent { fn category(&self) -> ComponentCategory { self.1.clone() } - - fn is_supported(&self) -> bool { - true - } } #[derive(Clone, Debug, PartialEq)] diff --git a/src/graph_traits.rs b/src/graph_traits.rs index 1e92c7a..12baa7e 100644 --- a/src/graph_traits.rs +++ b/src/graph_traits.rs @@ -116,10 +116,6 @@ impl frequenz_microgrid_component_graph::Node for common::v1::microgrid::compone pb::ComponentCategory::Hvac => gr::ComponentCategory::Hvac, } } - - fn is_supported(&self) -> bool { - self.status != common::v1::microgrid::components::ComponentStatus::Inactive as i32 - } } ``` @@ -130,8 +126,6 @@ pub trait Node { fn component_id(&self) -> u64; /// Returns the category of the category. fn category(&self) -> ComponentCategory; - /// Returns true if the component can be read from and/or controlled. - fn is_supported(&self) -> bool; } /** From 75d067e578fd14870e2755d3ff5113c8ee6ed8b5 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 6 Jan 2025 11:16:06 +0100 Subject: [PATCH 19/19] Add a direction parameter to the `find_all` method Earlier the direction was hard coded, which made it unclear that the method was only finding outgoing nodes. Signed-off-by: Sahas Subramanian --- src/graph/formulas/generators/battery.rs | 7 ++++- src/graph/formulas/generators/chp.rs | 7 ++++- src/graph/formulas/generators/consumer.rs | 7 ++++- src/graph/formulas/generators/ev_charger.rs | 7 ++++- src/graph/formulas/generators/producer.rs | 1 + src/graph/formulas/generators/pv.rs | 7 ++++- src/graph/retrieval.rs | 33 ++++++++++++++++----- 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs index c778658..eddf836 100644 --- a/src/graph/formulas/generators/battery.rs +++ b/src/graph/formulas/generators/battery.rs @@ -29,7 +29,12 @@ where let inverter_ids = if let Some(battery_ids) = battery_ids { Self::find_inverter_ids(graph, &battery_ids)? } else { - graph.find_all(graph.root_id, |node| node.is_battery_inverter(), false)? + graph.find_all( + graph.root_id, + |node| node.is_battery_inverter(), + petgraph::Direction::Outgoing, + false, + )? }; Ok(Self { graph, diff --git a/src/graph/formulas/generators/chp.rs b/src/graph/formulas/generators/chp.rs index 26aeb40..098468f 100644 --- a/src/graph/formulas/generators/chp.rs +++ b/src/graph/formulas/generators/chp.rs @@ -29,7 +29,12 @@ where let chp_ids = if let Some(chp_ids) = chp_ids { chp_ids } else { - graph.find_all(graph.root_id, |node| node.is_chp(), false)? + graph.find_all( + graph.root_id, + |node| node.is_chp(), + petgraph::Direction::Outgoing, + false, + )? }; Ok(Self { graph, chp_ids }) } diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index 3926467..e441cf7 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -24,7 +24,12 @@ where { pub fn try_new(graph: &'a ComponentGraph) -> Result { Ok(Self { - unvisited_meters: graph.find_all(graph.root_id, |node| node.is_meter(), true)?, + unvisited_meters: graph.find_all( + graph.root_id, + |node| node.is_meter(), + petgraph::Direction::Outgoing, + true, + )?, graph, }) } diff --git a/src/graph/formulas/generators/ev_charger.rs b/src/graph/formulas/generators/ev_charger.rs index bf3678f..9c0db26 100644 --- a/src/graph/formulas/generators/ev_charger.rs +++ b/src/graph/formulas/generators/ev_charger.rs @@ -29,7 +29,12 @@ where let ev_charger_ids = if let Some(ev_charger_ids) = ev_charger_ids { ev_charger_ids } else { - graph.find_all(graph.root_id, |node| node.is_ev_charger(), false)? + graph.find_all( + graph.root_id, + |node| node.is_ev_charger(), + petgraph::Direction::Outgoing, + false, + )? }; Ok(Self { graph, diff --git a/src/graph/formulas/generators/producer.rs b/src/graph/formulas/generators/producer.rs index be37ebb..5f13379 100644 --- a/src/graph/formulas/generators/producer.rs +++ b/src/graph/formulas/generators/producer.rs @@ -41,6 +41,7 @@ where || node.is_pv_inverter() || node.is_chp() }, + petgraph::Direction::Outgoing, false, )? { let comp_expr = Self::min_zero(self.graph.fallback_expr([component_id], false)?); diff --git a/src/graph/formulas/generators/pv.rs b/src/graph/formulas/generators/pv.rs index 4bb387e..ec5e5ab 100644 --- a/src/graph/formulas/generators/pv.rs +++ b/src/graph/formulas/generators/pv.rs @@ -29,7 +29,12 @@ where let pv_inverter_ids = if let Some(pv_inverter_ids) = pv_inverter_ids { pv_inverter_ids } else { - graph.find_all(graph.root_id, |node| node.is_pv_inverter(), false)? + graph.find_all( + graph.root_id, + |node| node.is_pv_inverter(), + petgraph::Direction::Outgoing, + false, + )? }; Ok(Self { graph, diff --git a/src/graph/retrieval.rs b/src/graph/retrieval.rs index 3609644..d7d8d3a 100644 --- a/src/graph/retrieval.rs +++ b/src/graph/retrieval.rs @@ -108,7 +108,7 @@ where } /// Returns a set of all components that match the given predicate, starting - /// from the component with the given `component_id`. + /// from the component with the given `component_id`, in the given direction. /// /// If `follow_after_match` is `true`, the search continues deeper beyond /// the matching components. @@ -116,6 +116,7 @@ where &self, from: u64, mut pred: impl FnMut(&N) -> bool, + direction: petgraph::Direction, follow_after_match: bool, ) -> Result, Error> { let index = self.node_indices.get(&from).ok_or_else(|| { @@ -133,9 +134,7 @@ where } } - let neighbors = self - .graph - .neighbors_directed(index, petgraph::Direction::Outgoing); + let neighbors = self.graph.neighbors_directed(index, direction); stack.extend(neighbors); } @@ -353,15 +352,26 @@ mod tests { let (components, connections) = nodes_and_edges(); let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; - let found = graph.find_all(graph.root_id, |x| x.is_meter(), false)?; + let found = graph.find_all( + graph.root_id, + |x| x.is_meter(), + petgraph::Direction::Outgoing, + false, + )?; assert_eq!(found, [2].iter().cloned().collect()); - let found = graph.find_all(graph.root_id, |x| x.is_meter(), true)?; + let found = graph.find_all( + graph.root_id, + |x| x.is_meter(), + petgraph::Direction::Outgoing, + true, + )?; assert_eq!(found, [2, 3, 6].iter().cloned().collect()); let found = graph.find_all( graph.root_id, |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false), + petgraph::Direction::Outgoing, true, )?; assert_eq!(found, [2, 4, 5, 7, 8].iter().cloned().collect()); @@ -369,6 +379,7 @@ mod tests { let found = graph.find_all( 6, |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false), + petgraph::Direction::Outgoing, true, )?; assert_eq!(found, [7, 8].iter().cloned().collect()); @@ -376,14 +387,20 @@ mod tests { let found = graph.find_all( graph.root_id, |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false), + petgraph::Direction::Outgoing, false, )?; assert_eq!(found, [2].iter().cloned().collect()); - let found = graph.find_all(graph.root_id, |_| true, false)?; + let found = graph.find_all( + graph.root_id, + |_| true, + petgraph::Direction::Outgoing, + false, + )?; assert_eq!(found, [1].iter().cloned().collect()); - let found = graph.find_all(3, |_| true, true)?; + let found = graph.find_all(3, |_| true, petgraph::Direction::Outgoing, true)?; assert_eq!(found, [3, 4, 5].iter().cloned().collect()); Ok(())