use std::collections::BTreeSet;
use crate::component_category::CategoryPredicates;
use crate::graph::formulas::expr::Expr;
use crate::{ComponentGraph, Edge, Error, Node};
use super::SourcePreference;
use super::predicates::{
ids_with_telemetry, is_grid_meter, reached_only_through, reaches_any_below,
};
pub(super) fn measure<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
id: u64,
policy: SourcePreference,
) -> Result<Expr, Error> {
let own = Expr::component(id);
let component = graph.component(id)?;
if !component.is_meter() {
if !component.provides_telemetry() {
return Ok(Expr::number(0.0));
}
return Ok(own.coalesce(Expr::number(0.0)));
}
let children: Vec<&N> = graph.successors(id)?.collect();
let mut contributing: Vec<&N> = Vec::new();
for child in children.iter().copied() {
if child.provides_telemetry()
|| (child.is_meter()
&& matches!(child_term_kind(graph, child)?, ChildTerm::Recurse)
&& graph.reaches_any(
child.component_id(),
|node| node.provides_telemetry(),
petgraph::Direction::Outgoing,
)?)
{
contributing.push(child);
}
}
let contributing_ids: BTreeSet<u64> = contributing.iter().map(|c| c.component_id()).collect();
let meters = BTreeSet::from([id]);
let mut kept: Vec<&N> = Vec::new();
for child in &contributing {
let child_id = child.component_id();
if !reaches_any_below(graph, child_id, &contributing_ids)?
&& reached_only_through(graph, child_id, &meters)?
{
kept.push(child);
}
}
if !component.provides_telemetry() {
if is_grid_meter(graph, component)? {
return Ok(Expr::None);
}
let terms = kept
.iter()
.map(|c| child_best_effort_term(graph, c, policy))
.collect::<Result<Vec<_>, _>>()?;
return Ok(sum(terms).unwrap_or_else(|| Expr::number(0.0)));
}
let standing = stands_alone(graph, id, policy)?;
if standing && is_grid_meter(graph, component)? {
return Ok(own);
}
if kept.is_empty() {
return Ok(if !children.is_empty() && contributing.is_empty() {
own.coalesce(Expr::number(0.0))
} else {
own
});
}
let empty = || Error::internal("Meter children sum is empty.");
let terms = kept
.iter()
.map(|c| child_best_effort_term(graph, c, policy))
.collect::<Result<Vec<_>, _>>()?;
let best = sum(terms).ok_or_else(empty)?;
if standing {
return Ok(own.coalesce(best));
}
if policy.meters_first() {
Ok(own.coalesce(best))
} else {
let exact =
sum(kept.iter().map(|c| Expr::component(c.component_id()))).ok_or_else(empty)?;
let last_resort = if kept.len() > 1 {
best
} else if kept[0].is_meter() {
Expr::None
} else {
Expr::number(0.0)
};
Ok(exact.coalesce(own).coalesce(last_resort))
}
}
fn exact_sum(ids: &[u64]) -> Option<Expr> {
sum(ids.iter().map(|&id| Expr::component(id)))
}
fn best_effort_sum(ids: &[u64]) -> Option<Expr> {
sum(ids
.iter()
.map(|&id| Expr::coalesce(Expr::component(id), Expr::number(0.0))))
}
pub(super) fn diamond_term<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
components: &[u64],
meters: &[u64],
policy: SourcePreference,
) -> Result<Expr, Error> {
let empty = || Error::internal("Diamond measurement with no meters or components.");
if components.is_empty() {
return Err(empty());
}
let meter_best = best_effort_sum(meters).ok_or_else(empty)?;
let all_report =
ids_with_telemetry(graph, components.iter().copied())?.len() == components.len();
if all_report {
let component_sum = exact_sum(components).ok_or_else(empty)?;
Ok(if policy.meters_first() {
let meter_sum = exact_sum(meters).ok_or_else(empty)?;
meter_sum.coalesce(component_sum).coalesce(meter_best)
} else {
component_sum.coalesce(meter_best)
})
} else {
Ok(exact_sum(meters).ok_or_else(empty)?.coalesce(meter_best))
}
}
pub(super) fn subtraction_term<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
parent_meters: &[u64],
subtracted: &[u64],
components: &[u64],
policy: SourcePreference,
) -> Result<Expr, Error> {
let empty = || Error::internal("Subtraction measurement with no components.");
let no_meters = || Error::internal("Subtraction measurement with no parent meters.");
if components.is_empty() {
return Err(empty());
}
let meter_sum = exact_sum(parent_meters).ok_or_else(no_meters)?;
let difference = subtracted
.iter()
.fold(meter_sum, |expr, &m| expr - Expr::component(m));
let telemetry_components = ids_with_telemetry(graph, components.iter().copied())?;
let all_report = telemetry_components.len() == components.len();
let best = best_effort_sum(&telemetry_components);
if policy.meters_first() || !all_report {
Ok(difference.coalesce(best.unwrap_or_else(|| Expr::number(0.0))))
} else {
let exact = exact_sum(&telemetry_components).ok_or_else(empty)?;
let best = best.ok_or_else(empty)?;
let last_resort = if components.len() > 1 {
best
} else {
Expr::number(0.0)
};
Ok(exact.coalesce(difference).coalesce(last_resort))
}
}
enum ChildTerm {
ReadingOr0,
Bare,
Recurse,
}
fn child_term_kind<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
child: &N,
) -> Result<ChildTerm, Error> {
if !child.is_meter() {
return Ok(ChildTerm::ReadingOr0);
}
let meter_id = child.component_id();
let meters = BTreeSet::from([meter_id]);
for successor in graph.successors(meter_id)? {
if !reached_only_through(graph, successor.component_id(), &meters)? {
return Ok(ChildTerm::Bare);
}
}
Ok(ChildTerm::Recurse)
}
fn child_best_effort_term<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
child: &N,
policy: SourcePreference,
) -> Result<Expr, Error> {
let id = child.component_id();
Ok(match child_term_kind(graph, child)? {
ChildTerm::ReadingOr0 => Expr::coalesce(Expr::component(id), Expr::number(0.0)),
ChildTerm::Bare => Expr::component(id),
ChildTerm::Recurse => measure(graph, id, policy)?,
})
}
pub(super) fn stands_alone<N: Node, E: Edge>(
graph: &ComponentGraph<N, E>,
id: u64,
policy: SourcePreference,
) -> Result<bool, Error> {
let successors: Vec<&N> = graph.successors(id)?.collect();
if successors.is_empty() {
return Ok(true);
}
if successors.iter().any(|s| !s.provides_telemetry()) {
return Ok(true);
}
if !successors.iter().any(|successor| successor.is_meter()) {
return is_grid_meter(graph, graph.component(id)?);
}
if !policy.allows_meter_chains() || successors.len() > 1 {
return Ok(true);
}
graph.is_component_meter(successors[0].component_id())
}
pub(super) fn sum(exprs: impl IntoIterator<Item = Expr>) -> Option<Expr> {
exprs.into_iter().reduce(|a, b| a + b)
}