Skip to main content

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::{ComponentGraph, Edge, Error, Node, component_category::CategoryPredicates};
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 Wind Turbine meter.
75    ///
76    /// A meter is identified as a Wind Turbine meter if
77    ///   - has atleast one successor
78    ///   - all its successors are Wind Turbines.
79    pub fn is_wind_turbine_meter(&self, component_id: u64) -> Result<bool, Error> {
80        let mut has_successors = false;
81        Ok(self.component(component_id)?.is_meter()
82            && self.successors(component_id)?.all(|n| {
83                has_successors = true;
84                n.is_wind_turbine()
85            })
86            && has_successors)
87    }
88
89    /// Returns true if the node is a steam boiler meter.
90    ///
91    /// A meter is identified as a steam boiler meter if
92    ///   - it has at least one successor
93    ///   - all its successors are steam boilers.
94    pub fn is_steam_boiler_meter(&self, component_id: u64) -> Result<bool, Error> {
95        let mut has_successors = false;
96        Ok(self.component(component_id)?.is_meter()
97            && self.successors(component_id)?.all(|n| {
98                has_successors = true;
99                n.is_steam_boiler()
100            })
101            && has_successors)
102    }
103
104    /// Returns true if the node is a component meter.
105    ///
106    /// A meter is a component meter if it is one of the following:
107    ///  - a PV meter,
108    ///  - a battery meter,
109    ///  - an EV charger meter,
110    ///  - a CHP meter,
111    ///  - a Wind Turbine meter,
112    ///  - a Steam Boiler meter.
113    pub fn is_component_meter(&self, component_id: u64) -> Result<bool, Error> {
114        Ok(self.is_pv_meter(component_id)?
115            || self.is_battery_meter(component_id)?
116            || self.is_ev_charger_meter(component_id)?
117            || self.is_chp_meter(component_id)?
118            || self.is_wind_turbine_meter(component_id)?
119            || self.is_steam_boiler_meter(component_id)?)
120    }
121
122    /// Returns true if the node is part of a battery chain.
123    ///
124    /// A component is part of a battery chain if it is one of the following:
125    ///  - a battery meter,
126    ///  - a battery inverter,
127    ///  - a battery.
128    pub fn is_battery_chain(&self, component_id: u64) -> Result<bool, Error> {
129        Ok(self.is_battery_meter(component_id)? || {
130            let component = self.component(component_id)?;
131            component.is_battery() || component.is_battery_inverter(&self.config)
132        })
133    }
134
135    /// Returns true if the node is part of a PV chain.
136    ///
137    /// A component is part of a PV chain if it is one of the following:
138    ///  - a PV meter,
139    ///  - a PV inverter.
140    pub fn is_pv_chain(&self, component_id: u64) -> Result<bool, Error> {
141        Ok(self.is_pv_meter(component_id)? || self.component(component_id)?.is_pv_inverter())
142    }
143
144    /// Returns true if the node is part of a CHP chain.
145    ///
146    /// A component is part of a CHP chain if it is one of the following:
147    ///  - a CHP meter,
148    ///  - a CHP.
149    pub fn is_chp_chain(&self, component_id: u64) -> Result<bool, Error> {
150        Ok(self.is_chp_meter(component_id)? || self.component(component_id)?.is_chp())
151    }
152
153    /// Returns true if the node is part of an EV charger chain.
154    ///
155    /// A component is part of an EV charger chain if it is one of the following:
156    ///  - an EV charger meter,
157    ///  - an EV charger.
158    pub fn is_ev_charger_chain(&self, component_id: u64) -> Result<bool, Error> {
159        Ok(
160            self.is_ev_charger_meter(component_id)?
161                || self.component(component_id)?.is_ev_charger(),
162        )
163    }
164
165    /// Returns true if the node is part of a Wind Turbine chain.
166    ///
167    /// A component is part of a Wind Turbine chain if it is one of the following:
168    /// - a Wind Turbine meter,
169    /// - a Wind Turbine.
170    pub fn is_wind_turbine_chain(&self, component_id: u64) -> Result<bool, Error> {
171        Ok(self.is_wind_turbine_meter(component_id)?
172            || self.component(component_id)?.is_wind_turbine())
173    }
174
175    /// Returns true if the node is part of a steam boiler chain.
176    ///
177    /// A component is part of a steam boiler chain if it is one of the following:
178    /// - a steam boiler meter,
179    /// - a steam boiler.
180    pub fn is_steam_boiler_chain(&self, component_id: u64) -> Result<bool, Error> {
181        Ok(self.is_steam_boiler_meter(component_id)?
182            || self.component(component_id)?.is_steam_boiler())
183    }
184
185    /// Returns true if the node is part of a component chain.
186    ///
187    /// A component is part of a component chain if it is part of one of the
188    /// following:
189    ///  - a battery chain,
190    ///  - a PV chain,
191    ///  - an EV charger chain,
192    ///  - a CHP chain,
193    ///  - a Wind Turbine chain,
194    ///  - a steam boiler chain.
195    pub fn is_component_chain(&self, component_id: u64) -> Result<bool, Error> {
196        Ok(self.is_battery_chain(component_id)?
197            || self.is_pv_chain(component_id)?
198            || self.is_ev_charger_chain(component_id)?
199            || self.is_chp_chain(component_id)?
200            || self.is_wind_turbine_chain(component_id)?
201            || self.is_steam_boiler_chain(component_id)?)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::ComponentCategory;
209    use crate::ComponentGraphConfig;
210    use crate::InverterType;
211    use crate::component_category::BatteryType;
212    use crate::component_category::EvChargerType;
213    use crate::error::Error;
214    use crate::graph::test_utils::{TestComponent, TestConnection};
215
216    fn nodes_and_edges() -> (Vec<TestComponent>, Vec<TestConnection>) {
217        let components = vec![
218            TestComponent::new(1, ComponentCategory::GridConnectionPoint),
219            TestComponent::new(2, ComponentCategory::Meter),
220            TestComponent::new(3, ComponentCategory::Meter),
221            TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery)),
222            TestComponent::new(5, ComponentCategory::Battery(BatteryType::NaIon)),
223            TestComponent::new(6, ComponentCategory::Meter),
224            TestComponent::new(7, ComponentCategory::Inverter(InverterType::Battery)),
225            TestComponent::new(8, ComponentCategory::Battery(BatteryType::Unspecified)),
226            TestComponent::new(9, ComponentCategory::Meter),
227            TestComponent::new(10, ComponentCategory::Inverter(InverterType::Pv)),
228            TestComponent::new(11, ComponentCategory::Inverter(InverterType::Pv)),
229            TestComponent::new(12, ComponentCategory::Meter),
230            TestComponent::new(13, ComponentCategory::Chp),
231            TestComponent::new(14, ComponentCategory::Meter),
232            TestComponent::new(15, ComponentCategory::Chp),
233            TestComponent::new(16, ComponentCategory::Inverter(InverterType::Pv)),
234            TestComponent::new(17, ComponentCategory::Inverter(InverterType::Battery)),
235            TestComponent::new(18, ComponentCategory::Battery(BatteryType::LiIon)),
236            TestComponent::new(19, ComponentCategory::Meter),
237            TestComponent::new(20, ComponentCategory::SteamBoiler),
238        ];
239        let connections = vec![
240            // Single Grid meter
241            TestConnection::new(1, 2),
242            // Battery chain
243            TestConnection::new(2, 3),
244            TestConnection::new(3, 4),
245            TestConnection::new(4, 5),
246            // Battery chain
247            TestConnection::new(2, 6),
248            TestConnection::new(6, 7),
249            TestConnection::new(7, 8),
250            // Solar chain
251            TestConnection::new(2, 9),
252            TestConnection::new(9, 10),
253            TestConnection::new(9, 11),
254            // CHP chain
255            TestConnection::new(2, 12),
256            TestConnection::new(12, 13),
257            // Mixed chain
258            TestConnection::new(2, 14),
259            TestConnection::new(14, 15),
260            TestConnection::new(14, 16),
261            TestConnection::new(14, 17),
262            TestConnection::new(17, 18),
263            // Steam boiler chain
264            TestConnection::new(2, 19),
265            TestConnection::new(19, 20),
266        ];
267
268        (components, connections)
269    }
270
271    fn with_multiple_grid_meters() -> (Vec<TestComponent>, Vec<TestConnection>) {
272        let (mut components, mut connections) = nodes_and_edges();
273
274        // Add a meter to the grid without successors
275        components.push(TestComponent::new(21, ComponentCategory::Meter));
276        connections.push(TestConnection::new(1, 21));
277
278        // Add a meter to the grid that has a battery meter and a PV meter as
279        // successors.
280        components.push(TestComponent::new(22, ComponentCategory::Meter));
281        connections.push(TestConnection::new(1, 22));
282
283        // battery chain
284        components.push(TestComponent::new(23, ComponentCategory::Meter));
285        components.push(TestComponent::new(
286            24,
287            ComponentCategory::Inverter(InverterType::Battery),
288        ));
289        components.push(TestComponent::new(
290            25,
291            ComponentCategory::Battery(BatteryType::Unspecified),
292        ));
293        connections.push(TestConnection::new(22, 23));
294        connections.push(TestConnection::new(23, 24));
295        connections.push(TestConnection::new(24, 25));
296
297        // pv chain
298        components.push(TestComponent::new(26, ComponentCategory::Meter));
299        components.push(TestComponent::new(
300            27,
301            ComponentCategory::Inverter(InverterType::Pv),
302        ));
303        connections.push(TestConnection::new(22, 26));
304        connections.push(TestConnection::new(26, 27));
305
306        // steam boiler chain
307        components.push(TestComponent::new(28, ComponentCategory::Meter));
308        components.push(TestComponent::new(29, ComponentCategory::SteamBoiler));
309        connections.push(TestConnection::new(22, 28));
310        connections.push(TestConnection::new(28, 29));
311
312        (components, connections)
313    }
314
315    fn without_grid_meters() -> (Vec<TestComponent>, Vec<TestConnection>) {
316        let (mut components, mut connections) = nodes_and_edges();
317
318        // Add an EV charger meter to the grid, then none of the meters
319        // connected to the grid should be detected as grid meters.
320        components.push(TestComponent::new(21, ComponentCategory::Meter));
321        components.push(TestComponent::new(
322            22,
323            ComponentCategory::EvCharger(EvChargerType::Ac),
324        ));
325        connections.push(TestConnection::new(1, 21));
326        connections.push(TestConnection::new(21, 22));
327
328        (components, connections)
329    }
330
331    fn find_matching_components(
332        components: Vec<TestComponent>,
333        connections: Vec<TestConnection>,
334        filter: impl Fn(&ComponentGraph<TestComponent, TestConnection>, u64) -> Result<bool, Error>,
335    ) -> Result<Vec<u64>, Error> {
336        let config = ComponentGraphConfig::default();
337
338        let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?;
339
340        let mut found_meters = vec![];
341        for comp in graph.components() {
342            if filter(&graph, comp.component_id())? {
343                found_meters.push(comp.component_id());
344            }
345        }
346
347        Ok(found_meters)
348    }
349
350    #[test]
351    fn test_is_pv_meter() -> Result<(), Error> {
352        let (components, connections) = nodes_and_edges();
353        assert_eq!(
354            find_matching_components(components, connections, ComponentGraph::is_pv_meter)?,
355            vec![9],
356        );
357
358        let (components, connections) = with_multiple_grid_meters();
359        assert_eq!(
360            find_matching_components(components, connections, ComponentGraph::is_pv_meter)?,
361            vec![9, 26],
362        );
363
364        let (components, connections) = without_grid_meters();
365        assert_eq!(
366            find_matching_components(components, connections, ComponentGraph::is_pv_meter)?,
367            vec![9],
368        );
369
370        Ok(())
371    }
372
373    #[test]
374    fn test_is_battery_meter() -> Result<(), Error> {
375        let (components, connections) = nodes_and_edges();
376        assert_eq!(
377            find_matching_components(components, connections, ComponentGraph::is_battery_meter)?,
378            vec![3, 6],
379        );
380
381        let (components, connections) = with_multiple_grid_meters();
382        assert_eq!(
383            find_matching_components(components, connections, ComponentGraph::is_battery_meter)?,
384            vec![3, 6, 23],
385        );
386
387        let (components, connections) = without_grid_meters();
388        assert_eq!(
389            find_matching_components(components, connections, ComponentGraph::is_battery_meter)?,
390            vec![3, 6],
391        );
392
393        Ok(())
394    }
395
396    #[test]
397    fn test_is_chp_meter() -> Result<(), Error> {
398        let (components, connections) = nodes_and_edges();
399        assert_eq!(
400            find_matching_components(components, connections, ComponentGraph::is_chp_meter)?,
401            vec![12],
402        );
403
404        let (components, connections) = with_multiple_grid_meters();
405        assert_eq!(
406            find_matching_components(components, connections, ComponentGraph::is_chp_meter)?,
407            vec![12],
408        );
409
410        let (components, connections) = without_grid_meters();
411        assert_eq!(
412            find_matching_components(components, connections, ComponentGraph::is_chp_meter)?,
413            vec![12],
414        );
415
416        Ok(())
417    }
418
419    #[test]
420    fn test_is_ev_charger_meter() -> Result<(), Error> {
421        let (components, connections) = nodes_and_edges();
422        assert_eq!(
423            find_matching_components(components, connections, ComponentGraph::is_ev_charger_meter)?,
424            vec![],
425        );
426
427        let (components, connections) = with_multiple_grid_meters();
428        assert_eq!(
429            find_matching_components(components, connections, ComponentGraph::is_ev_charger_meter)?,
430            vec![],
431        );
432
433        let (components, connections) = without_grid_meters();
434        assert_eq!(
435            find_matching_components(components, connections, ComponentGraph::is_ev_charger_meter)?,
436            vec![21],
437        );
438
439        Ok(())
440    }
441
442    #[test]
443    fn test_is_steam_boiler_meter() -> Result<(), Error> {
444        let (components, connections) = nodes_and_edges();
445        assert_eq!(
446            find_matching_components(
447                components,
448                connections,
449                ComponentGraph::is_steam_boiler_meter
450            )?,
451            vec![19],
452        );
453
454        let (components, connections) = with_multiple_grid_meters();
455        assert_eq!(
456            find_matching_components(
457                components,
458                connections,
459                ComponentGraph::is_steam_boiler_meter
460            )?,
461            vec![19, 28],
462        );
463
464        let (components, connections) = without_grid_meters();
465        assert_eq!(
466            find_matching_components(
467                components,
468                connections,
469                ComponentGraph::is_steam_boiler_meter
470            )?,
471            vec![19],
472        );
473
474        Ok(())
475    }
476}