use std::collections::BTreeSet;
use super::{SourcePreference, aggregate};
use crate::graph::test_utils::ComponentGraphBuilder;
use crate::{ComponentCategory, ComponentGraphConfig, Error, InverterType, OperationalMode};
#[test]
fn test_no_meters() {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let inverter = builder.solar_inverter();
builder.connect(grid, inverter);
let graph = builder.build(None).unwrap();
let expr = graph.pv_formula(None).unwrap().to_string();
assert_eq!(expr, "COALESCE(#1, 0.0)");
let inverter = builder.solar_inverter();
builder.connect(grid, inverter);
let graph = builder.build(None).unwrap();
let expr = graph.pv_formula(None).unwrap().to_string();
assert_eq!(expr, "COALESCE(#1, 0.0) + COALESCE(#2, 0.0)");
}
#[test]
fn test_aggregate() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let meter_bat_chain = builder.meter_bat_chain(1, 1);
builder.connect(grid_meter, meter_bat_chain);
assert_eq!(grid_meter.component_id(), 1);
assert_eq!(meter_bat_chain.component_id(), 2);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([1]),
SourcePreference::MetersFirstWithChains,
)?;
assert_eq!(expr.to_string(), "#1");
let expr = aggregate(
&graph,
BTreeSet::from([1, 2]),
SourcePreference::MetersFirstWithChains,
)?;
assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3, 0.0)");
let expr = aggregate(
&graph,
BTreeSet::from([1, 2]),
SourcePreference::ComponentsFirst,
)?;
assert_eq!(expr.to_string(), "#1 + COALESCE(#3, #2, 0.0)");
let expr = aggregate(
&graph,
BTreeSet::from([1, 2]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "#1 + COALESCE(#2, #3, 0.0)");
let expr = aggregate(&graph, BTreeSet::from([3]), SourcePreference::MetersFirst)?;
assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)");
let expr = aggregate(
&graph,
BTreeSet::from([3]),
SourcePreference::MetersFirstWithChains,
)?;
assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)");
let expr = aggregate(
&graph,
BTreeSet::from([2]),
SourcePreference::MetersFirstWithChains,
)?;
assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)");
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.disable_fallback_components(true)
.build(),
))?;
let expr = aggregate(
&graph,
BTreeSet::from([1, 2]),
SourcePreference::ComponentsFirst,
)?;
assert_eq!(expr.to_string(), "#1 + #2");
let expr = aggregate(
&graph,
BTreeSet::from([1, 2]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "#1 + #2");
let expr = aggregate(&graph, BTreeSet::from([3]), SourcePreference::MetersFirst)?;
assert_eq!(expr.to_string(), "#3");
let meter_bat_chain = builder.meter_bat_chain(3, 3);
builder.connect(grid_meter, meter_bat_chain);
assert_eq!(meter_bat_chain.component_id(), 5);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([3, 5]),
SourcePreference::ComponentsFirst,
)?;
assert_eq!(
expr.to_string(),
concat!(
"COALESCE(#3, #2, 0.0) + ",
"COALESCE(",
"#8 + #7 + #6, ",
"#5, ",
"COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0)",
")"
)
);
let expr = aggregate(
&graph,
BTreeSet::from([2, 5]),
SourcePreference::MetersFirst,
)?;
assert_eq!(
expr.to_string(),
concat!(
"COALESCE(#2, #3, 0.0) + ",
"COALESCE(#5, COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0))"
)
);
let expr = aggregate(
&graph,
BTreeSet::from([2, 6, 7, 8]),
SourcePreference::MetersFirst,
)?;
assert_eq!(
expr.to_string(),
concat!(
"COALESCE(#2, #3, 0.0) + ",
"COALESCE(#5, COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0))"
)
);
let expr = aggregate(
&graph,
BTreeSet::from([2, 7, 8]),
SourcePreference::MetersFirst,
)?;
assert_eq!(
expr.to_string(),
concat!(
"COALESCE(#2, #3, 0.0) + ",
"COALESCE(#5 - #6, COALESCE(#7, 0.0) + COALESCE(#8, 0.0))"
)
);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.disable_fallback_components(true)
.build(),
))?;
let expr = aggregate(
&graph,
BTreeSet::from([3, 5]),
SourcePreference::ComponentsFirst,
)?;
assert_eq!(expr.to_string(), "#3 + #5");
let expr = aggregate(
&graph,
BTreeSet::from([2, 5]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "#2 + #5");
let expr = aggregate(
&graph,
BTreeSet::from([2, 6, 7, 8]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "#2 + #6 + #7 + #8");
let expr = aggregate(
&graph,
BTreeSet::from([2, 7, 8]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "#2 + #7 + #8");
let meter = builder.meter();
let chp = builder.chp();
let pv_inverter = builder.solar_inverter();
builder.connect(grid_meter, meter);
builder.connect(meter, chp);
builder.connect(meter, pv_inverter);
assert_eq!(meter.component_id(), 12);
assert_eq!(chp.component_id(), 13);
assert_eq!(pv_inverter.component_id(), 14);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([5, 12]),
SourcePreference::MetersFirst,
)?;
assert_eq!(
expr.to_string(),
concat!(
"COALESCE(#5, COALESCE(#8, 0.0) + COALESCE(#7, 0.0) + COALESCE(#6, 0.0)) + ",
"COALESCE(#12, COALESCE(#14, 0.0) + COALESCE(#13, 0.0))"
)
);
let expr = aggregate(
&graph,
BTreeSet::from([7, 14]),
SourcePreference::ComponentsFirst,
)?;
assert_eq!(
expr.to_string(),
"COALESCE(#7, #5 - #6 - #8, 0.0) + COALESCE(#14, #12 - #13, 0.0)"
);
Ok(())
}
#[test]
fn test_aggregate_through_meter_chain() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let meter1 = builder.meter();
let meter2 = builder.meter();
let bat_chain = builder.meter_bat_chain(1, 1);
let pv_chain = builder.meter_pv_chain(1);
builder.connect(grid, meter1);
builder.connect(meter1, meter2);
builder.connect(meter2, bat_chain);
builder.connect(meter2, pv_chain);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([meter1.component_id()]),
SourcePreference::MetersFirstWithChains,
)?;
assert_eq!(expr.to_string(), "COALESCE(#1, #2)");
Ok(())
}
#[test]
fn test_measurement_points() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let meter = builder.meter();
let metered = builder.battery_inverter();
let battery = builder.battery();
builder.connect(grid, meter);
builder.connect(meter, metered);
builder.connect(metered, battery);
let meterless = builder.battery_inverter();
let meterless_battery = builder.battery();
builder.connect(grid, meterless);
builder.connect(meterless, meterless_battery);
let graph = builder.build(None)?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([metered.component_id()]))?,
vec![super::resolve::Measurement::Single(meter.component_id())],
);
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([meterless.component_id()]))?,
vec![super::resolve::Measurement::Single(
meterless.component_id()
)],
);
Ok(())
}
#[test]
fn test_aggregate_diamond() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let m1 = builder.meter();
let m2 = builder.meter();
let inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(grid, m1);
builder.connect(grid, m2);
builder.connect(m1, inverter);
builder.connect(m2, inverter);
builder.connect(inverter, battery);
let graph = builder.build(None)?;
let targets = BTreeSet::from([inverter.component_id()]);
assert_eq!(
super::resolve::measurement_points(&graph, &targets)?,
vec![super::resolve::Measurement::Diamond {
components: vec![inverter.component_id()],
meters: vec![m1.component_id(), m2.component_id()],
}],
);
assert_eq!(
aggregate(&graph, targets.clone(), SourcePreference::MetersFirst)?.to_string(),
"COALESCE(#1 + #2, #3, COALESCE(#1, 0.0) + COALESCE(#2, 0.0))",
);
assert_eq!(
aggregate(&graph, targets, SourcePreference::ComponentsFirst)?.to_string(),
"COALESCE(#3, COALESCE(#1, 0.0) + COALESCE(#2, 0.0))",
);
Ok(())
}
#[test]
fn test_stands_alone() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let component_meter = builder.meter();
let inv1 = builder.battery_inverter();
let inv2 = builder.battery_inverter();
let battery = builder.battery();
builder.connect(grid, component_meter);
builder.connect(component_meter, inv1);
builder.connect(component_meter, inv2);
builder.connect(inv1, battery);
builder.connect(inv2, battery);
let mixed_meter = builder.meter();
let child_meter = builder.meter();
let inv3 = builder.battery_inverter();
let battery2 = builder.battery();
builder.connect(grid, mixed_meter);
builder.connect(mixed_meter, child_meter);
builder.connect(mixed_meter, inv3);
builder.connect(inv3, battery2);
let graph = builder.build(None)?;
assert!(!super::emit::stands_alone(
&graph,
component_meter.component_id(),
SourcePreference::MetersFirst,
)?);
assert!(super::emit::stands_alone(
&graph,
mixed_meter.component_id(),
SourcePreference::MetersFirstWithChains,
)?);
Ok(())
}
#[test]
fn test_stands_alone_total_through_child_meter() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let battery_meter = builder.meter_bat_chain(1, 1);
builder.connect(mixed_meter, battery_meter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([mixed_meter.component_id()]),
SourcePreference::MetersFirst,
)?;
assert_eq!(
expr.to_string(),
"COALESCE(#2, COALESCE(#4, #5, 0.0) + COALESCE(#3, 0.0))",
);
Ok(())
}
#[test]
fn test_substitution_asymmetric_diamond_order_independent() -> Result<(), Error> {
for shared_id_first in [false, true] {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let m_a = builder.meter();
let m_b = builder.meter();
builder.connect(grid_meter, m_a);
builder.connect(grid_meter, m_b);
let first = builder.solar_inverter();
let second = builder.solar_inverter();
let (a, b) = if shared_id_first {
(second, first)
} else {
(first, second)
};
builder.connect(m_a, a);
builder.connect(m_a, b);
builder.connect(m_b, b);
let graph = builder.build(None)?;
let targets = BTreeSet::from([a.component_id(), b.component_id()]);
assert_eq!(
super::resolve::measurement_points(&graph, &targets)?,
vec![super::resolve::Measurement::Diamond {
components: vec![4, 5],
meters: vec![m_a.component_id(), m_b.component_id()],
}],
"shared_id_first: {shared_id_first}",
);
assert_eq!(
aggregate(&graph, targets, SourcePreference::MetersFirst)?.to_string(),
"COALESCE(#2 + #3, #4 + #5, COALESCE(#2, 0.0) + COALESCE(#3, 0.0))",
"shared_id_first: {shared_id_first}",
);
}
Ok(())
}
#[test]
fn test_substitution_rejects_parent_meter_as_sibling() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let battery_meter = builder.meter();
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(mixed_meter, battery_meter);
builder.connect(battery_meter, battery_inverter);
builder.connect(battery_inverter, battery);
builder.connect(mixed_meter, battery_inverter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([3, 4, 5]))?,
vec![super::resolve::Measurement::Single(2)],
);
Ok(())
}
#[test]
fn test_child_meter_diamond_stays_bare() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv1 = builder.solar_inverter();
builder.connect(mixed_meter, pv1);
let m_a = builder.meter();
let m_b = builder.meter();
builder.connect(mixed_meter, m_a);
builder.connect(mixed_meter, m_b);
let pv2 = builder.solar_inverter();
builder.connect(m_a, pv2);
builder.connect(m_b, pv2);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([mixed_meter.component_id()]),
SourcePreference::MetersFirst,
)?
.to_string(),
"COALESCE(#2, #5 + #4 + COALESCE(#3, 0.0))",
);
Ok(())
}
#[test]
fn test_aggregate_subtraction() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let inverters: Vec<_> = (0..5).map(|_| builder.solar_inverter()).collect();
for inverter in &inverters {
builder.connect(mixed_meter, *inverter);
}
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
let load_meter = builder.meter();
builder.connect(main_meter, load_meter);
let graph = builder.build(None)?;
let targets = BTreeSet::from([3, 4, 5, 6, 7]);
assert_eq!(
super::resolve::measurement_points(&graph, &targets)?,
vec![super::resolve::Measurement::Subtraction {
parent_meters: vec![mixed_meter.component_id()],
subtracted: vec![sub_meter.component_id()],
components: vec![3, 4, 5, 6, 7],
}],
);
assert_eq!(
aggregate(&graph, targets.clone(), SourcePreference::MetersFirst)?.to_string(),
concat!(
"COALESCE(#2 - #8, ",
"COALESCE(#3, 0.0) + COALESCE(#4, 0.0) + COALESCE(#5, 0.0) + ",
"COALESCE(#6, 0.0) + COALESCE(#7, 0.0))"
),
);
assert_eq!(
aggregate(&graph, targets, SourcePreference::ComponentsFirst)?.to_string(),
concat!(
"COALESCE(#3 + #4 + #5 + #6 + #7, #2 - #8, ",
"COALESCE(#3, 0.0) + COALESCE(#4, 0.0) + COALESCE(#5, 0.0) + ",
"COALESCE(#6, 0.0) + COALESCE(#7, 0.0))"
),
);
assert_eq!(
graph.pv_formula(None)?.to_string(),
concat!(
"COALESCE(#3 + #4 + #5 + #6 + #7, #2 - #8, ",
"COALESCE(#3, 0.0) + COALESCE(#4, 0.0) + COALESCE(#5, 0.0) + ",
"COALESCE(#6, 0.0) + COALESCE(#7, 0.0))"
),
);
assert_eq!(
graph.pv_formula(Some(BTreeSet::from([3])))?.to_string(),
"COALESCE(#3, #2 - #4 - #5 - #6 - #7 - #8, 0.0)",
);
Ok(())
}
#[test]
fn test_aggregate_subtraction_diamond() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let m_a = builder.meter();
let m_b = builder.meter();
builder.connect(grid_meter, m_a);
builder.connect(grid_meter, m_b);
let pv1 = builder.solar_inverter();
let pv2 = builder.solar_inverter();
for meter in [m_a, m_b] {
builder.connect(meter, pv1);
builder.connect(meter, pv2);
}
let graph = builder.build(None)?;
let targets = BTreeSet::from([pv1.component_id()]);
assert_eq!(
super::resolve::measurement_points(&graph, &targets)?,
vec![super::resolve::Measurement::Subtraction {
parent_meters: vec![m_a.component_id(), m_b.component_id()],
subtracted: vec![pv2.component_id()],
components: vec![pv1.component_id()],
}],
);
assert_eq!(
aggregate(&graph, targets.clone(), SourcePreference::ComponentsFirst)?.to_string(),
"COALESCE(#4, #2 + #3 - #5, 0.0)",
);
assert_eq!(
aggregate(&graph, targets, SourcePreference::MetersFirst)?.to_string(),
"COALESCE(#2 + #3 - #5, #4, 0.0)",
);
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([4, 5]))?,
vec![super::resolve::Measurement::Diamond {
components: vec![4, 5],
meters: vec![2, 3],
}],
);
Ok(())
}
#[test]
fn test_subtraction_diamond_subsumes_earlier_single() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let m_a = builder.meter();
let m_b = builder.meter();
builder.connect(grid_meter, m_a);
builder.connect(grid_meter, m_b);
let pv1 = builder.solar_inverter();
let bat_inverter = builder.battery_inverter();
let battery = builder.battery();
let pv2 = builder.solar_inverter();
builder.connect(m_a, pv1);
builder.connect(m_a, bat_inverter);
builder.connect(bat_inverter, battery);
builder.connect(m_a, pv2);
builder.connect(m_b, pv2);
let graph = builder.build(None)?;
let targets = BTreeSet::from([pv1.component_id(), pv2.component_id()]);
assert_eq!(
super::resolve::measurement_points(&graph, &targets)?,
vec![super::resolve::Measurement::Subtraction {
parent_meters: vec![m_a.component_id(), m_b.component_id()],
subtracted: vec![bat_inverter.component_id()],
components: vec![pv1.component_id(), pv2.component_id()],
}],
);
assert_eq!(
aggregate(&graph, targets, SourcePreference::ComponentsFirst)?.to_string(),
"COALESCE(#4 + #7, #2 + #3 - #5, COALESCE(#4, 0.0) + COALESCE(#7, 0.0))",
);
Ok(())
}
#[test]
fn test_subtraction_diamond_disqualified_by_grid_meters() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let m1 = builder.meter();
let m2 = builder.meter();
builder.connect(grid, m1);
builder.connect(grid, m2);
let pv = builder.solar_inverter();
let sub_meter = builder.meter();
for meter in [m1, m2] {
builder.connect(meter, pv);
builder.connect(meter, sub_meter);
}
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#3, 0.0)");
Ok(())
}
#[test]
fn test_aggregate_subtraction_component_siblings() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let pv_meter = builder.meter_pv_chain(3);
builder.connect(grid, main_meter);
builder.connect(main_meter, pv_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(Some(BTreeSet::from([3])))?.to_string(),
"COALESCE(#3, #2 - #4 - #5, 0.0)",
);
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let battery_inverter = builder.inv_bat_chain(1);
builder.connect(mixed_meter, battery_inverter);
let load_meter = builder.meter();
builder.connect(main_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#3, #2 - #4, 0.0)",
);
assert_eq!(
graph.battery_formula(None)?.to_string(),
"COALESCE(#4, #2 - #3, 0.0)",
);
Ok(())
}
#[test]
fn test_aggregate_subtraction_component_sub_meter() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let pv1 = builder.solar_inverter();
let pv2 = builder.solar_inverter();
builder.connect(mixed_meter, pv1);
builder.connect(mixed_meter, pv2);
let battery_meter = builder.meter_bat_chain(1, 1);
builder.connect(mixed_meter, battery_meter);
let load_meter = builder.meter();
builder.connect(main_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#3 + #4, #2 - #5, COALESCE(#3, 0.0) + COALESCE(#4, 0.0))",
);
assert_eq!(
graph.battery_formula(None)?.to_string(),
"COALESCE(#6, #5, 0.0)",
);
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let battery_inverter = builder.inv_bat_chain(1);
builder.connect(mixed_meter, battery_inverter);
let pv_meter = builder.meter_pv_chain(1);
builder.connect(mixed_meter, pv_meter);
let load_meter = builder.meter();
builder.connect(main_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.battery_formula(None)?.to_string(),
"COALESCE(#3, #2 - #5, 0.0)",
);
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#6, #5, 0.0)");
Ok(())
}
#[test]
fn test_subtraction_disqualifiers() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let hybrid = builder.add_component(crate::ComponentCategory::Inverter(
crate::InverterType::Hybrid,
));
let battery = builder.battery();
builder.connect(mixed_meter, hybrid);
builder.connect(hybrid, battery);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#3, 0.0)");
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let pv_meter = builder.meter_pv_chain(1);
builder.connect(mixed_meter, pv_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#3, 0.0) + COALESCE(#5, #4, 0.0)",
);
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let pv = builder.solar_inverter();
builder.connect(grid_meter, pv);
let sub_meter = builder.meter();
builder.connect(grid_meter, sub_meter);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#2, 0.0)");
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
let fallback_grid_meter = builder.meter();
builder.connect(grid, grid_meter);
builder.connect(grid_meter, fallback_grid_meter);
let pv = builder.solar_inverter();
builder.connect(fallback_grid_meter, pv);
let sub_meter = builder.meter();
builder.connect(fallback_grid_meter, sub_meter);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#3, 0.0)");
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
builder.connect(main_meter, sub_meter);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#3, 0.0)");
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let main_meter = builder.meter();
let mixed_meter = builder.meter();
let other_meter = builder.meter();
builder.connect(grid, main_meter);
builder.connect(main_meter, mixed_meter);
builder.connect(main_meter, other_meter);
let pv = builder.solar_inverter();
let pv_external = builder.solar_inverter();
let sub_meter = builder.meter();
builder.connect(mixed_meter, pv);
builder.connect(mixed_meter, pv_external);
builder.connect(mixed_meter, sub_meter);
builder.connect(grid, pv_external);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.allow_component_validation_failures(true)
.build(),
))?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#4, 0.0) + COALESCE(#5, 0.0)",
);
Ok(())
}
#[test]
fn test_children_fallback_drops_feeder_of_sibling() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let battery_meter = builder.meter();
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(mixed_meter, battery_meter);
builder.connect(battery_meter, battery_inverter);
builder.connect(battery_inverter, battery);
builder.connect(mixed_meter, battery_inverter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([mixed_meter.component_id()]),
SourcePreference::MetersFirst,
)?
.to_string(),
"COALESCE(#2, COALESCE(#5, 0.0) + COALESCE(#3, 0.0))",
);
Ok(())
}
#[test]
fn test_children_fallback_drops_transitive_feeder() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let sub_a = builder.meter();
builder.connect(mixed_meter, sub_a);
let sub_b = builder.meter();
builder.connect(sub_a, sub_b);
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(sub_b, battery_inverter);
builder.connect(mixed_meter, battery_inverter);
builder.connect(battery_inverter, battery);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([mixed_meter.component_id()]),
SourcePreference::MetersFirst,
)?
.to_string(),
"COALESCE(#2, #5, 0.0)",
);
Ok(())
}
#[test]
fn test_children_fallback_drops_device_feeder() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(mixed_meter, battery_inverter);
builder.connect(battery_inverter, battery);
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
builder.connect(battery_inverter, sub_meter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.allow_component_validation_failures(true)
.build(),
))?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([mixed_meter.component_id()]),
SourcePreference::MetersFirst,
)?
.to_string(),
"#2",
);
Ok(())
}
#[test]
fn test_children_fallback_drops_child_shared_with_parallel_meter() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let meter_a = builder.meter();
builder.connect(grid_meter, meter_a);
let meter_b = builder.meter();
builder.connect(grid_meter, meter_b);
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(meter_a, battery_inverter);
builder.connect(meter_b, battery_inverter);
builder.connect(battery_inverter, battery);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([meter_a.component_id(), meter_b.component_id()]),
SourcePreference::MetersFirst,
)?
.to_string(),
"#2 + #3",
);
Ok(())
}
#[test]
fn test_subsumed_sub_meter_stays_claimed() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let pv = builder.solar_inverter();
builder.connect(grid_meter, pv);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let sub_meter = builder.meter();
let chp = builder.chp();
builder.connect(mixed_meter, sub_meter);
builder.connect(mixed_meter, chp);
let nested_chp = builder.chp();
builder.connect(sub_meter, nested_chp);
let graph = builder.build(None)?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([
sub_meter.component_id(),
chp.component_id(),
nested_chp.component_id(),
]),
SourcePreference::MetersFirst,
)?
.to_string(),
"COALESCE(#3, COALESCE(#5, 0.0) + COALESCE(#4, #6, 0.0))",
);
Ok(())
}
#[test]
fn test_grid_meter_never_backed_by_children() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let fallback_grid_meter = builder.meter();
builder.connect(grid_meter, fallback_grid_meter);
let pv = builder.solar_inverter();
let chp = builder.chp();
builder.connect(fallback_grid_meter, pv);
builder.connect(fallback_grid_meter, chp);
let graph = builder.build(None)?;
assert_eq!(
aggregate(
&graph,
BTreeSet::from([grid_meter.component_id()]),
SourcePreference::MetersFirstWithChains,
)?
.to_string(),
"COALESCE(#1, #2)",
);
assert_eq!(
aggregate(
&graph,
BTreeSet::from([fallback_grid_meter.component_id()]),
SourcePreference::MetersFirst,
)?
.to_string(),
"#2",
);
Ok(())
}
#[test]
fn test_subtraction_covered_feeder() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(mixed_meter, battery_inverter);
builder.connect(sub_meter, battery_inverter);
builder.connect(battery_inverter, battery);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#3, #2 - #5, 0.0)",
);
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(mixed_meter, battery_inverter);
builder.connect(sub_meter, battery_inverter);
builder.connect(battery_inverter, battery);
let chp = builder.chp();
builder.connect(sub_meter, chp);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#3, 0.0)");
Ok(())
}
#[test]
fn test_grid_meter_second_feed_order_independent() -> Result<(), Error> {
for grid_edge_first in [true, false] {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let other_meter = builder.meter();
builder.connect(grid, other_meter);
let meter = builder.meter();
if grid_edge_first {
builder.connect(grid, meter);
builder.connect(other_meter, meter);
} else {
builder.connect(other_meter, meter);
builder.connect(grid, meter);
}
let pv = builder.solar_inverter();
builder.connect(meter, pv);
let sub_meter = builder.meter();
builder.connect(meter, sub_meter);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.allow_component_validation_failures(true)
.build(),
))?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#3, 0.0)",
"grid_edge_first: {grid_edge_first}",
);
}
Ok(())
}
#[test]
fn test_off_root_cycle_terminates() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let meter = builder.meter();
builder.connect(grid_meter, meter);
let pv = builder.solar_inverter();
builder.connect(meter, pv);
let x = builder.meter();
let y = builder.meter();
builder.connect(x, y);
builder.connect(y, x);
builder.connect(x, meter);
builder.connect(x, pv);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.allow_unconnected_components(true)
.allow_component_validation_failures(true)
.build(),
))?;
graph.pv_formula(None)?;
Ok(())
}
#[test]
fn test_pruned_empty_subtraction_keeps_shape() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let meter = builder.meter();
let pv = builder.solar_inverter();
builder.connect(meter, pv);
let a = builder.meter();
let b = builder.meter();
builder.connect(meter, a);
builder.connect(meter, b);
builder.connect(a, b);
builder.connect(b, a);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.allow_unconnected_components(true)
.allow_component_validation_failures(true)
.build(),
))?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([3]))?,
vec![super::resolve::Measurement::Subtraction {
parent_meters: vec![2],
subtracted: vec![],
components: vec![3],
}],
);
Ok(())
}
#[test]
fn test_claim_semantics_meter_targets() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let pv1 = builder.solar_inverter();
builder.connect(mixed_meter, pv1);
let pv2 = builder.solar_inverter();
builder.connect(mixed_meter, pv2);
let battery_inverter = builder.inv_bat_chain(1);
builder.connect(mixed_meter, battery_inverter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([2, 3]))?,
vec![
super::resolve::Measurement::Single(2),
super::resolve::Measurement::Single(3)
],
);
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([2, 3, 4]))?,
vec![
super::resolve::Measurement::Single(2),
super::resolve::Measurement::Single(3),
super::resolve::Measurement::Single(4)
],
);
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([2, 3, 4, 5]))?,
vec![super::resolve::Measurement::Single(2)],
);
Ok(())
}
#[test]
fn test_subsumed_claim_stays_claimed() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let battery_inverter = builder.battery_inverter();
let battery = builder.battery();
builder.connect(sub_meter, battery_inverter);
builder.connect(battery_inverter, battery);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([3, 4, 5]))?,
vec![super::resolve::Measurement::Single(2)],
);
Ok(())
}
#[test]
fn test_subtraction_rejects_target_meter_sibling() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let sub_meter = builder.meter();
builder.connect(mixed_meter, sub_meter);
let pv = builder.solar_inverter();
builder.connect(mixed_meter, pv);
let battery_inverter = builder.inv_bat_chain(1);
builder.connect(mixed_meter, battery_inverter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([3, 4]))?,
vec![
super::resolve::Measurement::Single(3),
super::resolve::Measurement::Single(4)
],
);
Ok(())
}
#[test]
fn test_no_group_across_overlapping_diamonds() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let m_a = builder.meter();
let m_b = builder.meter();
let m_c = builder.meter();
builder.connect(grid_meter, m_a);
builder.connect(grid_meter, m_b);
builder.connect(grid_meter, m_c);
let pv1 = builder.solar_inverter();
let pv2 = builder.solar_inverter();
builder.connect(m_a, pv1);
builder.connect(m_b, pv1);
builder.connect(m_b, pv2);
builder.connect(m_c, pv2);
let graph = builder.build(None)?;
assert_eq!(
super::resolve::measurement_points(&graph, &BTreeSet::from([5, 6]))?,
vec![
super::resolve::Measurement::Single(5),
super::resolve::Measurement::Single(6)
],
);
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#5, 0.0) + COALESCE(#6, 0.0)",
);
Ok(())
}
#[test]
fn test_subtraction_diamond_subsume_order_independent() -> Result<(), Error> {
for shared_id_first in [false, true] {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let m_a = builder.meter();
let m_b = builder.meter();
builder.connect(grid_meter, m_a);
builder.connect(grid_meter, m_b);
let first = builder.solar_inverter();
let bat_inverter = builder.battery_inverter();
let battery = builder.battery();
let second = builder.solar_inverter();
let (pv_x, pv_shared) = if shared_id_first {
(second, first)
} else {
(first, second)
};
builder.connect(m_a, pv_x);
builder.connect(m_a, bat_inverter);
builder.connect(bat_inverter, battery);
builder.connect(m_a, pv_shared);
builder.connect(m_b, pv_shared);
let graph = builder.build(None)?;
let targets = BTreeSet::from([pv_x.component_id(), pv_shared.component_id()]);
assert_eq!(
super::resolve::measurement_points(&graph, &targets)?,
vec![super::resolve::Measurement::Subtraction {
parent_meters: vec![m_a.component_id(), m_b.component_id()],
subtracted: vec![bat_inverter.component_id()],
components: {
let mut components = vec![pv_x.component_id(), pv_shared.component_id()];
components.sort_unstable();
components
},
}],
"shared_id_first: {shared_id_first}",
);
}
Ok(())
}
#[test]
fn test_no_telemetry_component_measured_via_meter() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let pv_meter = builder.meter();
builder.connect(grid_meter, pv_meter);
let pv = builder.solar_inverter();
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
builder.connect(pv_meter, pv);
builder.connect(pv_meter, pv_no_telemetry);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#2, #3, 0.0)",);
assert!(graph.is_pv_meter(pv_meter.component_id())?);
assert!(graph.is_pv_chain(pv_no_telemetry.component_id())?);
Ok(())
}
#[test]
fn test_no_telemetry_children_meter_stays_total() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let pv_meter = builder.meter();
builder.connect(grid_meter, pv_meter);
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
builder.connect(pv_meter, pv_no_telemetry);
let other_meter = builder.meter();
builder.connect(grid_meter, other_meter);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#2, 0.0)");
Ok(())
}
#[test]
fn test_no_telemetry_meterless_component_dropped() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::Inactive,
);
builder.connect(grid, pv_no_telemetry);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "0.0");
Ok(())
}
#[test]
fn test_no_telemetry_in_diamond() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let m1 = builder.meter();
let m2 = builder.meter();
builder.connect(grid, m1);
builder.connect(grid, m2);
let pv = builder.solar_inverter();
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
for meter in [m1, m2] {
builder.connect(meter, pv);
builder.connect(meter, pv_no_telemetry);
}
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#1 + #2, COALESCE(#1, 0.0) + COALESCE(#2, 0.0))",
);
Ok(())
}
#[test]
fn test_no_telemetry_in_subtraction() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, grid_meter);
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
let sub_meter = builder.meter();
builder.connect(mixed_meter, pv);
builder.connect(mixed_meter, pv_no_telemetry);
builder.connect(mixed_meter, sub_meter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#2 - #5, #3, 0.0)",
);
Ok(())
}
#[test]
fn test_all_no_telemetry_in_subtraction() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, grid_meter);
builder.connect(grid_meter, mixed_meter);
let pv = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
let sub_meter = builder.meter();
builder.connect(mixed_meter, pv);
builder.connect(mixed_meter, pv_no_telemetry);
builder.connect(mixed_meter, sub_meter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#2 - #5, 0.0)",
);
Ok(())
}
#[test]
fn test_no_telemetry_disable_fallback() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let meter = builder.meter();
builder.connect(grid, meter);
let pv = builder.solar_inverter();
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
builder.connect(meter, pv);
builder.connect(meter, pv_no_telemetry);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.disable_fallback_components(true)
.build(),
))?;
assert_eq!(graph.pv_formula(None)?.to_string(), "#2");
Ok(())
}
#[test]
fn test_no_telemetry_disable_fallback_all_dropped() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let meter = builder.meter();
builder.connect(grid, meter);
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
builder.connect(meter, pv_no_telemetry);
let graph = builder.build(Some(
ComponentGraphConfig::builder()
.disable_fallback_components(true)
.build(),
))?;
assert_eq!(graph.pv_formula(None)?.to_string(), "0.0");
Ok(())
}
#[test]
fn test_no_telemetry_meter_dropped() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let pv_meter =
builder.add_component_with_mode(ComponentCategory::Meter, OperationalMode::Inactive);
builder.connect(grid_meter, pv_meter);
let pv = builder.solar_inverter();
builder.connect(pv_meter, pv);
let other_meter = builder.meter();
builder.connect(grid_meter, other_meter);
let graph = builder.build(None)?;
assert_eq!(graph.pv_formula(None)?.to_string(), "COALESCE(#3, 0.0)");
let expr = aggregate(
&graph,
BTreeSet::from([pv_meter.component_id()]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "COALESCE(#3, 0.0)");
Ok(())
}
#[test]
fn test_no_telemetry_sibling_disqualifies_subtraction() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
let mixed_meter = builder.meter();
builder.connect(grid, grid_meter);
builder.connect(grid_meter, mixed_meter);
let pv = builder.solar_inverter();
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
let sub_meter = builder.meter();
builder.connect(mixed_meter, pv);
builder.connect(mixed_meter, pv_no_telemetry);
builder.connect(mixed_meter, sub_meter);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([pv.component_id()]),
SourcePreference::ComponentsFirst,
)?;
assert_eq!(expr.to_string(), "COALESCE(#3, 0.0)");
Ok(())
}
#[test]
fn test_no_telemetry_sibling_keeps_feeder_meter() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let mixed_meter = builder.meter();
builder.connect(grid_meter, mixed_meter);
let feeder_meter = builder.meter();
builder.connect(mixed_meter, feeder_meter);
let pv_no_telemetry = builder.add_component_with_mode(
ComponentCategory::Inverter(InverterType::Pv),
OperationalMode::ControlOnly,
);
builder.connect(mixed_meter, pv_no_telemetry);
builder.connect(feeder_meter, pv_no_telemetry);
let load_meter = builder.meter();
builder.connect(grid_meter, load_meter);
let graph = builder.build(None)?;
let expr = aggregate(
&graph,
BTreeSet::from([mixed_meter.component_id()]),
SourcePreference::MetersFirst,
)?;
assert_eq!(expr.to_string(), "COALESCE(#2, #3)");
Ok(())
}
#[test]
fn test_no_telemetry_meter_sibling_blocks_group() -> Result<(), Error> {
let mut builder = ComponentGraphBuilder::new();
let grid = builder.grid();
let grid_meter = builder.meter();
builder.connect(grid, grid_meter);
let pv_meter = builder.meter();
builder.connect(grid_meter, pv_meter);
let pv = builder.solar_inverter();
builder.connect(pv_meter, pv);
let silent =
builder.add_component_with_mode(ComponentCategory::Meter, OperationalMode::Inactive);
builder.connect(pv_meter, silent);
let pv_below = builder.solar_inverter();
builder.connect(silent, pv_below);
let graph = builder.build(None)?;
assert_eq!(
graph.pv_formula(None)?.to_string(),
"COALESCE(#3, 0.0) + COALESCE(#5, 0.0)"
);
Ok(())
}