diff --git a/src/graph.rs b/src/graph.rs index 579e15c..6d8b0c5 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -13,6 +13,7 @@ mod formulas; pub mod iterators; use crate::{ComponentGraphConfig, Edge, Node}; +pub use formulas::Formula; use petgraph::graph::{DiGraph, NodeIndex}; use std::collections::HashMap; diff --git a/src/graph/formulas.rs b/src/graph/formulas.rs index dedc9f1..0802ebf 100644 --- a/src/graph/formulas.rs +++ b/src/graph/formulas.rs @@ -12,9 +12,12 @@ use crate::Node; mod expr; mod fallback; +mod formula; mod generators; mod traversal; +pub use formula::Formula; + /// Formulas for various microgrid metrics. impl ComponentGraph where @@ -22,32 +25,32 @@ where E: Edge, { /// Returns a string representing the consumer formula for the graph. - pub fn consumer_formula(&self) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + pub fn pv_formula(&self, pv_inverter_ids: Option>) -> Result { generators::pv::PVFormulaBuilder::try_new(self, pv_inverter_ids)?.build() } @@ -55,7 +58,7 @@ where /// /// 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 { + pub fn coalesce(&self, component_ids: BTreeSet) -> Result { generators::generic::CoalesceFormulaBuilder::try_new(self, component_ids)?.build() } @@ -63,7 +66,7 @@ where pub fn ev_charger_formula( &self, ev_charger_ids: Option>, - ) -> Result { + ) -> Result { generators::ev_charger::EVChargerFormulaBuilder::try_new(self, ev_charger_ids)?.build() } } diff --git a/src/graph/formulas/formula.rs b/src/graph/formulas/formula.rs new file mode 100644 index 0000000..e99202e --- /dev/null +++ b/src/graph/formulas/formula.rs @@ -0,0 +1,54 @@ +// License: MIT +// Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +//! This module defines the `Formula` struct and its operations. + +use super::expr::Expr; + +// This struct represents the microgrid metric formulas that are generated by +// traversing the component graph. +// +// `Formula` objects can be added or subtracted from each other, and they can +// converted to a string representation, before they are passed to an evaluator. +#[derive(Debug, Clone)] +pub struct Formula { + expr: Expr, +} + +impl Formula { + pub(crate) fn new(expr: Expr) -> Self { + Formula { expr } + } +} + +impl std::ops::Add for Formula { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Formula { + expr: self.expr + rhs.expr, + } + } +} + +impl std::ops::Sub for Formula { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Formula { + expr: self.expr - rhs.expr, + } + } +} + +impl std::fmt::Display for Formula { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.expr.fmt(f) + } +} + +impl From for String { + fn from(formula: Formula) -> Self { + formula.expr.to_string() + } +} diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs index 045566c..e30822f 100644 --- a/src/graph/formulas/generators/battery.rs +++ b/src/graph/formulas/generators/battery.rs @@ -6,6 +6,8 @@ use std::collections::BTreeSet; use crate::component_category::CategoryPredicates; +use crate::graph::formulas::expr::Expr; +use crate::graph::formulas::Formula; use crate::{ComponentGraph, Edge, Error, Node}; pub(crate) struct BatteryFormulaBuilder<'a, N, E> @@ -47,14 +49,14 @@ where /// 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 { + pub fn build(self) -> Result { if self.inverter_ids.is_empty() { - return Ok("0.0".to_string()); + return Ok(Formula::new(Expr::number(0.0))); } self.graph .fallback_expr(self.inverter_ids, false) - .map(|expr| expr.to_string()) + .map(Formula::new) } fn find_inverter_ids( @@ -104,7 +106,7 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.battery_formula(None)?; + let formula = graph.battery_formula(None)?.to_string(); assert_eq!(formula, "0.0"); // Add a battery meter with one inverter and one battery. @@ -115,7 +117,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 2); let graph = builder.build(None)?; - let formula = graph.battery_formula(None)?; + let formula = graph.battery_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a second battery meter with one inverter and two batteries. @@ -125,18 +127,22 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 5); let graph = builder.build(None)?; - let formula = graph.battery_formula(None)?; + let formula = graph.battery_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#6, #5, 0.0)"); - let formula = graph.battery_formula(Some(BTreeSet::from([4])))?; + let formula = graph + .battery_formula(Some(BTreeSet::from([4])))? + .to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); - let formula = graph.battery_formula(Some(BTreeSet::from([7, 8])))?; + let formula = graph + .battery_formula(Some(BTreeSet::from([7, 8])))? + .to_string(); assert_eq!(formula, "COALESCE(#6, #5, 0.0)"); let formula = graph - .battery_formula(Some(BTreeSet::from([4, 8, 7]))) - .unwrap(); + .battery_formula(Some(BTreeSet::from([4, 8, 7])))? + .to_string(); 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. @@ -146,7 +152,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 9); let graph = builder.build(None)?; - let formula = graph.battery_formula(None)?; + let formula = graph.battery_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -157,8 +163,8 @@ mod tests { ); let formula = graph - .battery_formula(Some(BTreeSet::from([12, 13]))) - .unwrap(); + .battery_formula(Some(BTreeSet::from([12, 13])))? + .to_string(); assert_eq!( formula, "COALESCE(#11 + #10, #9, COALESCE(#11, 0.0) + COALESCE(#10, 0.0))" @@ -171,7 +177,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 14); let graph = builder.build(None)?; - let formula = graph.battery_formula(None)?; + let formula = graph.battery_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -208,7 +214,7 @@ mod tests { allow_unspecified_inverters: true, ..Default::default() }))?; - let formula = graph.battery_formula(None)?; + let formula = graph.battery_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -220,22 +226,26 @@ mod tests { ); let formula = graph - .battery_formula(Some(BTreeSet::from([19, 21]))) - .unwrap(); + .battery_formula(Some(BTreeSet::from([19, 21])))? + .to_string(); 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(); + let formula = graph + .battery_formula(Some(BTreeSet::from([19])))? + .to_string(); assert_eq!(formula, "COALESCE(#18, 0.0)"); - let formula = graph.battery_formula(Some(BTreeSet::from([21]))).unwrap(); + let formula = graph + .battery_formula(Some(BTreeSet::from([21])))? + .to_string(); assert_eq!(formula, "COALESCE(#20, 0.0)"); let formula = graph - .battery_formula(Some(BTreeSet::from([4, 12, 13, 19]))) - .unwrap(); + .battery_formula(Some(BTreeSet::from([4, 12, 13, 19])))? + .to_string(); assert_eq!( formula, concat!( diff --git a/src/graph/formulas/generators/chp.rs b/src/graph/formulas/generators/chp.rs index ebf2f44..803ee1e 100644 --- a/src/graph/formulas/generators/chp.rs +++ b/src/graph/formulas/generators/chp.rs @@ -6,6 +6,8 @@ use std::collections::BTreeSet; use crate::component_category::CategoryPredicates; +use crate::graph::formulas::expr::Expr; +use crate::graph::formulas::Formula; use crate::{ComponentGraph, Edge, Error, Node}; pub(crate) struct CHPFormulaBuilder<'a, N, E> @@ -43,9 +45,9 @@ where /// /// 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 { + pub fn build(self) -> Result { if self.chp_ids.is_empty() { - return Ok("0.0".to_string()); + return Ok(Formula::new(Expr::number(0.0))); } for id in &self.chp_ids { @@ -59,7 +61,7 @@ where self.graph .fallback_expr(self.chp_ids, false) - .map(|expr| expr.to_string()) + .map(Formula::new) } } @@ -78,7 +80,7 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.chp_formula(None)?; + let formula = graph.chp_formula(None)?.to_string(); assert_eq!(formula, "0.0"); // Add a chp meter with one chp @@ -89,7 +91,7 @@ mod tests { assert_eq!(meter_chp_chain.component_id(), 2); let graph = builder.build(None)?; - let formula = graph.chp_formula(None)?; + let formula = graph.chp_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a battery meter with one inverter and two batteries. @@ -99,7 +101,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 4); let graph = builder.build(None)?; - let formula = graph.chp_formula(None)?; + let formula = graph.chp_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a chp meter with two CHPs. @@ -109,7 +111,7 @@ mod tests { assert_eq!(meter_chp_chain.component_id(), 8); let graph = builder.build(None)?; - let formula = graph.chp_formula(None)?; + let formula = graph.chp_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -118,7 +120,9 @@ mod tests { ) ); - let formula = graph.chp_formula(Some(BTreeSet::from([10, 3]))).unwrap(); + let formula = graph + .chp_formula(Some(BTreeSet::from([10, 3])))? + .to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#10, 0.0)"); // add a meter direct to the grid with three CHPs @@ -128,7 +132,7 @@ mod tests { assert_eq!(meter_chp_chain.component_id(), 11); let graph = builder.build(None)?; - let formula = graph.chp_formula(None)?; + let formula = graph.chp_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -143,8 +147,8 @@ mod tests { ); let formula = graph - .chp_formula(Some(BTreeSet::from([3, 9, 10, 12, 13]))) - .unwrap(); + .chp_formula(Some(BTreeSet::from([3, 9, 10, 12, 13])))? + .to_string(); assert_eq!( formula, concat!( @@ -155,8 +159,8 @@ mod tests { ); let formula = graph - .chp_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14]))) - .unwrap(); + .chp_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14])))? + .to_string(); assert_eq!( formula, concat!( @@ -170,7 +174,9 @@ mod tests { ), ); - let formula = graph.chp_formula(Some(BTreeSet::from([10, 14]))).unwrap(); + let formula = graph + .chp_formula(Some(BTreeSet::from([10, 14])))? + .to_string(); assert_eq!(formula, "COALESCE(#10, 0.0) + COALESCE(#14, 0.0)"); // Failure cases: diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index 0e17a05..cfa07ae 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -6,7 +6,10 @@ use std::collections::{BTreeMap, BTreeSet}; use super::super::expr::Expr; -use crate::{component_category::CategoryPredicates, ComponentGraph, Edge, Error, Node}; +use crate::{ + component_category::CategoryPredicates, graph::formulas::Formula, ComponentGraph, Edge, Error, + Node, +}; pub(crate) struct ConsumerFormulaBuilder<'a, N, E> where @@ -35,7 +38,7 @@ where } /// Generates the consumer formula for the given node. - pub fn build(mut self) -> Result { + 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)?; @@ -60,9 +63,9 @@ where }; 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()), + (Some(lhs), Some(rhs)) => Ok(Formula::new(lhs + rhs)), + (None, Some(expr)) | (Some(expr), None) => Ok(Formula::new(expr)), + (None, None) => Ok(Formula::new(Expr::number(0.0))), } } @@ -138,7 +141,7 @@ mod tests { builder.connect(grid, inv_bat_chain); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!(formula, "0.0"); Ok(()) @@ -154,7 +157,7 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!(formula, "MAX(0.0, #1)"); // Add a battery meter with one battery inverter and one battery to the @@ -165,7 +168,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 2); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); // Formula subtracts the battery meter from the grid meter, and the // battery inverter from the battery meter. assert_eq!( @@ -180,7 +183,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 5); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -212,7 +215,7 @@ mod tests { assert_eq!(meter.component_id(), 11); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -234,7 +237,7 @@ mod tests { disable_fallback_components: true, ..Default::default() }))?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -258,7 +261,7 @@ mod tests { assert_eq!(dangling_meter.component_id(), 15); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -297,7 +300,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 1); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + 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)"); @@ -315,7 +318,7 @@ mod tests { assert_eq!(dangling_meter_2.component_id(), 7); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -334,7 +337,7 @@ mod tests { builder.connect(grid, inv_bat_chain); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -358,7 +361,7 @@ mod tests { assert_eq!(chp.component_id(), 11); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -388,7 +391,7 @@ mod tests { builder.connect(grid, grid_meter_3); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); 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. @@ -403,7 +406,7 @@ mod tests { assert_eq!(meter_pv_chain_2.component_id(), 6); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -426,7 +429,7 @@ mod tests { assert_eq!(meter.component_id(), 8); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -444,7 +447,7 @@ mod tests { builder.connect(grid_meter_1, meter_bat_chain); let graph = builder.build(None)?; - let formula = graph.consumer_formula()?; + let formula = graph.consumer_formula()?.to_string(); assert_eq!( formula, concat!( diff --git a/src/graph/formulas/generators/ev_charger.rs b/src/graph/formulas/generators/ev_charger.rs index cb00eff..7cabda6 100644 --- a/src/graph/formulas/generators/ev_charger.rs +++ b/src/graph/formulas/generators/ev_charger.rs @@ -6,6 +6,8 @@ use std::collections::BTreeSet; use crate::component_category::CategoryPredicates; +use crate::graph::formulas::expr::Expr; +use crate::graph::formulas::Formula; use crate::{ComponentGraph, Edge, Error, Node}; pub(crate) struct EVChargerFormulaBuilder<'a, N, E> @@ -46,9 +48,9 @@ where /// /// 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 { + pub fn build(self) -> Result { if self.ev_charger_ids.is_empty() { - return Ok("0.0".to_string()); + return Ok(Formula::new(Expr::number(0.0))); } for id in &self.ev_charger_ids { @@ -62,7 +64,7 @@ where self.graph .fallback_expr(self.ev_charger_ids, false) - .map(|expr| expr.to_string()) + .map(Formula::new) } } @@ -81,7 +83,7 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.ev_charger_formula(None)?; + let formula = graph.ev_charger_formula(None)?.to_string(); assert_eq!(formula, "0.0"); // Add a EV charger meter with one EV charger. @@ -92,7 +94,7 @@ mod tests { assert_eq!(meter_ev_charger_chain.component_id(), 2); let graph = builder.build(None)?; - let formula = graph.ev_charger_formula(None)?; + let formula = graph.ev_charger_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a battery meter with one inverter and two batteries. @@ -102,7 +104,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 4); let graph = builder.build(None)?; - let formula = graph.ev_charger_formula(None)?; + let formula = graph.ev_charger_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a EV charger meter with two EV chargers. @@ -112,7 +114,7 @@ mod tests { assert_eq!(meter_ev_charger_chain.component_id(), 8); let graph = builder.build(None)?; - let formula = graph.ev_charger_formula(None)?; + let formula = graph.ev_charger_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -122,8 +124,8 @@ mod tests { ); let formula = graph - .ev_charger_formula(Some(BTreeSet::from([10, 3]))) - .unwrap(); + .ev_charger_formula(Some(BTreeSet::from([10, 3])))? + .to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#10, 0.0)"); // add a meter direct to the grid with three EV chargers @@ -133,7 +135,7 @@ mod tests { assert_eq!(meter_ev_charger_chain.component_id(), 11); let graph = builder.build(None)?; - let formula = graph.ev_charger_formula(None)?; + let formula = graph.ev_charger_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -148,8 +150,8 @@ mod tests { ); let formula = graph - .ev_charger_formula(Some(BTreeSet::from([3, 9, 10, 12, 13]))) - .unwrap(); + .ev_charger_formula(Some(BTreeSet::from([3, 9, 10, 12, 13])))? + .to_string(); assert_eq!( formula, concat!( @@ -161,8 +163,8 @@ mod tests { ); let formula = graph - .ev_charger_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14]))) - .unwrap(); + .ev_charger_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14])))? + .to_string(); assert_eq!( formula, concat!( @@ -177,8 +179,8 @@ mod tests { ); let formula = graph - .ev_charger_formula(Some(BTreeSet::from([10, 14]))) - .unwrap(); + .ev_charger_formula(Some(BTreeSet::from([10, 14])))? + .to_string(); assert_eq!(formula, "COALESCE(#10, 0.0) + COALESCE(#14, 0.0)"); // Failure cases: diff --git a/src/graph/formulas/generators/generic/coalesce.rs b/src/graph/formulas/generators/generic/coalesce.rs index 16f88ce..0feeec7 100644 --- a/src/graph/formulas/generators/generic/coalesce.rs +++ b/src/graph/formulas/generators/generic/coalesce.rs @@ -7,7 +7,10 @@ use std::collections::BTreeSet; -use crate::{graph::formulas::expr::Expr, ComponentGraph, Edge, Error, Node}; +use crate::{ + graph::formulas::{expr::Expr, Formula}, + ComponentGraph, Edge, Error, Node, +}; pub(crate) struct CoalesceFormulaBuilder { component_ids: BTreeSet, @@ -34,10 +37,10 @@ impl CoalesceFormulaBuilder { /// 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 { + pub fn build(self) -> Result { if self.component_ids.len() == 1 { if let Some(component_id) = self.component_ids.into_iter().next() { - return Ok(Expr::Component { component_id }.to_string()); + return Ok(Formula::new(Expr::component(component_id))); } else { return Err(Error::internal( "Failed to create expression for single component ID.", @@ -50,7 +53,7 @@ impl CoalesceFormulaBuilder { .map(|component_id| Expr::Component { component_id }) .collect(), ); - Ok(expr.to_string()) + Ok(Formula::new(expr)) } } @@ -71,14 +74,14 @@ mod tests { builder.connect(grid, grid_meter_2); let graph = builder.build(None)?; - let formula = graph.coalesce(BTreeSet::from([1, 2]))?; + let formula = graph.coalesce(BTreeSet::from([1, 2]))?.to_string(); assert_eq!(formula, "COALESCE(#1, #2)"); - let formula = graph.coalesce(BTreeSet::from([1]))?; + let formula = graph.coalesce(BTreeSet::from([1]))?.to_string(); assert_eq!(formula, "#1"); - let formula = graph.coalesce(BTreeSet::from([])); + let formula = graph.coalesce(BTreeSet::from([])).unwrap_err(); assert_eq!( formula, - Err(Error::missing_parameters("No component IDs specified.")) + Error::missing_parameters("No component IDs specified.") ); Ok(()) diff --git a/src/graph/formulas/generators/grid.rs b/src/graph/formulas/generators/grid.rs index 95c8edc..168627c 100644 --- a/src/graph/formulas/generators/grid.rs +++ b/src/graph/formulas/generators/grid.rs @@ -3,7 +3,10 @@ //! This module contains the methods for generating grid formulas. -use crate::{ComponentGraph, Edge, Error, Node}; +use crate::{ + graph::formulas::{expr::Expr, Formula}, + ComponentGraph, Edge, Error, Node, +}; pub(crate) struct GridFormulaBuilder<'a, N, E> where @@ -27,7 +30,7 @@ where /// 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 { + 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)?; @@ -37,8 +40,8 @@ where }; } Ok(expr - .map(|e| e.to_string()) - .unwrap_or_else(|| "0.0".to_string())) + .map(Formula::new) + .unwrap_or_else(|| Formula::new(Expr::number(0.0)))) } } @@ -59,7 +62,7 @@ mod tests { builder.connect(grid_meter, meter_bat_chain); let graph = builder.build(None)?; - let formula = graph.grid_formula()?; + let formula = graph.grid_formula()?.to_string(); assert_eq!(formula, "#1"); // Add an additional dangling meter, and a PV chain and a battery chain @@ -76,7 +79,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 9); let graph = builder.build(None)?; - let formula = graph.grid_formula()?; + let formula = graph.grid_formula()?.to_string(); assert_eq!( formula, "#1 + #5 + COALESCE(#6, #7, 0.0) + COALESCE(#9, #10, 0.0)" @@ -89,7 +92,7 @@ mod tests { assert_eq!(pv_inverter.component_id(), 11); let graph = builder.build(None)?; - let formula = graph.grid_formula()?; + let formula = graph.grid_formula()?.to_string(); assert_eq!( formula, "#1 + #5 + COALESCE(#6, #7, 0.0) + COALESCE(#9, #10, 0.0) + COALESCE(#11, 0.0)" diff --git a/src/graph/formulas/generators/producer.rs b/src/graph/formulas/generators/producer.rs index 12a0db7..36fb850 100644 --- a/src/graph/formulas/generators/producer.rs +++ b/src/graph/formulas/generators/producer.rs @@ -5,6 +5,7 @@ use super::super::expr::Expr; use crate::component_category::CategoryPredicates; +use crate::graph::formulas::Formula; use crate::{ComponentGraph, Edge, Error, Node}; pub(crate) struct ProducerFormulaBuilder<'a, N, E> @@ -28,7 +29,7 @@ where /// /// The production formula is the sum of all the PV and CHP components in /// the graph. - pub fn build(self) -> Result { + pub fn build(self) -> Result { let mut expr = None; for component_id in self.graph.find_all( self.graph.root_id, @@ -51,8 +52,8 @@ where }; } Ok(expr - .map(|e| e.to_string()) - .unwrap_or_else(|| "0.0".to_string())) + .map(Formula::new) + .unwrap_or_else(|| Formula::new(Expr::number(0.0)))) } /// Returns a formula expression for just the production part of the given @@ -78,14 +79,14 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.producer_formula()?; + let formula = graph.producer_formula()?.to_string(); 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(None)?; - let formula = graph.producer_formula()?; + 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)))" @@ -96,7 +97,7 @@ mod tests { builder.connect(grid, meter_chp_chain); let graph = builder.build(None)?; - let formula = graph.producer_formula()?; + let formula = graph.producer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -110,7 +111,7 @@ mod tests { builder.connect(grid, chp); let graph = builder.build(None)?; - let formula = graph.producer_formula()?; + let formula = graph.producer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -125,7 +126,7 @@ mod tests { builder.connect(grid_meter, pv_inverter); let graph = builder.build(None)?; - let formula = graph.producer_formula()?; + let formula = graph.producer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -141,7 +142,7 @@ mod tests { builder.connect(grid_meter, meter_bat_chain); let graph = builder.build(None)?; - let formula = graph.producer_formula()?; + let formula = graph.producer_formula()?.to_string(); assert_eq!( formula, concat!( @@ -161,7 +162,7 @@ mod tests { builder.connect(grid_meter, meter); let graph = builder.build(None)?; - let formula = graph.producer_formula()?; + let formula = graph.producer_formula()?.to_string(); assert_eq!( formula, concat!( diff --git a/src/graph/formulas/generators/pv.rs b/src/graph/formulas/generators/pv.rs index 8065b35..2af7fe1 100644 --- a/src/graph/formulas/generators/pv.rs +++ b/src/graph/formulas/generators/pv.rs @@ -6,6 +6,8 @@ use std::collections::BTreeSet; use crate::component_category::CategoryPredicates; +use crate::graph::formulas::expr::Expr; +use crate::graph::formulas::Formula; use crate::{ComponentGraph, Edge, Error, Node}; pub(crate) struct PVFormulaBuilder<'a, N, E> @@ -46,9 +48,9 @@ where /// /// 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 { + pub fn build(self) -> Result { if self.pv_inverter_ids.is_empty() { - return Ok("0.0".to_string()); + return Ok(Formula::new(Expr::number(0.0))); } for id in &self.pv_inverter_ids { @@ -62,7 +64,7 @@ where self.graph .fallback_expr(self.pv_inverter_ids, false) - .map(|expr| expr.to_string()) + .map(Formula::new) } } @@ -81,7 +83,7 @@ mod tests { builder.connect(grid, grid_meter); let graph = builder.build(None)?; - let formula = graph.pv_formula(None)?; + let formula = graph.pv_formula(None)?.to_string(); assert_eq!(formula, "0.0"); // Add a PV meter with one PV inverter. @@ -92,7 +94,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 2); let graph = builder.build(None)?; - let formula = graph.pv_formula(None)?; + let formula = graph.pv_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a battery meter with one inverter and two batteries. @@ -102,7 +104,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 4); let graph = builder.build(None)?; - let formula = graph.pv_formula(None)?; + let formula = graph.pv_formula(None)?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); // Add a PV meter with two PV inverters. @@ -112,7 +114,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 8); let graph = builder.build(None)?; - let formula = graph.pv_formula(None)?; + let formula = graph.pv_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -121,7 +123,7 @@ mod tests { ) ); - let formula = graph.pv_formula(Some(BTreeSet::from([10, 3]))).unwrap(); + let formula = graph.pv_formula(Some(BTreeSet::from([10, 3])))?.to_string(); assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#10, 0.0)"); // add a meter direct to the grid with three PV inverters @@ -131,7 +133,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 11); let graph = builder.build(None)?; - let formula = graph.pv_formula(None)?; + let formula = graph.pv_formula(None)?.to_string(); assert_eq!( formula, concat!( @@ -146,8 +148,8 @@ mod tests { ); let formula = graph - .pv_formula(Some(BTreeSet::from([3, 9, 10, 12, 13]))) - .unwrap(); + .pv_formula(Some(BTreeSet::from([3, 9, 10, 12, 13])))? + .to_string(); assert_eq!( formula, concat!( @@ -159,8 +161,8 @@ mod tests { ); let formula = graph - .pv_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14]))) - .unwrap(); + .pv_formula(Some(BTreeSet::from([3, 9, 10, 12, 13, 14])))? + .to_string(); assert_eq!( formula, concat!( @@ -174,7 +176,9 @@ mod tests { ) ); - let formula = graph.pv_formula(Some(BTreeSet::from([10, 14]))).unwrap(); + let formula = graph + .pv_formula(Some(BTreeSet::from([10, 14])))? + .to_string(); assert_eq!(formula, "COALESCE(#10, 0.0) + COALESCE(#14, 0.0)"); // Failure cases: diff --git a/src/lib.rs b/src/lib.rs index d844bee..748c8b5 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, ComponentGraph}; +pub use graph::{iterators, ComponentGraph, Formula}; mod graph_traits; pub use graph_traits::{Edge, Node};