Skip to main content

frequenz_microgrid_component_graph/graph/
formulas.rs

1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4//! Methods for building formulas for various microgrid metrics.
5
6use std::collections::BTreeSet;
7
8use crate::ComponentGraph;
9use crate::Edge;
10use crate::Error;
11use crate::Node;
12use crate::component_category::CategoryPredicates;
13
14mod expr;
15mod fallback;
16mod formula;
17mod generators;
18mod traversal;
19
20use expr::Expr;
21pub use formula::Formula;
22
23/// Formulas for various microgrid metrics.
24impl<N, E> ComponentGraph<N, E>
25where
26    N: Node,
27    E: Edge,
28{
29    /// Returns the consumer formula for the graph.
30    pub fn consumer_formula(&self) -> Result<Formula, Error> {
31        generators::consumer::ConsumerFormulaBuilder::try_new(self)?.build()
32    }
33
34    /// Returns the grid formula for the graph.
35    pub fn grid_formula(&self) -> Result<Formula, Error> {
36        generators::grid::GridFormulaBuilder::try_new(self)?.build()
37    }
38
39    /// Returns the producer formula for the graph.
40    pub fn producer_formula(&self) -> Result<Formula, Error> {
41        generators::producer::ProducerFormulaBuilder::try_new(self)?.build()
42    }
43
44    /// Returns the battery formula with the given battery IDs.
45    ///
46    /// If `battery_ids` is `None`, the formula will contain all batteries in
47    /// the graph.
48    pub fn battery_formula(&self, battery_ids: Option<BTreeSet<u64>>) -> Result<Formula, Error> {
49        generators::battery::BatteryFormulaBuilder::try_new(self, battery_ids)?.build()
50    }
51
52    /// Returns the CHP formula for the graph.
53    pub fn chp_formula(&self, chp_ids: Option<BTreeSet<u64>>) -> Result<Formula, Error> {
54        generators::category::category_formula(
55            self,
56            chp_ids,
57            |node| node.is_chp(),
58            "a CHP",
59            self.config.prefer_meters_in_chp_formula(),
60        )
61    }
62
63    /// Returns the PV formula for the graph.
64    pub fn pv_formula(&self, pv_inverter_ids: Option<BTreeSet<u64>>) -> Result<Formula, Error> {
65        generators::category::category_formula(
66            self,
67            pv_inverter_ids,
68            |node| node.is_pv_inverter(),
69            "a PV inverter",
70            self.config.prefer_meters_in_pv_formula(),
71        )
72    }
73
74    /// Returns the wind_turbine formula for the graph.
75    pub fn wind_turbine_formula(
76        &self,
77        wind_turbine_ids: Option<BTreeSet<u64>>,
78    ) -> Result<Formula, Error> {
79        generators::category::category_formula(
80            self,
81            wind_turbine_ids,
82            |node| node.is_wind_turbine(),
83            "a wind turbine",
84            self.config.prefer_meters_in_wind_turbine_formula(),
85        )
86    }
87
88    /// Returns the EV charger formula for the graph.
89    pub fn ev_charger_formula(
90        &self,
91        ev_charger_ids: Option<BTreeSet<u64>>,
92    ) -> Result<Formula, Error> {
93        generators::category::category_formula(
94            self,
95            ev_charger_ids,
96            |node| node.is_ev_charger(),
97            "an EV charger",
98            self.config.prefer_meters_in_ev_charger_formula(),
99        )
100    }
101
102    /// Returns the formula for a specific component by its ID.
103    pub fn component_formula(&self, component_id: u64) -> Result<Formula, Error> {
104        Ok(Expr::component(component_id).into())
105    }
106
107    /// Returns the grid coalesce formula for the graph.
108    ///
109    /// This formula is used for non-aggregating metrics like AC voltage or
110    /// frequency.
111    ///
112    /// The formula is a `COALESCE` expression that includes all meters,
113    /// PV inverters, and battery inverters that are directly connected to the
114    /// grid.
115    pub fn grid_coalesce_formula(&self) -> Result<Formula, Error> {
116        generators::grid_coalesce::GridCoalesceFormulaBuilder::try_new(self)?.build()
117    }
118
119    /// Returns the battery AC coalesce formula for the given components.
120    ///
121    /// This formula is used for non-aggregating metrics like AC voltage or
122    /// frequency.
123    ///
124    /// The formula is a `COALESCE` expression that includes all the specified
125    /// battery meters and corresponding inverters.
126    ///
127    /// When the `battery_ids` parameter is `None`, it will include all the
128    /// battery meters and inverters in the graph.
129    pub fn battery_ac_coalesce_formula(
130        &self,
131        battery_ids: Option<BTreeSet<u64>>,
132    ) -> Result<Formula, Error> {
133        generators::battery_ac_coalesce::BatteryAcCoalesceFormulaBuilder::try_new(
134            self,
135            battery_ids,
136        )?
137        .build()
138    }
139
140    /// Returns the PV AC coalesce formula for the given components.
141    ///
142    /// This formula is used for non-aggregating metrics like AC voltage or
143    /// frequency.
144    ///
145    /// The formula is a `COALESCE` expression that includes all the specified
146    /// PV meters and corresponding inverters.
147    ///
148    /// When the `pv_inverter_ids` parameter is `None`, it will include all the
149    /// PV meters and inverters in the graph.
150    pub fn pv_ac_coalesce_formula(
151        &self,
152        pv_inverter_ids: Option<BTreeSet<u64>>,
153    ) -> Result<Formula, Error> {
154        generators::pv_ac_coalesce::PVAcCoalesceFormulaBuilder::try_new(self, pv_inverter_ids)?
155            .build()
156    }
157
158    /// Returns the AC coalesce formula for a specific component by its ID.
159    pub fn component_ac_coalesce_formula(&self, component_id: u64) -> Result<Formula, Error> {
160        Ok(Expr::component(component_id).into())
161    }
162
163    /// Returns the steam boiler formula for the graph.
164    pub fn steam_boiler_formula(
165        &self,
166        steam_boiler_ids: Option<BTreeSet<u64>>,
167    ) -> Result<Formula, Error> {
168        generators::category::category_formula(
169            self,
170            steam_boiler_ids,
171            |node| node.is_steam_boiler(),
172            "a steam boiler",
173            self.config.prefer_meters_in_steam_boiler_formula(),
174        )
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use crate::{Error, graph::test_utils::ComponentGraphBuilder};
181
182    /// `component_formula` and `component_ac_coalesce_formula` return the bare
183    /// reading of the requested component — no meter fallback, even when the
184    /// component sits behind a meter the category formulas would drill into.
185    #[test]
186    fn test_component_formula() -> Result<(), Error> {
187        let mut builder = ComponentGraphBuilder::new();
188        let grid = builder.grid();
189        let meter = builder.meter();
190        let inverter = builder.battery_inverter();
191        let battery = builder.battery();
192        builder.connect(grid, meter);
193        builder.connect(meter, inverter);
194        builder.connect(inverter, battery);
195
196        let graph = builder.build(None)?;
197        let inv = inverter.component_id();
198
199        assert_eq!(graph.component_formula(inv)?.to_string(), format!("#{inv}"));
200        assert_eq!(
201            graph.component_ac_coalesce_formula(inv)?.to_string(),
202            format!("#{inv}")
203        );
204        Ok(())
205    }
206}