Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 5 additions & 11 deletions src/logical_meter/formula.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -25,27 +24,22 @@ 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;
async fn subscribe(&self) -> Result<broadcast::Receiver<Sample<Self::QuantityType>>, Error>;
}

/// Parameters for creating a logical meter formula.
pub(super) struct FormulaParams<F: GraphFormulaConnector, M: Metric> {
pub(super) formula: F::GraphFormulaType,
pub(super) struct FormulaParams<M: Metric> {
pub(super) formula: frequenz_microgrid_component_graph::Formula,
pub(super) instructions_tx: mpsc::Sender<logical_meter_actor::Instruction>,
phantom: PhantomData<M>,
}

impl<F: GraphFormulaConnector, M: Metric> FormulaParams<F, M> {
impl<M: Metric> FormulaParams<M> {
pub(super) fn new(
formula: F::GraphFormulaType,
formula: frequenz_microgrid_component_graph::Formula,
instructions_tx: mpsc::Sender<logical_meter_actor::Instruction>,
) -> Self {
Self {
Expand Down
75 changes: 0 additions & 75 deletions src/logical_meter/formula/aggregation_formula.rs

This file was deleted.

75 changes: 0 additions & 75 deletions src/logical_meter/formula/coalesce_formula.rs

This file was deleted.

94 changes: 94 additions & 0 deletions src/logical_meter/formula/graph_formula.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! 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;

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};

/// 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<M: Metric, K> {
formula: frequenz_microgrid_component_graph::Formula,
instructions_tx: mpsc::Sender<logical_meter_actor::Instruction>,
phantom: PhantomData<(M, K)>,
}

/// A formula that supports aggregation operations.
pub type AggregationFormula<M> = GraphFormula<M, Aggregation>;

/// A formula built from the component graph's coalesce methods.
pub type CoalesceFormula<M> = GraphFormula<M, Coalesce>;

// Manual `Clone`: a derive would demand `K: Clone`, but the marker types are
// uninhabited and carry no data.
impl<M: Metric, K> Clone for GraphFormula<M, K> {
fn clone(&self) -> Self {
Self {
formula: self.formula.clone(),
instructions_tx: self.instructions_tx.clone(),
phantom: PhantomData,
}
}
}

impl<M: Metric, K> std::fmt::Display for GraphFormula<M, K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}::({})", M::METRIC.as_str_name(), self.formula)
}
}

#[async_trait]
impl<Q: Quantity + 'static, M: Metric<QuantityType = Q> + Sync + Send, K: Sync + Send>
FormulaSubscriber for GraphFormula<M, K>
{
type QuantityType = Q;

async fn subscribe(&self) -> Result<broadcast::Receiver<Sample<Q>>, 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<M: Metric, K> From<FormulaParams<M>> for GraphFormula<M, K> {
fn from(params: FormulaParams<M>) -> Self {
Self {
formula: params.formula,
instructions_tx: params.instructions_tx,
phantom: PhantomData,
}
}
}
7 changes: 3 additions & 4 deletions src/logical_meter/formula/graph_formula_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -84,7 +83,7 @@ macro_rules! impl_graph_formula_provider {
)+};
}

impl<M: Metric> GraphFormulaProvider for AggregationFormula<M> {
impl<M: Metric> GraphFormulaProvider for GraphFormula<M, Aggregation> {
type MetricType = M;

impl_graph_formula_provider!(
Expand All @@ -99,7 +98,7 @@ impl<M: Metric> GraphFormulaProvider for AggregationFormula<M> {
);
}

impl<M: Metric> GraphFormulaProvider for CoalesceFormula<M> {
impl<M: Metric> GraphFormulaProvider for GraphFormula<M, Coalesce> {
type MetricType = M;

impl_graph_formula_provider!(
Expand Down
36 changes: 36 additions & 0 deletions src/logical_meter/logical_meter_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<f32>::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,
Expand Down
2 changes: 1 addition & 1 deletion src/logical_meter/logical_meter_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading