diff --git a/src/error.rs b/src/error.rs index 0197714..a8a2d6e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,8 +52,7 @@ ErrorKind!( (Internal, internal), (InvalidComponent, invalid_component), (InvalidConnection, invalid_connection), - (InvalidGraph, invalid_graph), - (MissingParameters, missing_parameters) + (InvalidGraph, invalid_graph) ); /// An error that can occur during the creation or traversal of a diff --git a/src/graph.rs b/src/graph.rs index 794a0bf..d246175 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -13,7 +13,7 @@ mod formulas; pub mod iterators; use crate::{ComponentGraphConfig, Edge, Node}; -pub use formulas::{AggregationFormula, CoalesceFormula}; +pub use formulas::{AggregationFormula, CoalesceFormula, Formula}; use petgraph::graph::{DiGraph, NodeIndex}; use std::collections::HashMap; diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index 0951f32..98a5298 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -16,7 +16,8 @@ mod formula; mod generators; mod traversal; -pub use formula::{AggregationFormula, CoalesceFormula}; +use expr::Expr; +pub use formula::{AggregationFormula, CoalesceFormula, Formula}; /// Formulas for various microgrid metrics. impl ComponentGraph @@ -63,15 +64,7 @@ where generators::pv::PVFormulaBuilder::try_new(self, pv_inverter_ids)?.build() } - /// Returns the coalesce formula for the given component IDs. - /// - /// This formula uses the `COALESCE` function to return the first non-null - /// value from the components with the provided IDs. - pub fn coalesce(&self, component_ids: BTreeSet) -> Result { - generators::generic::CoalesceFormulaBuilder::try_new(self, component_ids)?.build() - } - - /// Returns a string representing the EV charger formula for the graph. + /// Returns the EV charger formula for the graph. pub fn ev_charger_formula( &self, ev_charger_ids: Option>, @@ -79,6 +72,11 @@ where generators::ev_charger::EVChargerFormulaBuilder::try_new(self, ev_charger_ids)?.build() } + /// Returns the formula for a specific component by its ID. + pub fn component_formula(&self, component_id: u64) -> Result { + Ok(Expr::component(component_id).into()) + } + /// Returns the grid coalesce formula for the graph. /// /// This formula is used for non-aggregating metrics like AC voltage or @@ -129,4 +127,12 @@ where generators::pv_ac_coalesce::PVAcCoalesceFormulaBuilder::try_new(self, pv_inverter_ids)? .build() } + + /// Returns the AC coalesce formula for a specific component by its ID. + pub fn component_ac_coalesce_formula( + &self, + component_id: u64, + ) -> Result { + Ok(Expr::component(component_id).into()) + } } diff --git a/src/graph/formulas/expr.rs b/src/graph/formulas/expr.rs index 191b1e6..e702133 100644 --- a/src/graph/formulas/expr.rs +++ b/src/graph/formulas/expr.rs @@ -5,6 +5,9 @@ use crate::Node; #[derive(Debug, Clone, PartialEq)] pub(crate) enum Expr { + /// An empty expression, which as a formula would evaluate to None. + None, + /// A negation of an expression. Neg { param: Box }, @@ -35,6 +38,7 @@ impl std::ops::Add for Expr { fn add(self, rhs: Self) -> Self { match (self, rhs) { + (Self::None, other) | (other, Self::None) => other, // -a + -b = -(a + b) (Self::Neg { param: lhs }, Self::Neg { param: rhs }) => -(*lhs + *rhs), // -a + b = b - a @@ -68,6 +72,8 @@ impl std::ops::Sub for Expr { fn sub(self, rhs: Self) -> Self { match (self, rhs) { + (Self::None, other) => -other, + (other, Self::None) => other, // (a - b) - -c = a - b + c (sub @ Self::Sub { .. }, Self::Neg { param }) => sub + *param, // -a - (b - c) = c - b - a @@ -98,6 +104,7 @@ impl std::ops::Neg for Expr { fn neg(self) -> Self { match self { + Self::None => Self::None, // -(-a) = a Expr::Neg { param: inner } => *inner, // -(a - b) = b - a @@ -124,28 +131,113 @@ impl From<&N> for Expr { /// Constructors for `FormulaExpression`. impl Expr { + #[must_use] pub(crate) fn number(value: f64) -> Self { Self::Number { value } } + #[must_use] pub(crate) fn component(component_id: u64) -> Self { Self::Component { component_id } } - pub(crate) fn coalesce(params: Vec) -> Self { - if let [param] = params.as_slice() { - param.clone() - } else { - Self::Coalesce { params } + #[must_use] + pub(crate) fn coalesce(self, other: Expr) -> Self { + match (self, other) { + (Expr::None, other) | (other, Expr::None) => other, + ( + Expr::Coalesce { mut params }, + Expr::Coalesce { + params: other_params, + }, + ) => { + // If both parameters are coalesce expressions, merge them. + params.extend(other_params); + Self::Coalesce { params } + } + (Expr::Coalesce { mut params }, other) => { + // If the first parameter is a coalesce expression, add the second + // parameter to it. + params.push(other); + Self::Coalesce { params } + } + ( + param, + Expr::Coalesce { + params: other_params, + }, + ) => { + // If the second parameter is a coalesce expression, add the first + // parameter to it. + let mut params = vec![param]; + params.extend(other_params); + Self::Coalesce { params } + } + (first, second) => { + // If neither parameter is a coalesce expression, create a new one. + Self::Coalesce { + params: vec![first, second], + } + } } } - pub(crate) fn min(params: Vec) -> Self { - Self::Min { params } + #[must_use] + pub(crate) fn min(self, other: Expr) -> Self { + match (self, other) { + (Expr::None, expr) | (expr, Expr::None) => expr, + ( + Expr::Min { mut params }, + Expr::Min { + params: other_params, + }, + ) => { + // If both parameters are min expressions, merge them. + params.extend(other_params); + Self::Min { params } + } + (Expr::Min { mut params }, other) | (other, Expr::Min { mut params }) => { + // If one parameter is a min expression, add the other parameter + // to it. + params.push(other); + Self::Min { params } + } + (first, second) => { + // If neither parameter is a min expression, create a new one. + Self::Min { + params: vec![first, second], + } + } + } } - pub(crate) fn max(params: Vec) -> Self { - Self::Max { params } + #[must_use] + pub(crate) fn max(self, other: Expr) -> Self { + match (self, other) { + (Expr::None, expr) | (expr, Expr::None) => expr, + ( + Expr::Max { mut params }, + Expr::Max { + params: other_params, + }, + ) => { + // If both parameters are max expressions, merge them. + params.extend(other_params); + Self::Max { params } + } + (Expr::Max { mut params }, other) | (other, Expr::Max { mut params }) => { + // If one parameter is a max expression, add the other parameter + // to it. + params.push(other); + Self::Max { params } + } + (first, second) => { + // If neither parameter is a max expression, create a new one. + Self::Max { + params: vec![first, second], + } + } + } } } @@ -178,6 +270,7 @@ impl Expr { /// component, the whole expression is enclosed in brackets. fn generate_string(&self, bracket_whole: bool) -> String { match self { + Self::None => String::from("None"), Self::Neg { param } => format!("-{}", param.generate_string(true)), Self::Number { value } => { if value.fract() == 0.0 { @@ -365,16 +458,15 @@ mod tests { 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)]), - ])], + &[ + comp(1) - (coalesce(comp(5), comp(7) + comp(6)) + coalesce(comp(2), comp(3))) + + coalesce( + number(0.0).max(comp(5)), + number(0.0).max(comp(7)) + number(0.0).max(comp(6)), + ), + ], concat!( "#1 - (COALESCE(#5, #7 + #6) + COALESCE(#2, #3)) + ", "COALESCE(MAX(0.0, #5), MAX(0.0, #7) + MAX(0.0, #6))" @@ -382,12 +474,10 @@ mod tests { ); 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), - ])], + &[number(0.0).min(comp(5)).min(comp(7) + comp(6)) + - coalesce(comp(5), comp(7) + comp(6)) + .max(comp(7)) + .max(number(22.44))], "MIN(0.0, #5, #7 + #6) - MAX(COALESCE(#5, #7 + #6), #7, 22.44)", ) } diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs index f7c1c11..4db70e9 100644 --- a/src/graph/formulas/fallback.rs +++ b/src/graph/formulas/fallback.rs @@ -81,7 +81,7 @@ where .map(|node| { ( Expr::from(node), - Expr::coalesce(vec![Expr::from(node), Expr::number(0.0)]), + Expr::coalesce(Expr::from(node), Expr::number(0.0)), ) }) .reduce(|a, b| (a.0 + b.0, a.1 + b.1)) @@ -91,27 +91,26 @@ where let has_multiple_successors = matches!(sum_of_successors, Expr::Add { .. }); - let mut to_be_coalesced: Vec = vec![]; + let mut coalesced = Expr::component(component_id); if !self.prefer_meters { - to_be_coalesced.push(sum_of_successors.clone()); + coalesced = sum_of_successors.clone().coalesce(coalesced); } - to_be_coalesced.push(Expr::component(component_id)); if self.prefer_meters { if has_multiple_successors { - to_be_coalesced.push(sum_of_coalesced_successors); + coalesced = coalesced.coalesce(sum_of_coalesced_successors); } else { - to_be_coalesced.push(sum_of_successors); - to_be_coalesced.push(Expr::number(0.0)); + coalesced = coalesced.coalesce(sum_of_successors); + coalesced = coalesced.coalesce(Expr::number(0.0)); } } else if has_multiple_successors { - to_be_coalesced.push(sum_of_coalesced_successors); + coalesced = coalesced.coalesce(sum_of_coalesced_successors); } else { - to_be_coalesced.push(Expr::number(0.0)); + coalesced = coalesced.coalesce(Expr::number(0.0)); } - Ok(Some(Expr::coalesce(to_be_coalesced))) + Ok(Some(coalesced)) } /// Returns a fallback expression for components with the following categories: @@ -145,10 +144,10 @@ where .iter() .all(|sibling| component_ids.contains(&sibling.component_id())) { - return Ok(Some(Expr::coalesce(vec![ + return Ok(Some(Expr::coalesce( Expr::component(component_id), Expr::number(0.0), - ]))); + ))); } for sibling in siblings { diff --git a/src/graph/formulas/formula.rs b/src/graph/formulas/formula.rs index 40f6528..8154c93 100644 --- a/src/graph/formulas/formula.rs +++ b/src/graph/formulas/formula.rs @@ -5,6 +5,33 @@ use super::expr::Expr; +/// A trait with methods for combining formulas. +pub trait Formula { + fn coalesce(self, other: Self) -> Self; + fn min(self, other: Self) -> Self; + fn max(self, other: Self) -> Self; +} + +/// Blanket implementation of the `Formula` trait for any type that can be +/// converted to and from `Expr`. +impl Formula for T +where + T: From, + Expr: From, +{ + fn coalesce(self, other: Self) -> Self { + Expr::coalesce(self.into(), other.into()).into() + } + + fn min(self, other: Self) -> Self { + Expr::min(self.into(), other.into()).into() + } + + fn max(self, other: Self) -> Self { + Expr::max(self.into(), other.into()).into() + } +} + // This struct represents the microgrid metric formulas that are generated by // traversing the component graph. // @@ -19,6 +46,18 @@ pub struct AggregationFormula { expr: Expr, } +impl From for AggregationFormula { + fn from(expr: Expr) -> Self { + AggregationFormula { expr } + } +} + +impl From for Expr { + fn from(formula: AggregationFormula) -> Self { + formula.expr + } +} + impl AggregationFormula { pub(crate) fn new(expr: Expr) -> Self { AggregationFormula { expr } @@ -66,6 +105,18 @@ pub struct CoalesceFormula { expr: Expr, } +impl From for CoalesceFormula { + fn from(expr: Expr) -> Self { + CoalesceFormula { expr } + } +} + +impl From for Expr { + fn from(formula: CoalesceFormula) -> Self { + formula.expr + } +} + impl CoalesceFormula { pub(crate) fn new(expr: Expr) -> Self { CoalesceFormula { expr } @@ -83,3 +134,42 @@ impl From for String { formula.expr.to_string() } } + +#[test] +fn test_aggregation_formula_arith() { + let formula1 = AggregationFormula::new(Expr::component(1)); + let formula2 = AggregationFormula::new(Expr::component(2)); + + let result = formula1.clone() + formula2.clone(); + assert_eq!(result.to_string(), "#1 + #2"); + + let result = formula1 - formula2; + assert_eq!(result.to_string(), "#1 - #2"); +} + +#[test] +fn test_formula_trait() { + let formula1 = AggregationFormula::new(Expr::component(1)); + let formula2 = AggregationFormula::new(Expr::component(2)); + + let coalesce_result = formula1.clone().coalesce(formula2.clone()); + assert_eq!(coalesce_result.to_string(), "COALESCE(#1, #2)"); + + let min_result = formula1.clone().min(formula2.clone()); + assert_eq!(min_result.to_string(), "MIN(#1, #2)"); + + let max_result = formula1.max(formula2); + assert_eq!(max_result.to_string(), "MAX(#1, #2)"); + + let formula3 = CoalesceFormula::new(Expr::component(3)); + let formula4 = CoalesceFormula::new(Expr::component(4)); + + let coalesce_result = formula3.clone().coalesce(formula4.clone()); + assert_eq!(coalesce_result.to_string(), "COALESCE(#3, #4)"); + + let min_result = formula3.clone().min(formula4.clone()); + assert_eq!(min_result.to_string(), "MIN(#3, #4)"); + + let max_result = formula3.max(formula4); + assert_eq!(max_result.to_string(), "MAX(#3, #4)"); +} diff --git a/src/graph/formulas/generators.rs b/src/graph/formulas/generators.rs index e586b43..0496629 100644 --- a/src/graph/formulas/generators.rs +++ b/src/graph/formulas/generators.rs @@ -8,7 +8,6 @@ pub(super) mod battery_ac_coalesce; pub(super) mod chp; pub(super) mod consumer; pub(super) mod ev_charger; -pub(super) mod generic; pub(super) mod grid; pub(super) mod grid_coalesce; pub(super) mod producer; diff --git a/src/graph/formulas/generators/battery_ac_coalesce.rs b/src/graph/formulas/generators/battery_ac_coalesce.rs index bf83e25..ce6b76b 100644 --- a/src/graph/formulas/generators/battery_ac_coalesce.rs +++ b/src/graph/formulas/generators/battery_ac_coalesce.rs @@ -59,7 +59,6 @@ where /// battery meters and inverters in the graph. pub fn build(self) -> Result { let mut meters: BTreeSet = BTreeSet::new(); - let mut source_components: Vec = vec![]; for inv_id in &self.inverter_ids { for pred in self.graph.predecessors(*inv_id)? { @@ -68,18 +67,14 @@ where } } } - source_components.extend( - meters - .iter() - .chain(self.inverter_ids.iter()) - .map(|component_id: &u64| Expr::component(*component_id)), - ); - - if source_components.is_empty() { - return Err(Error::component_not_found("No battery inverters found.")); - } - - Ok(CoalesceFormula::new(Expr::coalesce(source_components))) + let coalesced = meters + .into_iter() + .chain(self.inverter_ids) + .fold(Expr::None, |expr, component_id: u64| { + expr.coalesce(Expr::component(component_id)) + }); + + Ok(CoalesceFormula::new(coalesced)) } } @@ -100,11 +95,8 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.battery_ac_coalesce_formula(None); - assert_eq!( - formula, - Err(Error::component_not_found("No battery inverters found.")) - ); + let formula = graph.battery_ac_coalesce_formula(None)?.to_string(); + assert_eq!(formula, "None"); // Add a battery meter with one inverter and one battery. let meter_bat_chain = builder.meter_bat_chain(1, 1); diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index 21291d7..90e6541 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -69,14 +69,6 @@ where } } - /// 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() { @@ -110,18 +102,18 @@ where expr = expr - successor_expr; } - expr = Self::max_zero(expr); + expr = expr.max(Expr::number(0.0)); // 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)]); + expr = expr.coalesce(Expr::number(0.0)); } Ok(expr) } else { - Ok(Self::max_zero(component.into())) + Ok(Expr::from(component).max(Expr::number(0.0))) } } } @@ -158,7 +150,7 @@ mod tests { let graph = builder.build(None)?; let formula = graph.consumer_formula()?.to_string(); - assert_eq!(formula, "MAX(0.0, #1)"); + assert_eq!(formula, "MAX(#1, 0.0)"); // Add a battery meter with one battery inverter and one battery to the // grid meter. @@ -173,7 +165,7 @@ mod tests { // battery inverter from the battery meter. assert_eq!( formula, - "MAX(0.0, #1 - COALESCE(#2, #3, 0.0)) + COALESCE(MAX(0.0, #2 - #3), 0.0)" + "MAX(#1 - COALESCE(#2, #3, 0.0), 0.0) + COALESCE(MAX(#2 - #3, 0.0), 0.0)" ); // Add a solar meter with two solar inverters to the grid meter. @@ -189,12 +181,12 @@ mod tests { concat!( // difference of grid meter from all its suceessors "MAX(", - "0.0, ", - "#1 - COALESCE(#2, #3, 0.0) - COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0))", + "#1 - COALESCE(#2, #3, 0.0) - COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)), ", + "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)", + "COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0)", ) ); @@ -220,17 +212,17 @@ mod tests { formula, concat!( // difference of grid meter from all its suceessors - "MAX(0.0, ", + "MAX(", "#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(#11, COALESCE(#10, 0.0) + COALESCE(#9, 0.0) + COALESCE(#8, 0.0)), ", + "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) + ", + "COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0) + ", // difference of "mixed" meter from its successors. - "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0)" + "COALESCE(MAX(#11 - #8 - #9 - #10, 0.0), 0.0)" ) ); let graph = builder.build(Some(ComponentGraphConfig { @@ -242,12 +234,12 @@ mod tests { formula, concat!( // difference of grid meter from all its suceessors (without fallbacks) - "MAX(0.0, #1 - #2 - #5 - #11) + ", + "MAX(#1 - #2 - #5 - #11, 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) + ", + "COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0) + ", // difference of "mixed" meter from its successors. - "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0)" + "COALESCE(MAX(#11 - #8 - #9 - #10, 0.0), 0.0)" ) ); @@ -266,22 +258,22 @@ mod tests { formula, concat!( // difference of grid meter from all its suceessors - "MAX(0.0, ", + "MAX(", "#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)", - ") + ", + "COALESCE(#12, #13, 0.0), ", + "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) + ", + "COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0) + ", // difference of "mixed" meter from its successors. - "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0) + ", + "COALESCE(MAX(#11 - #8 - #9 - #10, 0.0), 0.0) + ", // difference of second battery meter from inverter. - "COALESCE(MAX(0.0, #12 - #13), 0.0) + ", + "COALESCE(MAX(#12 - #13, 0.0), 0.0) + ", // consumption component of the dangling meter. - "MAX(0.0, #15)" + "MAX(#15, 0.0)" ) ); @@ -303,7 +295,7 @@ mod tests { let formula = graph.consumer_formula()?.to_string(); // 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)"); + assert_eq!(formula, "COALESCE(MAX(#1 - #2, 0.0), 0.0)"); // Add a pv meter with one solar inverter and two dangling meter. let meter_pv_chain = builder.meter_pv_chain(1); @@ -323,9 +315,9 @@ mod tests { formula, concat!( // subtract meter successors from meters - "COALESCE(MAX(0.0, #1 - #2), 0.0) + COALESCE(MAX(0.0, #4 - #5), 0.0) + ", + "COALESCE(MAX(#1 - #2, 0.0), 0.0) + COALESCE(MAX(#4 - #5, 0.0), 0.0) + ", // dangling meters - "MAX(0.0, #6) + MAX(0.0, #7)" + "MAX(#6, 0.0) + MAX(#7, 0.0)" ) ); @@ -342,9 +334,9 @@ mod tests { formula, concat!( // subtract meter successors from meters - "COALESCE(MAX(0.0, #1 - #2), 0.0) + COALESCE(MAX(0.0, #4 - #5), 0.0) + ", + "COALESCE(MAX(#1 - #2, 0.0), 0.0) + COALESCE(MAX(#4 - #5, 0.0), 0.0) + ", // dangling meters - "MAX(0.0, #6) + MAX(0.0, #7)" + "MAX(#6, 0.0) + MAX(#7, 0.0)" ) ); @@ -366,11 +358,11 @@ mod tests { formula, concat!( // subtract meter successors from meters - "COALESCE(MAX(0.0, #1 - #2), 0.0) + COALESCE(MAX(0.0, #4 - #5), 0.0) + ", + "COALESCE(MAX(#1 - #2, 0.0), 0.0) + COALESCE(MAX(#4 - #5, 0.0), 0.0) + ", // dangling meters - "MAX(0.0, #6) + MAX(0.0, #7) + ", + "MAX(#6, 0.0) + MAX(#7, 0.0) + ", // PV inverter and CHP - "MAX(0.0, #11) + MAX(0.0, #10)", + "MAX(#11, 0.0) + MAX(#10, 0.0)", ) ); @@ -392,7 +384,7 @@ mod tests { let graph = builder.build(None)?; let formula = graph.consumer_formula()?.to_string(); - assert_eq!(formula, "MAX(0.0, #1) + MAX(0.0, #2) + MAX(0.0, #3)"); + assert_eq!(formula, "MAX(#1, 0.0) + MAX(#2, 0.0) + MAX(#3, 0.0)"); // Add two solar inverters with two grid meters as predecessors. let meter_pv_chain_1 = builder.meter_pv_chain(1); @@ -411,11 +403,11 @@ mod tests { formula, concat!( // difference of pv powers from first two grid meters - "MAX(0.0, #1 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0)) + ", + "MAX(#1 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0), 0.0) + ", // third grid meter still dangling - "MAX(0.0, #3) + ", + "MAX(#3, 0.0) + ", // difference of solar inverters from their meters - "COALESCE(MAX(0.0, #4 - #5), 0.0) + COALESCE(MAX(0.0, #6 - #7), 0.0)" + "COALESCE(MAX(#4 - #5, 0.0), 0.0) + COALESCE(MAX(#6 - #7, 0.0), 0.0)" ) ); @@ -434,11 +426,11 @@ 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, 0.0) - COALESCE(#6, #7, 0.0)) + ", + "MAX(#1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0), 0.0) + ", // difference of meter#8 from third grid meter - "MAX(0.0, #3 - #8) + ", + "MAX(#3 - #8, 0.0) + ", // difference of solar inverters from their meters - "COALESCE(MAX(0.0, #4 - #5), 0.0) + COALESCE(MAX(0.0, #6 - #7), 0.0)" + "COALESCE(MAX(#4 - #5, 0.0), 0.0) + COALESCE(MAX(#6 - #7, 0.0), 0.0)" ) ); @@ -453,15 +445,15 @@ mod tests { concat!( // difference of pv and battery powers from first two grid // meters and meter#8 - "MAX(0.0, ", - "#1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0) - COALESCE(#9, #10, 0.0)", - ") + ", + "MAX(", + "#1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0) - COALESCE(#9, #10, 0.0), ", + "0.0) + ", // difference of meter#8 from third grid meter - "MAX(0.0, #3 - #8) + ", + "MAX(#3 - #8, 0.0) + ", // difference of solar inverters from their meters - "COALESCE(MAX(0.0, #4 - #5), 0.0) + COALESCE(MAX(0.0, #6 - #7), 0.0) + ", + "COALESCE(MAX(#4 - #5, 0.0), 0.0) + COALESCE(MAX(#6 - #7, 0.0), 0.0) + ", // difference of battery inverter from battery meter - "COALESCE(MAX(0.0, #9 - #10), 0.0)" + "COALESCE(MAX(#9 - #10, 0.0), 0.0)" ) ); diff --git a/src/graph/formulas/generators/generic.rs b/src/graph/formulas/generators/generic.rs deleted file mode 100644 index 8c7934a..0000000 --- a/src/graph/formulas/generators/generic.rs +++ /dev/null @@ -1,8 +0,0 @@ -// License: MIT -// Copyright © 2025 Frequenz Energy-as-a-Service GmbH - -//! This module contains the methods for generating generic formulas. - -mod coalesce; - -pub(crate) use coalesce::CoalesceFormulaBuilder; diff --git a/src/graph/formulas/generators/generic/coalesce.rs b/src/graph/formulas/generators/generic/coalesce.rs deleted file mode 100644 index 8b6bbaa..0000000 --- a/src/graph/formulas/generators/generic/coalesce.rs +++ /dev/null @@ -1,88 +0,0 @@ -// License: MIT -// Copyright © 2025 Frequenz Energy-as-a-Service GmbH - -//! This module contains the methods for generating `COALESCE` formulas for -//! measuring metrics from individual components, with fallback to other -//! components. - -use std::collections::BTreeSet; - -use crate::{ - graph::formulas::{expr::Expr, AggregationFormula}, - ComponentGraph, Edge, Error, Node, -}; - -pub(crate) struct CoalesceFormulaBuilder { - component_ids: BTreeSet, -} - -impl CoalesceFormulaBuilder { - pub fn try_new( - graph: &ComponentGraph, - component_ids: BTreeSet, - ) -> Result { - if component_ids.is_empty() { - return Err(Error::missing_parameters("No component IDs specified.")); - } - for component_id in &component_ids { - if graph.component(*component_id).is_err() { - return Err(Error::component_not_found(format!( - "Component with ID {component_id} not found in the graph." - ))); - } - } - Ok(Self { component_ids }) - } - - /// Generates a formula that uses the `COALESCE` function to return the first - /// non-null value from the provided component IDs. - pub fn build(self) -> Result { - if self.component_ids.len() == 1 { - if let Some(component_id) = self.component_ids.into_iter().next() { - return Ok(AggregationFormula::new(Expr::component(component_id))); - } else { - return Err(Error::internal( - "Failed to create expression for single component ID.", - )); - } - } - let expr = Expr::coalesce( - self.component_ids - .into_iter() - .map(|component_id| Expr::Component { component_id }) - .collect(), - ); - Ok(AggregationFormula::new(expr)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::graph::test_utils::ComponentGraphBuilder; - - #[test] - fn test_coalesce_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_1 = builder.meter(); - builder.connect(grid, grid_meter_1); - let grid_meter_2 = builder.meter(); - builder.connect(grid, grid_meter_2); - - let graph = builder.build(None)?; - let formula = graph.coalesce(BTreeSet::from([1, 2]))?.to_string(); - assert_eq!(formula, "COALESCE(#1, #2)"); - let formula = graph.coalesce(BTreeSet::from([1]))?.to_string(); - assert_eq!(formula, "#1"); - let formula = graph.coalesce(BTreeSet::from([])).unwrap_err(); - assert_eq!( - formula, - Error::missing_parameters("No component IDs specified.") - ); - - Ok(()) - } -} diff --git a/src/graph/formulas/generators/grid_coalesce.rs b/src/graph/formulas/generators/grid_coalesce.rs index 7804ef5..150aa97 100644 --- a/src/graph/formulas/generators/grid_coalesce.rs +++ b/src/graph/formulas/generators/grid_coalesce.rs @@ -33,17 +33,19 @@ where /// inverters, and battery inverters that are directly connected to the /// grid. pub fn build(self) -> Result { - Ok(CoalesceFormula::new(Expr::coalesce( - self.graph - .successors(self.graph.root_id)? - .filter(|node| { - node.is_meter() - || node.is_pv_inverter() - || node.is_battery_inverter(&self.graph.config) - }) - .map(|comp| Expr::component(comp.component_id())) - .collect(), - ))) + let expr = self + .graph + .successors(self.graph.root_id)? + .filter(|node| { + node.is_meter() + || node.is_pv_inverter() + || node.is_battery_inverter(&self.graph.config) + }) + .fold(Expr::None, |coalesced, component| { + coalesced.coalesce(Expr::component(component.component_id())) + }); + + Ok(CoalesceFormula::new(expr)) } } diff --git a/src/graph/formulas/generators/producer.rs b/src/graph/formulas/generators/producer.rs index 9eee0e0..2299ca1 100644 --- a/src/graph/formulas/generators/producer.rs +++ b/src/graph/formulas/generators/producer.rs @@ -45,7 +45,10 @@ where petgraph::Direction::Outgoing, false, )? { - let comp_expr = Self::min_zero(self.graph.fallback_expr([component_id], false)?); + let comp_expr = self + .graph + .fallback_expr([component_id], false)? + .min(Expr::number(0.0)); expr = match expr { None => Some(comp_expr), Some(e) => Some(e + comp_expr), @@ -55,14 +58,6 @@ where .map(AggregationFormula::new) .unwrap_or_else(|| AggregationFormula::new(Expr::number(0.0)))) } - - /// 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)] @@ -89,7 +84,7 @@ mod tests { let formula = graph.producer_formula()?.to_string(); assert_eq!( formula, - "MIN(0.0, COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)))" + "MIN(COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)), 0.0)" ); // Add a CHP meter to the grid with a CHP behind it. @@ -101,8 +96,8 @@ mod tests { assert_eq!( formula, 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(COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)), 0.0) + ", + "MIN(COALESCE(#6, #5, 0.0), 0.0)" ) ); @@ -115,9 +110,9 @@ mod tests { assert_eq!( formula, 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))" + "MIN(COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)), 0.0) + ", + "MIN(COALESCE(#6, #5, 0.0), 0.0) + ", + "MIN(COALESCE(#7, 0.0), 0.0)" ) ); @@ -130,10 +125,10 @@ mod tests { assert_eq!( formula, 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)) + ", - "MIN(0.0, COALESCE(#8, 0.0))" + "MIN(COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)), 0.0) + ", + "MIN(COALESCE(#6, #5, 0.0), 0.0) + ", + "MIN(COALESCE(#7, 0.0), 0.0) + ", + "MIN(COALESCE(#8, 0.0), 0.0)" ) ); @@ -146,10 +141,10 @@ mod tests { assert_eq!( formula, 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)) + ", - "MIN(0.0, COALESCE(#8, 0.0))" + "MIN(COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)), 0.0) + ", + "MIN(COALESCE(#6, #5, 0.0), 0.0) + ", + "MIN(COALESCE(#7, 0.0), 0.0) + ", + "MIN(COALESCE(#8, 0.0), 0.0)" ) ); @@ -166,12 +161,12 @@ mod tests { assert_eq!( formula, 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)) + ", - "MIN(0.0, COALESCE(#8, 0.0)) + ", - "MIN(0.0, COALESCE(#13, 0.0)) + ", - "MIN(0.0, COALESCE(#14, 0.0))" + "MIN(COALESCE(#4 + #3, #2, COALESCE(#4, 0.0) + COALESCE(#3, 0.0)), 0.0) + ", + "MIN(COALESCE(#6, #5, 0.0), 0.0) + ", + "MIN(COALESCE(#7, 0.0), 0.0) + ", + "MIN(COALESCE(#8, 0.0), 0.0) + ", + "MIN(COALESCE(#13, 0.0), 0.0) + ", + "MIN(COALESCE(#14, 0.0), 0.0)" ) ); diff --git a/src/graph/formulas/generators/pv_ac_coalesce.rs b/src/graph/formulas/generators/pv_ac_coalesce.rs index 0f24548..a85570e 100644 --- a/src/graph/formulas/generators/pv_ac_coalesce.rs +++ b/src/graph/formulas/generators/pv_ac_coalesce.rs @@ -57,7 +57,6 @@ where /// meters and inverters in the graph. pub fn build(self) -> Result { let mut meters: BTreeSet = BTreeSet::new(); - let mut source_components: Vec = vec![]; for inv_id in &self.pv_inverter_ids { if !self.graph.component(*inv_id)?.is_pv_inverter() { @@ -71,18 +70,15 @@ where } } } - source_components.extend( - meters - .iter() - .chain(self.pv_inverter_ids.iter()) - .map(|component_id: &u64| Expr::component(*component_id)), - ); - if source_components.is_empty() { - return Err(Error::component_not_found("No PV inverters found.")); - } + let coalesced = meters + .iter() + .chain(self.pv_inverter_ids.iter()) + .fold(Expr::None, |expr, component_id: &u64| { + expr.coalesce(Expr::component(*component_id)) + }); - Ok(CoalesceFormula::new(Expr::coalesce(source_components))) + Ok(CoalesceFormula::new(coalesced)) } } @@ -101,11 +97,8 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.pv_ac_coalesce_formula(None); - assert_eq!( - formula, - Err(Error::component_not_found("No PV inverters found.")) - ); + let formula = graph.pv_ac_coalesce_formula(None)?.to_string(); + assert_eq!(formula, "None"); // Add a PV meter with one PV inverter. let meter_pv_chain = builder.meter_pv_chain(1); diff --git a/src/lib.rs b/src/lib.rs index d4cab7b..d07d6cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ mod component_category; pub use component_category::{BatteryType, ComponentCategory, EvChargerType, InverterType}; mod graph; -pub use graph::{iterators, AggregationFormula, CoalesceFormula, ComponentGraph}; +pub use graph::{iterators, AggregationFormula, CoalesceFormula, ComponentGraph, Formula}; mod graph_traits; pub use graph_traits::{Edge, Node};