use std::collections::{BTreeMap, BTreeSet};
use super::super::expr::Expr;
use crate::{
ComponentGraph, Edge, Error, Node,
component_category::CategoryPredicates,
graph::formulas::{
AggregationFormula, fallback::FallbackExpr, generators::grid::GridFormulaBuilder,
},
};
pub(crate) struct ConsumerFormulaBuilder<'a, N, E>
where
N: Node,
E: Edge,
{
unvisited_meters: BTreeSet<u64>,
graph: &'a ComponentGraph<N, E>,
}
fn is_grid_meter<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
component: &N,
) -> Result<bool, Error> {
if let Some(predecessor) = graph.predecessors(component.component_id())?.next() {
let sibling_count = graph
.siblings_from_predecessors(component.component_id())?
.count();
let is_fallback_grid_meter = is_grid_meter(graph, predecessor)? && sibling_count == 0;
Ok((predecessor.is_grid() || is_fallback_grid_meter)
&& component.is_meter()
&& !graph.is_component_meter(component.component_id())?)
} else {
Ok(false)
}
}
impl<'a, N, E> ConsumerFormulaBuilder<'a, N, E>
where
N: Node,
E: Edge,
{
pub fn try_new(graph: &'a ComponentGraph<N, E>) -> Result<Self, Error> {
Ok(Self {
unvisited_meters: graph.find_all(
graph.root_id,
|node| node.is_meter(),
petgraph::Direction::Outgoing,
true,
)?,
graph,
})
}
pub fn build(mut self) -> Result<AggregationFormula, Error> {
if !self.graph.config.include_phantom_loads_in_consumer_formula {
return self.build_without_phantom_loads();
}
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(&self.graph.config))
.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(AggregationFormula::new(lhs + rhs)),
(None, Some(expr)) | (Some(expr), None) => Ok(AggregationFormula::new(expr)),
(None, None) => Ok(AggregationFormula::new(Expr::number(0.0))),
}
}
fn component_consumption(&mut self, component_id: u64) -> Result<Expr, Error> {
let component = self.graph.component(component_id)?;
if component.is_meter() {
self.unvisited_meters.remove(&component_id);
let mut expr = Expr::from(component);
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);
}
}
for successor in successors {
let successor_expr = if successor.1.is_meter() {
FallbackExpr::new()
.prefer_meters(true)
.generate(self.graph, BTreeSet::from([successor.0]))?
} else {
Expr::from(successor.1)
};
expr = expr - successor_expr;
}
expr = expr.max(Expr::number(0.0));
if self.graph.has_successors(component_id)?
&& !self.graph.has_meter_successors(component_id)?
{
expr = expr.coalesce(Expr::number(0.0));
}
Ok(expr)
} else {
Ok(Expr::from(component).max(Expr::number(0.0)))
}
}
fn build_without_phantom_loads(&self) -> Result<AggregationFormula, Error> {
let grid_successors = self
.graph
.successors(self.graph.root_id)?
.collect::<Vec<_>>();
if grid_successors.is_empty() {
return Ok(AggregationFormula::new(Expr::number(0.0)));
}
if grid_successors
.iter()
.all(|s| is_grid_meter(self.graph, s).unwrap_or(false))
{
self.build_with_grid_meter()
} else {
self.build_without_grid_meter()
}
}
fn build_with_grid_meter(&self) -> Result<AggregationFormula, Error> {
let non_consumer_components = self.graph.find_all(
self.graph.root_id,
|node| {
self.graph
.is_component_chain(node.component_id())
.unwrap_or(false)
},
petgraph::Direction::Outgoing,
false,
)?;
let mut expr = GridFormulaBuilder::try_new(self.graph)?.build()?.expr;
for component_id in non_consumer_components {
if is_grid_meter(self.graph, self.graph.component(component_id)?)? {
continue;
}
let component_with_fallback = FallbackExpr::new()
.prefer_meters(true)
.generate(self.graph, BTreeSet::from([component_id]))?;
expr = expr - component_with_fallback;
}
Ok(AggregationFormula::new(expr.max(Expr::number(0.0))))
}
fn build_without_grid_meter(&self) -> Result<AggregationFormula, Error> {
let consumer_components = self.graph.find_all(
self.graph.root_id,
|node| {
node.is_meter()
&& !self
.graph
.is_component_meter(node.component_id())
.unwrap_or(false)
},
petgraph::Direction::Outgoing,
false,
)?;
let mut expr = None;
for component_id in consumer_components {
let component = Expr::component(component_id);
expr = match expr {
None => Some(component),
Some(e) => Some(e + component),
};
}
Ok(AggregationFormula::new(
expr.map(|expr| expr.max(Expr::number(0.0)))
.unwrap_or_else(|| Expr::number(0.0)),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ComponentGraphConfig, graph::test_utils::ComponentGraphBuilder};
#[test]
fn test_zero_consumers() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let inv_bat_chain = builder.inv_bat_chain(1);
builder.connect(grid, inv_bat_chain);
let graph = builder.build(None)?;
let formula = graph.consumer_formula()?.to_string();
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();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let config = Some(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
);
let graph = builder.build(config)?;
let graph_no_phantom = builder.build(None)?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#1, 0.0)");
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#1, 0.0)");
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 config = Some(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
);
let graph = builder.build(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
"MAX(#1 - COALESCE(#2, #3, 0.0), 0.0) + COALESCE(MAX(#2 - #3, 0.0), 0.0)"
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#1 - COALESCE(#2, #3, 0.0), 0.0)");
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(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(",
"#1 - COALESCE(#2, #3, 0.0) - COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)), ",
"0.0",
") + ",
"COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0)",
)
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(",
"#1 - COALESCE(#2, #3, 0.0) - COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)), ",
"0.0",
")",
)
);
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(config)?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"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)), ",
"0.0) + ",
"COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0) + ",
"COALESCE(MAX(#11 - #8 - #9 - #10, 0.0), 0.0)"
)
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(",
"#1 - ",
"COALESCE(#2, #3, 0.0) - ",
"COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)) - ",
"COALESCE(#8, 0.0) - COALESCE(#9, 0.0) - COALESCE(#10, 0.0), ",
"0.0)"
)
);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.disable_fallback_components(true)
.include_phantom_loads_in_consumer_formula(true)
.build(),
))?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(#1 - #2 - #5 - #11, 0.0) + ",
"COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0) + ",
"COALESCE(MAX(#11 - #8 - #9 - #10, 0.0), 0.0)"
)
);
let graph_no_phantom = builder.build(Some(
ComponentGraphConfig::builder()
.disable_fallback_components(true)
.include_phantom_loads_in_consumer_formula(false)
.build(),
))?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#1 - #2 - #5 - #8 - #9 - #10, 0.0)");
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 config = Some(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
);
let graph = builder.build(config)?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"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), ",
"0.0) + ",
"COALESCE(MAX(#2 - #3, 0.0), 0.0) + COALESCE(MAX(#5 - #6 - #7, 0.0), 0.0) + ",
"COALESCE(MAX(#11 - #8 - #9 - #10, 0.0), 0.0) + ",
"COALESCE(MAX(#12 - #13, 0.0), 0.0) + ",
"MAX(#15, 0.0)"
)
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(",
"#1 + #15 - ",
"COALESCE(#2, #3, 0.0) - ",
"COALESCE(#5, COALESCE(#7, 0.0) + COALESCE(#6, 0.0)) - ",
"COALESCE(#8, 0.0) - COALESCE(#9, 0.0) - COALESCE(#10, 0.0) - ",
"COALESCE(#12, #13, 0.0), ",
"0.0)",
)
);
Ok(())
}
#[test]
fn test_consumer_formula_without_grid_meter() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
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 config = Some(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
);
let graph = builder.build(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(formula, "COALESCE(MAX(#1 - #2, 0.0), 0.0)");
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "0.0");
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(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"COALESCE(MAX(#1 - #2, 0.0), 0.0) + COALESCE(MAX(#4 - #5, 0.0), 0.0) + ",
"MAX(#6, 0.0) + MAX(#7, 0.0)"
)
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#6 + #7, 0.0)");
let inv_bat_chain = builder.inv_bat_chain(1);
builder.connect(grid, inv_bat_chain);
let graph = builder.build(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"COALESCE(MAX(#1 - #2, 0.0), 0.0) + COALESCE(MAX(#4 - #5, 0.0), 0.0) + ",
"MAX(#6, 0.0) + MAX(#7, 0.0)"
)
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#6 + #7, 0.0)");
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(config)?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"COALESCE(MAX(#1 - #2, 0.0), 0.0) + COALESCE(MAX(#4 - #5, 0.0), 0.0) + ",
"MAX(#6, 0.0) + MAX(#7, 0.0) + ",
"MAX(#11, 0.0) + MAX(#10, 0.0)",
)
);
let graph_no_phantom = builder.build(None)?;
let formula = graph_no_phantom.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#6 + #7, 0.0)");
Ok(())
}
#[test]
fn test_consumer_formula_diamond_meters() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.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 config = Some(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
);
let graph = builder.build(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(formula, "MAX(#1, 0.0) + MAX(#2, 0.0) + MAX(#3, 0.0)");
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(config)?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(#1 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0), 0.0) + ",
"MAX(#3, 0.0) + ",
"COALESCE(MAX(#4 - #5, 0.0), 0.0) + COALESCE(MAX(#6 - #7, 0.0), 0.0)"
)
);
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 config = Some(
ComponentGraphConfig::builder()
.include_phantom_loads_in_consumer_formula(true)
.build(),
);
let graph = builder.build(config.clone())?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(#1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0), 0.0) + ",
"MAX(#3 - #8, 0.0) + ",
"COALESCE(MAX(#4 - #5, 0.0), 0.0) + COALESCE(MAX(#6 - #7, 0.0), 0.0)"
)
);
let meter_bat_chain = builder.meter_bat_chain(1, 1);
builder.connect(grid_meter_1, meter_bat_chain);
let graph = builder.build(config)?;
let formula = graph.consumer_formula()?.to_string();
assert_eq!(
formula,
concat!(
"MAX(",
"#1 + #8 + #2 - COALESCE(#4, #5, 0.0) - COALESCE(#6, #7, 0.0) - COALESCE(#9, #10, 0.0), ",
"0.0) + ",
"MAX(#3 - #8, 0.0) + ",
"COALESCE(MAX(#4 - #5, 0.0), 0.0) + COALESCE(MAX(#6 - #7, 0.0), 0.0) + ",
"COALESCE(MAX(#9 - #10, 0.0), 0.0)"
)
);
Ok(())
}
}