From 69e2f67d5f5635817448509c8c397a9c7e8c590b Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Wed, 24 Jun 2026 19:34:55 +0000 Subject: [PATCH 1/3] Adopt the component-graph meter-subtraction branch Point the frequenz-microgrid-component-graph dependency at the meter-subtraction branch of the fork and port the logical-meter formula layer to the unified component-graph Formula type: the AggregationFormula and CoalesceFormula graph types and the Formula trait are gone, replaced by a single Formula struct, so FormulaParams and the formula wrappers hold it directly and the GraphFormulaConnector indirection is dropped. Update the pv-pool and logical-meter formula-display tests for the new component-graph behavior: per-category formulas are component-first by default (the exact sum leads), and a component sharing a meter with a sibling now falls back to the meter minus that sibling. Signed-off-by: Sahas Subramanian --- Cargo.toml | 2 +- src/logical_meter/formula.rs | 13 ++++--------- src/logical_meter/formula/aggregation_formula.rs | 14 +++++--------- src/logical_meter/formula/coalesce_formula.rs | 14 +++++--------- src/logical_meter/logical_meter_handle.rs | 2 +- src/microgrid/pv_pool.rs | 4 ++-- 6 files changed, 18 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fcae139..b972800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ path = "src/lib.rs" [dependencies] async-trait = "0.1.89" chrono = "0.4" -frequenz-microgrid-component-graph = "0.5.0" +frequenz-microgrid-component-graph = { git = "https://github.com/shsms/frequenz-microgrid-component-graph-rs", branch = "meter-subtraction" } frequenz-microgrid-formula-engine = "0.1.0" frequenz-resampling = "0.2" futures = "0.3.31" diff --git a/src/logical_meter/formula.rs b/src/logical_meter/formula.rs index 217e7cd..f3b41a0 100644 --- a/src/logical_meter/formula.rs +++ b/src/logical_meter/formula.rs @@ -25,11 +25,6 @@ use tokio::sync::{ use super::logical_meter_actor; -/// Connects logical meter formulas to the component graph formulas. -pub(crate) trait GraphFormulaConnector: std::fmt::Display { - type GraphFormulaType: frequenz_microgrid_component_graph::Formula; -} - #[async_trait] pub trait FormulaSubscriber: std::fmt::Display + Sync + Send { type QuantityType: Quantity; @@ -37,15 +32,15 @@ pub trait FormulaSubscriber: std::fmt::Display + Sync + Send { } /// Parameters for creating a logical meter formula. -pub(super) struct FormulaParams { - pub(super) formula: F::GraphFormulaType, +pub(super) struct FormulaParams { + pub(super) formula: frequenz_microgrid_component_graph::Formula, pub(super) instructions_tx: mpsc::Sender, phantom: PhantomData, } -impl FormulaParams { +impl FormulaParams { pub(super) fn new( - formula: F::GraphFormulaType, + formula: frequenz_microgrid_component_graph::Formula, instructions_tx: mpsc::Sender, ) -> Self { Self { diff --git a/src/logical_meter/formula/aggregation_formula.rs b/src/logical_meter/formula/aggregation_formula.rs index 25d10ec..886d3da 100644 --- a/src/logical_meter/formula/aggregation_formula.rs +++ b/src/logical_meter/formula/aggregation_formula.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; -use super::{FormulaParams, FormulaSubscriber, GraphFormulaConnector}; +use super::{FormulaParams, FormulaSubscriber}; use crate::{ Error, Sample, logical_meter::logical_meter_actor, metric::Metric, quantity::Quantity, }; @@ -14,7 +14,7 @@ use tokio::sync::{broadcast, mpsc, oneshot}; #[derive(Clone)] pub struct AggregationFormula { - formula: frequenz_microgrid_component_graph::AggregationFormula, + formula: frequenz_microgrid_component_graph::Formula, instructions_tx: mpsc::Sender, phantom: PhantomData, } @@ -25,10 +25,6 @@ impl std::fmt::Display for AggregationFormula { } } -impl GraphFormulaConnector for AggregationFormula { - type GraphFormulaType = frequenz_microgrid_component_graph::AggregationFormula; -} - #[async_trait] impl + Sync + Send> FormulaSubscriber for AggregationFormula @@ -54,8 +50,8 @@ impl + Sync + Send> FormulaSu } } -impl From, M>> for AggregationFormula { - fn from(params: FormulaParams, M>) -> Self { +impl From> for AggregationFormula { + fn from(params: FormulaParams) -> Self { Self { formula: params.formula, instructions_tx: params.instructions_tx, @@ -64,7 +60,7 @@ impl From, M>> for AggregationFor } } -impl From> for FormulaParams, M> { +impl From> for FormulaParams { fn from(formula: AggregationFormula) -> Self { FormulaParams { formula: formula.formula, diff --git a/src/logical_meter/formula/coalesce_formula.rs b/src/logical_meter/formula/coalesce_formula.rs index ee70aaf..2beba9b 100644 --- a/src/logical_meter/formula/coalesce_formula.rs +++ b/src/logical_meter/formula/coalesce_formula.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; -use super::{FormulaParams, FormulaSubscriber, GraphFormulaConnector}; +use super::{FormulaParams, FormulaSubscriber}; use crate::{ Error, Sample, logical_meter::logical_meter_actor, metric::Metric, quantity::Quantity, }; @@ -14,7 +14,7 @@ use tokio::sync::{broadcast, mpsc, oneshot}; #[derive(Clone)] pub struct CoalesceFormula { - formula: frequenz_microgrid_component_graph::CoalesceFormula, + formula: frequenz_microgrid_component_graph::Formula, instructions_tx: mpsc::Sender, phantom: PhantomData, } @@ -25,10 +25,6 @@ impl std::fmt::Display for CoalesceFormula { } } -impl GraphFormulaConnector for CoalesceFormula { - type GraphFormulaType = frequenz_microgrid_component_graph::CoalesceFormula; -} - #[async_trait] impl + Sync + Send> FormulaSubscriber for CoalesceFormula @@ -54,8 +50,8 @@ impl + Sync + Send> FormulaSu } } -impl From, M>> for CoalesceFormula { - fn from(params: FormulaParams, M>) -> Self { +impl From> for CoalesceFormula { + fn from(params: FormulaParams) -> Self { Self { formula: params.formula, instructions_tx: params.instructions_tx, @@ -64,7 +60,7 @@ impl From, M>> for CoalesceFormula From> for FormulaParams, M> { +impl From> for FormulaParams { fn from(formula: CoalesceFormula) -> Self { FormulaParams { formula: formula.formula, diff --git a/src/logical_meter/logical_meter_handle.rs b/src/logical_meter/logical_meter_handle.rs index 139c096..bb813cc 100644 --- a/src/logical_meter/logical_meter_handle.rs +++ b/src/logical_meter/logical_meter_handle.rs @@ -303,7 +303,7 @@ mod tests { .unwrap(); assert_eq!( formula.to_string(), - "METRIC_AC_POWER_ACTIVE::(COALESCE(#8, 0.0))" + "METRIC_AC_POWER_ACTIVE::(COALESCE(#8, #5 - #6, 0.0))" ); let formula = lm diff --git a/src/microgrid/pv_pool.rs b/src/microgrid/pv_pool.rs index 8d59f4f..7b79488 100644 --- a/src/microgrid/pv_pool.rs +++ b/src/microgrid/pv_pool.rs @@ -265,7 +265,7 @@ mod tests { let formula = pool.power().unwrap(); assert_eq!( formula.to_string(), - "METRIC_AC_POWER_ACTIVE::(COALESCE(#3, COALESCE(#5, 0.0) + COALESCE(#4, 0.0)))" + "METRIC_AC_POWER_ACTIVE::(COALESCE(#5 + #4, #3, COALESCE(#5, 0.0) + COALESCE(#4, 0.0)))" ); } @@ -276,7 +276,7 @@ mod tests { let formula = pool.power().unwrap(); assert_eq!( formula.to_string(), - "METRIC_AC_POWER_ACTIVE::(COALESCE(#3, COALESCE(#5, 0.0) + COALESCE(#4, 0.0)))" + "METRIC_AC_POWER_ACTIVE::(COALESCE(#5 + #4, #3, COALESCE(#5, 0.0) + COALESCE(#4, 0.0)))" ); } } From d4386d8d76d3c18b088b918692106d27f6d75546 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Wed, 24 Jun 2026 20:14:34 +0000 Subject: [PATCH 2/3] Unify the formula wrappers behind a marker type After the port to the unified component-graph Formula, AggregationFormula and CoalesceFormula had byte-identical bodies (field, Display, FormulaSubscriber, From); only their GraphFormulaProvider impls differed. Collapse them into one GraphFormula keyed by an Aggregation/Coalesce marker, keep the two provider impls, and alias the old names. The dead reverse From for FormulaParams impls are dropped. Signed-off-by: Sahas Subramanian --- src/logical_meter/formula.rs | 3 +- src/logical_meter/formula/coalesce_formula.rs | 71 ------------------- ...ggregation_formula.rs => graph_formula.rs} | 59 ++++++++++----- .../formula/graph_formula_provider.rs | 7 +- src/metric.rs | 3 +- 5 files changed, 46 insertions(+), 97 deletions(-) delete mode 100644 src/logical_meter/formula/coalesce_formula.rs rename src/logical_meter/formula/{aggregation_formula.rs => graph_formula.rs} (50%) diff --git a/src/logical_meter/formula.rs b/src/logical_meter/formula.rs index f3b41a0..0ba326a 100644 --- a/src/logical_meter/formula.rs +++ b/src/logical_meter/formula.rs @@ -6,9 +6,8 @@ use std::marker::PhantomData; use async_trait::async_trait; -pub(crate) mod aggregation_formula; mod async_formula; -pub(crate) mod coalesce_formula; +pub(crate) mod graph_formula; pub(crate) mod graph_formula_provider; pub use async_formula::Formula; diff --git a/src/logical_meter/formula/coalesce_formula.rs b/src/logical_meter/formula/coalesce_formula.rs deleted file mode 100644 index 2beba9b..0000000 --- a/src/logical_meter/formula/coalesce_formula.rs +++ /dev/null @@ -1,71 +0,0 @@ -// License: MIT -// Copyright © 2025 Frequenz Energy-as-a-Service GmbH - -//! An coalesce formula. - -use std::marker::PhantomData; - -use super::{FormulaParams, FormulaSubscriber}; -use crate::{ - Error, Sample, logical_meter::logical_meter_actor, metric::Metric, quantity::Quantity, -}; -use async_trait::async_trait; -use tokio::sync::{broadcast, mpsc, oneshot}; - -#[derive(Clone)] -pub struct CoalesceFormula { - formula: frequenz_microgrid_component_graph::Formula, - instructions_tx: mpsc::Sender, - phantom: PhantomData, -} - -impl std::fmt::Display for CoalesceFormula { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}::({})", M::METRIC.as_str_name(), self.formula) - } -} - -#[async_trait] -impl + Sync + Send> FormulaSubscriber - for CoalesceFormula -{ - type QuantityType = Q; - - async fn subscribe(&self) -> Result>, Error> { - let (tx, rx) = oneshot::channel(); - - self.instructions_tx - .send(logical_meter_actor::Instruction::SubscribeFormula { - formula: self.formula.to_string(), - metric: M::METRIC, - response_tx: tx.try_into()?, - }) - .await - .map_err(|e| Error::connection_failure(format!("Could not send instruction: {e}")))?; - let receiver = rx.await.map_err(|e| { - Error::connection_failure(format!("Could not receive instruction: {e}")) - })?; - - Ok(receiver) - } -} - -impl From> for CoalesceFormula { - fn from(params: FormulaParams) -> Self { - Self { - formula: params.formula, - instructions_tx: params.instructions_tx, - phantom: PhantomData, - } - } -} - -impl From> for FormulaParams { - fn from(formula: CoalesceFormula) -> Self { - FormulaParams { - formula: formula.formula, - phantom: formula.phantom, - instructions_tx: formula.instructions_tx, - } - } -} diff --git a/src/logical_meter/formula/aggregation_formula.rs b/src/logical_meter/formula/graph_formula.rs similarity index 50% rename from src/logical_meter/formula/aggregation_formula.rs rename to src/logical_meter/formula/graph_formula.rs index 886d3da..9116f6a 100644 --- a/src/logical_meter/formula/aggregation_formula.rs +++ b/src/logical_meter/formula/graph_formula.rs @@ -1,7 +1,15 @@ // License: MIT // Copyright © 2025 Frequenz Energy-as-a-Service GmbH -//! An formula that supports aggregation operations. +//! A formula generated from the component graph, subscribable through the +//! logical-meter actor. +//! +//! The wrapper body is identical for every formula; the marker type `K` only +//! selects which component-graph methods generate it (the aggregation +//! `*_formula` methods vs. the `*_coalesce_formula` ones), via the +//! [`GraphFormulaProvider`](super::graph_formula_provider::GraphFormulaProvider) +//! impls. The [`AggregationFormula`] / [`CoalesceFormula`] aliases name the two +//! kinds. use std::marker::PhantomData; @@ -12,22 +20,47 @@ use crate::{ use async_trait::async_trait; use tokio::sync::{broadcast, mpsc, oneshot}; -#[derive(Clone)] -pub struct AggregationFormula { +/// Marker for formulas from the aggregation (`*_formula`) graph methods. +pub enum Aggregation {} + +/// Marker for formulas from the coalesce (`*_coalesce_formula`) graph methods. +pub enum Coalesce {} + +/// A component-graph formula for metric `M`, tagged by the kind `K` that +/// selects the graph methods generating it. +pub struct GraphFormula { formula: frequenz_microgrid_component_graph::Formula, instructions_tx: mpsc::Sender, - phantom: PhantomData, + phantom: PhantomData<(M, K)>, } -impl std::fmt::Display for AggregationFormula { +/// A formula that supports aggregation operations. +pub type AggregationFormula = GraphFormula; + +/// A formula built from the component graph's coalesce methods. +pub type CoalesceFormula = GraphFormula; + +// Manual `Clone`: a derive would demand `K: Clone`, but the marker types are +// uninhabited and carry no data. +impl Clone for GraphFormula { + fn clone(&self) -> Self { + Self { + formula: self.formula.clone(), + instructions_tx: self.instructions_tx.clone(), + phantom: PhantomData, + } + } +} + +impl std::fmt::Display for GraphFormula { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}::({})", M::METRIC.as_str_name(), self.formula) } } #[async_trait] -impl + Sync + Send> FormulaSubscriber - for AggregationFormula +impl + Sync + Send, K: Sync + Send> + FormulaSubscriber for GraphFormula { type QuantityType = Q; @@ -50,7 +83,7 @@ impl + Sync + Send> FormulaSu } } -impl From> for AggregationFormula { +impl From> for GraphFormula { fn from(params: FormulaParams) -> Self { Self { formula: params.formula, @@ -59,13 +92,3 @@ impl From> for AggregationFormula { } } } - -impl From> for FormulaParams { - fn from(formula: AggregationFormula) -> Self { - FormulaParams { - formula: formula.formula, - instructions_tx: formula.instructions_tx, - phantom: PhantomData, - } - } -} diff --git a/src/logical_meter/formula/graph_formula_provider.rs b/src/logical_meter/formula/graph_formula_provider.rs index 5fdcbfd..8847e1c 100644 --- a/src/logical_meter/formula/graph_formula_provider.rs +++ b/src/logical_meter/formula/graph_formula_provider.rs @@ -8,8 +8,7 @@ use crate::client::proto::common::microgrid::electrical_components::{ ElectricalComponent, ElectricalComponentConnection, }; use crate::logical_meter::formula::FormulaParams; -use crate::logical_meter::formula::aggregation_formula::AggregationFormula; -use crate::logical_meter::formula::coalesce_formula::CoalesceFormula; +use crate::logical_meter::formula::graph_formula::{Aggregation, Coalesce, GraphFormula}; use crate::logical_meter::logical_meter_actor; use crate::metric::Metric; @@ -84,7 +83,7 @@ macro_rules! impl_graph_formula_provider { )+}; } -impl GraphFormulaProvider for AggregationFormula { +impl GraphFormulaProvider for GraphFormula { type MetricType = M; impl_graph_formula_provider!( @@ -99,7 +98,7 @@ impl GraphFormulaProvider for AggregationFormula { ); } -impl GraphFormulaProvider for CoalesceFormula { +impl GraphFormulaProvider for GraphFormula { type MetricType = M; impl_graph_formula_provider!( diff --git a/src/metric.rs b/src/metric.rs index 2d32b6b..13f8a4f 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -3,8 +3,7 @@ //! Metrics supported by the logical meter. -use crate::logical_meter::formula::aggregation_formula::AggregationFormula; -use crate::logical_meter::formula::coalesce_formula::CoalesceFormula; +use crate::logical_meter::formula::graph_formula::{AggregationFormula, CoalesceFormula}; use crate::{ client::proto::common::metrics::Metric as MetricPb, logical_meter::formula, logical_meter::formula::FormulaSubscriber, From 77fef192b7c5c2a2a93df093c34a93d369181b8c Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Wed, 24 Jun 2026 20:14:34 +0000 Subject: [PATCH 3/3] Cover subtraction-formula evaluation through the engine The new component-graph meter-minus-sibling formulas (e.g. COALESCE(#8, #5 - #6, 0.0)) were only checked by display-string assertions; add a numeric round-trip through FormulaEngine. Signed-off-by: Sahas Subramanian --- src/logical_meter/logical_meter_actor.rs | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/logical_meter/logical_meter_actor.rs b/src/logical_meter/logical_meter_actor.rs index df3379b..ac3a51f 100644 --- a/src/logical_meter/logical_meter_actor.rs +++ b/src/logical_meter/logical_meter_actor.rs @@ -639,6 +639,42 @@ mod tests { quantity::Power, }; + /// The component-graph meter-minus-sibling subtraction formulas the actor + /// now receives (e.g. `COALESCE(#8, #5 - #6, 0.0)`) parse and evaluate + /// through the formula engine: the component's own reading wins, else the + /// parent meter minus its sibling, else zero. + #[test] + fn subtraction_formula_evaluates_through_engine() { + let engine = FormulaEngine::::try_new("COALESCE(#8, #5 - #6, 0.0)") + .expect("formula should parse"); + + // Own reading present: used directly. + assert_eq!( + engine + .calculate(&HashMap::from([ + (8, Some(7.0)), + (5, Some(10.0)), + (6, Some(3.0)) + ])) + .unwrap(), + Some(7.0), + ); + // Own reading missing: parent meter minus its sibling (10 - 3). + assert_eq!( + engine + .calculate(&HashMap::from([(8, None), (5, Some(10.0)), (6, Some(3.0))])) + .unwrap(), + Some(7.0), + ); + // Own reading and meter missing: the difference is null, so zero wins. + assert_eq!( + engine + .calculate(&HashMap::from([(8, None), (5, None), (6, Some(3.0))])) + .unwrap(), + Some(0.0), + ); + } + async fn new_handle( meter: MockComponent, config: LogicalMeterConfig,