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