frequenz_microgrid_component_graph/graph/
meter_roles.rs

1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4//! Methods for checking the roles of meters in a [`ComponentGraph`].
5
6use crate::{component_category::CategoryPredicates, ComponentGraph, Edge, Error, Node};
7
8/// Meter role identification.
9impl<N, E> ComponentGraph<N, E>
10where
11    N: Node,
12    E: Edge,
13{
14    /// Returns true if the node is a PV meter.
15    ///
16    /// A meter is identified as a PV meter if:
17    ///   - it has atleast one successor,
18    ///   - all its successors are PV inverters.
19    pub fn is_pv_meter(&self, component_id: u64) -> Result<bool, Error> {
20        let mut has_successors = false;
21        Ok(self.component(component_id)?.is_meter()
22            && self.successors(component_id)?.all(|n| {
23                has_successors = true;
24                n.is_pv_inverter()
25            })
26            && has_successors)
27    }
28
29    /// Returns true if the node is a battery meter.
30    ///
31    /// A meter is identified as a battery meter if
32    ///   - it has atleast one successor,
33    ///   - all its successors are battery inverters.
34    pub fn is_battery_meter(&self, component_id: u64) -> Result<bool, Error> {
35        let mut has_successors = false;
36        Ok(self.component(component_id)?.is_meter()
37            && self.successors(component_id)?.all(|n| {
38                has_successors = true;
39                n.is_battery_inverter(&self.config)
40            })
41            && has_successors)
42    }
43
44    /// Returns true if the node is an EV charger meter.
45    ///
46    /// A meter is identified as an EV charger meter if
47    ///   - it has atleast one successor,
48    ///   - all its successors are EV chargers.
49    pub fn is_ev_charger_meter(&self, component_id: u64) -> Result<bool, Error> {
50        let mut has_successors = false;
51        Ok(self.component(component_id)?.is_meter()
52            && self.successors(component_id)?.all(|n| {
53                has_successors = true;
54                n.is_ev_charger()
55            })
56            && has_successors)
57    }
58
59    /// Returns true if the node is a CHP meter.
60    ///
61    /// A meter is identified as a CHP meter if
62    ///   - has atleast one successor,
63    ///   - all its successors are CHPs.
64    pub fn is_chp_meter(&self, component_id: u64) -> Result<bool, Error> {
65        let mut has_successors = false;
66        Ok(self.component(component_id)?.is_meter()
67            && self.successors(component_id)?.all(|n| {
68                has_successors = true;
69                n.is_chp()
70            })
71            && has_successors)
72    }
73
74    /// Returns true if the node is a component meter.
75    ///
76    /// A meter is a component meter if it is one of the following:
77    ///  - a PV meter,
78    ///  - a battery meter,
79    ///  - an EV charger meter,
80    ///  - a CHP meter.
81    pub fn is_component_meter(&self, component_id: u64) -> Result<bool, Error> {
82        Ok(self.is_pv_meter(component_id)?
83            || self.is_battery_meter(component_id)?
84            || self.is_ev_charger_meter(component_id)?
85            || self.is_chp_meter(component_id)?)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::component_category::BatteryType;
93    use crate::component_category::EvChargerType;
94    use crate::error::Error;
95    use crate::graph::test_utils::{TestComponent, TestConnection};
96    use crate::ComponentCategory;
97    use crate::ComponentGraphConfig;
98    use crate::InverterType;
99
100    fn nodes_and_edges() -> (Vec<TestComponent>, Vec<TestConnection>) {
101        let components = vec![
102            TestComponent::new(1, ComponentCategory::GridConnectionPoint),
103            TestComponent::new(2, ComponentCategory::Meter),
104            TestComponent::new(3, ComponentCategory::Meter),
105            TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery)),
106            TestComponent::new(5, ComponentCategory::Battery(BatteryType::NaIon)),
107            TestComponent::new(6, ComponentCategory::Meter),
108            TestComponent::new(7, ComponentCategory::Inverter(InverterType::Battery)),
109            TestComponent::new(8, ComponentCategory::Battery(BatteryType::Unspecified)),
110            TestComponent::new(9, ComponentCategory::Meter),
111            TestComponent::new(10, ComponentCategory::Inverter(InverterType::Pv)),
112            TestComponent::new(11, ComponentCategory::Inverter(InverterType::Pv)),
113            TestComponent::new(12, ComponentCategory::Meter),
114            TestComponent::new(13, ComponentCategory::Chp),
115            TestComponent::new(14, ComponentCategory::Meter),
116            TestComponent::new(15, ComponentCategory::Chp),
117            TestComponent::new(16, ComponentCategory::Inverter(InverterType::Pv)),
118            TestComponent::new(17, ComponentCategory::Inverter(InverterType::Battery)),
119            TestComponent::new(18, ComponentCategory::Battery(BatteryType::LiIon)),
120        ];
121        let connections = vec![
122            // Single Grid meter
123            TestConnection::new(1, 2),
124            // Battery chain
125            TestConnection::new(2, 3),
126            TestConnection::new(3, 4),
127            TestConnection::new(4, 5),
128            // Battery chain
129            TestConnection::new(2, 6),
130            TestConnection::new(6, 7),
131            TestConnection::new(7, 8),
132            // Solar chain
133            TestConnection::new(2, 9),
134            TestConnection::new(9, 10),
135            TestConnection::new(9, 11),
136            // CHP chain
137            TestConnection::new(2, 12),
138            TestConnection::new(12, 13),
139            // Mixed chain
140            TestConnection::new(2, 14),
141            TestConnection::new(14, 15),
142            TestConnection::new(14, 16),
143            TestConnection::new(14, 17),
144            TestConnection::new(17, 18),
145        ];
146
147        (components, connections)
148    }
149
150    fn with_multiple_grid_meters() -> (Vec<TestComponent>, Vec<TestConnection>) {
151        let (mut components, mut connections) = nodes_and_edges();
152
153        // Add a meter to the grid without successors
154        components.push(TestComponent::new(19, ComponentCategory::Meter));
155        connections.push(TestConnection::new(1, 19));
156
157        // Add a meter to the grid that has a battery meter and a PV meter as
158        // successors.
159        components.push(TestComponent::new(20, ComponentCategory::Meter));
160        connections.push(TestConnection::new(1, 20));
161
162        // battery chain
163        components.push(TestComponent::new(21, ComponentCategory::Meter));
164        components.push(TestComponent::new(
165            22,
166            ComponentCategory::Inverter(InverterType::Battery),
167        ));
168        components.push(TestComponent::new(
169            23,
170            ComponentCategory::Battery(BatteryType::Unspecified),
171        ));
172        connections.push(TestConnection::new(20, 21));
173        connections.push(TestConnection::new(21, 22));
174        connections.push(TestConnection::new(22, 23));
175
176        // pv chain
177        components.push(TestComponent::new(24, ComponentCategory::Meter));
178        components.push(TestComponent::new(
179            25,
180            ComponentCategory::Inverter(InverterType::Pv),
181        ));
182        connections.push(TestConnection::new(20, 24));
183        connections.push(TestConnection::new(24, 25));
184
185        (components, connections)
186    }
187
188    fn without_grid_meters() -> (Vec<TestComponent>, Vec<TestConnection>) {
189        let (mut components, mut connections) = nodes_and_edges();
190
191        // Add an EV charger meter to the grid, then none of the meters
192        // connected to the grid should be detected as grid meters.
193        components.push(TestComponent::new(20, ComponentCategory::Meter));
194        components.push(TestComponent::new(
195            21,
196            ComponentCategory::EvCharger(EvChargerType::Ac),
197        ));
198        connections.push(TestConnection::new(1, 20));
199        connections.push(TestConnection::new(20, 21));
200
201        (components, connections)
202    }
203
204    fn find_matching_components(
205        components: Vec<TestComponent>,
206        connections: Vec<TestConnection>,
207        filter: impl Fn(&ComponentGraph<TestComponent, TestConnection>, u64) -> Result<bool, Error>,
208    ) -> Result<Vec<u64>, Error> {
209        let config = ComponentGraphConfig::default();
210
211        let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?;
212
213        let mut found_meters = vec![];
214        for comp in graph.components() {
215            if filter(&graph, comp.component_id())? {
216                found_meters.push(comp.component_id());
217            }
218        }
219
220        Ok(found_meters)
221    }
222
223    #[test]
224    fn test_is_pv_meter() -> Result<(), Error> {
225        let (components, connections) = nodes_and_edges();
226        assert_eq!(
227            find_matching_components(components, connections, ComponentGraph::is_pv_meter)?,
228            vec![9],
229        );
230
231        let (components, connections) = with_multiple_grid_meters();
232        assert_eq!(
233            find_matching_components(components, connections, ComponentGraph::is_pv_meter)?,
234            vec![9, 24],
235        );
236
237        let (components, connections) = without_grid_meters();
238        assert_eq!(
239            find_matching_components(components, connections, ComponentGraph::is_pv_meter)?,
240            vec![9],
241        );
242
243        Ok(())
244    }
245
246    #[test]
247    fn test_is_battery_meter() -> Result<(), Error> {
248        let (components, connections) = nodes_and_edges();
249        assert_eq!(
250            find_matching_components(components, connections, ComponentGraph::is_battery_meter)?,
251            vec![3, 6],
252        );
253
254        let (components, connections) = with_multiple_grid_meters();
255        assert_eq!(
256            find_matching_components(components, connections, ComponentGraph::is_battery_meter)?,
257            vec![3, 6, 21],
258        );
259
260        let (components, connections) = without_grid_meters();
261        assert_eq!(
262            find_matching_components(components, connections, ComponentGraph::is_battery_meter)?,
263            vec![3, 6],
264        );
265
266        Ok(())
267    }
268
269    #[test]
270    fn test_is_chp_meter() -> Result<(), Error> {
271        let (components, connections) = nodes_and_edges();
272        assert_eq!(
273            find_matching_components(components, connections, ComponentGraph::is_chp_meter)?,
274            vec![12],
275        );
276
277        let (components, connections) = with_multiple_grid_meters();
278        assert_eq!(
279            find_matching_components(components, connections, ComponentGraph::is_chp_meter)?,
280            vec![12],
281        );
282
283        let (components, connections) = without_grid_meters();
284        assert_eq!(
285            find_matching_components(components, connections, ComponentGraph::is_chp_meter)?,
286            vec![12],
287        );
288
289        Ok(())
290    }
291
292    #[test]
293    fn test_is_ev_charger_meter() -> Result<(), Error> {
294        let (components, connections) = nodes_and_edges();
295        assert_eq!(
296            find_matching_components(components, connections, ComponentGraph::is_ev_charger_meter)?,
297            vec![],
298        );
299
300        let (components, connections) = with_multiple_grid_meters();
301        assert_eq!(
302            find_matching_components(components, connections, ComponentGraph::is_ev_charger_meter)?,
303            vec![],
304        );
305
306        let (components, connections) = without_grid_meters();
307        assert_eq!(
308            find_matching_components(components, connections, ComponentGraph::is_ev_charger_meter)?,
309            vec![20],
310        );
311
312        Ok(())
313    }
314}