Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
19 changes: 11 additions & 8 deletions src/graph/formulas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,61 @@ use crate::Node;

mod expr;
mod fallback;
mod formula;
mod generators;
mod traversal;

pub use formula::Formula;

/// Formulas for various microgrid metrics.
impl<N, E> ComponentGraph<N, E>
where
N: Node,
E: Edge,
{
/// Returns a string representing the consumer formula for the graph.
pub fn consumer_formula(&self) -> Result<String, Error> {
pub fn consumer_formula(&self) -> Result<Formula, Error> {
generators::consumer::ConsumerFormulaBuilder::try_new(self)?.build()
}

/// Returns a string representing the grid formula for the graph.
pub fn grid_formula(&self) -> Result<String, Error> {
pub fn grid_formula(&self) -> Result<Formula, Error> {
generators::grid::GridFormulaBuilder::try_new(self)?.build()
}

/// Returns a string representing the producer formula for the graph.
pub fn producer_formula(&self) -> Result<String, Error> {
pub fn producer_formula(&self) -> Result<Formula, Error> {
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<BTreeSet<u64>>) -> Result<String, Error> {
pub fn battery_formula(&self, battery_ids: Option<BTreeSet<u64>>) -> Result<Formula, Error> {
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<BTreeSet<u64>>) -> Result<String, Error> {
pub fn chp_formula(&self, chp_ids: Option<BTreeSet<u64>>) -> Result<Formula, Error> {
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<BTreeSet<u64>>) -> Result<String, Error> {
pub fn pv_formula(&self, pv_inverter_ids: Option<BTreeSet<u64>>) -> Result<Formula, Error> {
generators::pv::PVFormulaBuilder::try_new(self, pv_inverter_ids)?.build()
}

/// Returns a string with 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<u64>) -> Result<String, Error> {
pub fn coalesce(&self, component_ids: BTreeSet<u64>) -> Result<Formula, Error> {
generators::generic::CoalesceFormulaBuilder::try_new(self, component_ids)?.build()
}

/// Returns a string representing the EV charger formula for the graph.
pub fn ev_charger_formula(
&self,
ev_charger_ids: Option<BTreeSet<u64>>,
) -> Result<String, Error> {
) -> Result<Formula, Error> {
generators::ev_charger::EVChargerFormulaBuilder::try_new(self, ev_charger_ids)?.build()
}
}
54 changes: 54 additions & 0 deletions src/graph/formulas/formula.rs
Original file line number Diff line number Diff line change
@@ -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<Formula> for String {
fn from(formula: Formula) -> Self {
formula.expr.to_string()
}
}
52 changes: 31 additions & 21 deletions src/graph/formulas/generators/battery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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<String, Error> {
pub fn build(self) -> Result<Formula, Error> {
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(
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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!(
Expand All @@ -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))"
Expand All @@ -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!(
Expand Down Expand Up @@ -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!(
Expand All @@ -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!(
Expand Down
34 changes: 20 additions & 14 deletions src/graph/formulas/generators/chp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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<String, Error> {
pub fn build(self) -> Result<Formula, Error> {
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 {
Expand All @@ -59,7 +61,7 @@ where

self.graph
.fallback_expr(self.chp_ids, false)
.map(|expr| expr.to_string())
.map(Formula::new)
}
}

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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!(
Expand All @@ -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
Expand All @@ -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!(
Expand All @@ -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!(
Expand All @@ -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!(
Expand All @@ -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:
Expand Down
Loading