Skip to main content

frequenz_microgrid_component_graph/graph/
retrieval.rs

1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4//! Methods for retrieving components and connections from a [`ComponentGraph`].
5
6use crate::iterators::{Components, Connections, Neighbors, RawNeighbors, Siblings};
7use crate::{ComponentGraph, Edge, Error, Node};
8use petgraph::graph::NodeIndex;
9use std::collections::{BTreeSet, HashSet, VecDeque};
10
11/// `Component` and `Connection` retrieval.
12impl<N, E> ComponentGraph<N, E>
13where
14    N: Node,
15    E: Edge,
16{
17    /// Returns the component with the given `component_id`, if it exists.
18    pub fn component(&self, component_id: u64) -> Result<&N, Error> {
19        self.node_indices
20            .get(&component_id)
21            .map(|i| &self.graph[*i])
22            .ok_or_else(|| {
23                Error::component_not_found(format!("Component with id {component_id} not found."))
24            })
25    }
26
27    /// Returns an iterator over the components in the graph.
28    pub fn components(&self) -> Components<'_, N> {
29        Components {
30            iter: self.graph.raw_nodes().iter(),
31        }
32    }
33
34    /// Returns an iterator over the connections in the graph.
35    pub fn connections(&self) -> Connections<'_, N, E> {
36        Connections {
37            cg: self,
38            iter: self.graph.raw_edges().iter(),
39        }
40    }
41
42    /// Returns an iterator over the *raw* (graph-direct) predecessors of
43    /// the component with the given `component_id`.
44    ///
45    /// "Raw" means every node connected by an incoming edge, including
46    /// pass-through categories. Most callers want
47    /// [`predecessors`][Self::predecessors] instead, which walks past
48    /// pass-throughs transparently.
49    ///
50    /// Returns an error if the given `component_id` does not exist.
51    pub fn raw_predecessors(&self, component_id: u64) -> Result<RawNeighbors<'_, N>, Error> {
52        self.raw_neighbors(component_id, petgraph::Direction::Incoming)
53    }
54
55    /// Returns an iterator over the *raw* (graph-direct) successors of
56    /// the component with the given `component_id`.
57    ///
58    /// "Raw" means every node connected by an outgoing edge, including
59    /// pass-through categories. Most callers want
60    /// [`successors`][Self::successors] instead, which walks past
61    /// pass-throughs transparently.
62    ///
63    /// Returns an error if the given `component_id` does not exist.
64    pub fn raw_successors(&self, component_id: u64) -> Result<RawNeighbors<'_, N>, Error> {
65        self.raw_neighbors(component_id, petgraph::Direction::Outgoing)
66    }
67
68    /// Shared implementation for [`raw_predecessors`][Self::raw_predecessors]
69    /// and [`raw_successors`][Self::raw_successors].
70    fn raw_neighbors(
71        &self,
72        component_id: u64,
73        direction: petgraph::Direction,
74    ) -> Result<RawNeighbors<'_, N>, Error> {
75        self.node_indices
76            .get(&component_id)
77            .map(|&index| RawNeighbors {
78                graph: &self.graph,
79                iter: self.graph.neighbors_directed(index, direction),
80            })
81            .ok_or_else(|| {
82                Error::component_not_found(format!("Component with id {component_id} not found."))
83            })
84    }
85
86    /// Returns an iterator over the *predecessors* of the component with
87    /// the given `component_id`, walking transparently past pass-through
88    /// categories.
89    ///
90    /// Pass-through nodes are skipped: their non-pass-through ancestors
91    /// take their place in the iterator. For the raw (graph-direct) view
92    /// that includes pass-throughs, use
93    /// [`raw_predecessors`][Self::raw_predecessors].
94    ///
95    /// Returns an error if the given `component_id` does not exist.
96    pub fn predecessors(&self, component_id: u64) -> Result<Neighbors<'_, N>, Error> {
97        self.collect_effective_neighbors(component_id, petgraph::Direction::Incoming)
98    }
99
100    /// Returns an iterator over the *successors* of the component with
101    /// the given `component_id`, walking transparently past pass-through
102    /// categories.
103    ///
104    /// Pass-through nodes are skipped: their non-pass-through descendants
105    /// take their place in the iterator. For the raw (graph-direct) view
106    /// that includes pass-throughs, use
107    /// [`raw_successors`][Self::raw_successors].
108    ///
109    /// Returns an error if the given `component_id` does not exist.
110    pub fn successors(&self, component_id: u64) -> Result<Neighbors<'_, N>, Error> {
111        self.collect_effective_neighbors(component_id, petgraph::Direction::Outgoing)
112    }
113
114    /// BFS through pass-through nodes in the given direction, collecting
115    /// the first non-pass-through node along each branch.
116    fn collect_effective_neighbors(
117        &self,
118        component_id: u64,
119        direction: petgraph::Direction,
120    ) -> Result<Neighbors<'_, N>, Error> {
121        let start = *self.node_indices.get(&component_id).ok_or_else(|| {
122            Error::component_not_found(format!("Component with id {component_id} not found."))
123        })?;
124
125        let mut queue: VecDeque<NodeIndex> =
126            self.graph.neighbors_directed(start, direction).collect();
127        let mut visited: HashSet<NodeIndex> = HashSet::new();
128        let mut result: Vec<&N> = Vec::new();
129
130        while let Some(idx) = queue.pop_front() {
131            if !visited.insert(idx) {
132                continue;
133            }
134            let node = &self.graph[idx];
135            if node.category().is_passthrough() {
136                queue.extend(self.graph.neighbors_directed(idx, direction));
137            } else {
138                result.push(node);
139            }
140        }
141
142        Ok(Neighbors {
143            iter: result.into_iter(),
144        })
145    }
146
147    /// Returns an iterator over the *siblings* of the component with the
148    /// given `component_id`, that have shared predecessors.
149    ///
150    /// Returns an error if the given `component_id` does not exist.
151    pub(crate) fn siblings_from_predecessors(
152        &self,
153        component_id: u64,
154    ) -> Result<Siblings<'_, N>, Error> {
155        Ok(Siblings::new(
156            component_id,
157            self.predecessors(component_id)?
158                .map(|x| self.successors(x.component_id()))
159                .collect::<Result<Vec<_>, _>>()?
160                .into_iter()
161                .flatten(),
162        ))
163    }
164
165    /// Returns an iterator over the *siblings* of the component with the
166    /// given `component_id`, that have shared successors.
167    ///
168    /// Returns an error if the given `component_id` does not exist.
169    pub(crate) fn siblings_from_successors(
170        &self,
171        component_id: u64,
172    ) -> Result<Siblings<'_, N>, Error> {
173        Ok(Siblings::new(
174            component_id,
175            self.successors(component_id)?
176                .map(|x| self.predecessors(x.component_id()))
177                .collect::<Result<Vec<_>, _>>()?
178                .into_iter()
179                .flatten(),
180        ))
181    }
182
183    /// Returns a set of all components that match the given predicate, starting
184    /// from the component with the given `component_id`, in the given direction.
185    ///
186    /// If `follow_after_match` is `true`, the search continues deeper beyond
187    /// the matching components.
188    pub(crate) fn find_all(
189        &self,
190        from: u64,
191        pred: impl Fn(&N) -> bool,
192        direction: petgraph::Direction,
193        follow_after_match: bool,
194    ) -> Result<BTreeSet<u64>, Error> {
195        let index = self.node_indices.get(&from).ok_or_else(|| {
196            Error::component_not_found(format!("Component with id {from} not found."))
197        })?;
198        let mut stack = vec![*index];
199        let mut visited = HashSet::new();
200        let mut found = BTreeSet::new();
201
202        while let Some(index) = stack.pop() {
203            // Skip nodes already expanded: a DAG with diamonds reaches the
204            // same node by multiple paths, and re-expanding it is redundant
205            // (and exponential on chained diamonds).
206            if !visited.insert(index) {
207                continue;
208            }
209            let node = &self.graph[index];
210            // Pass-through nodes are transparent: skip the predicate
211            // check but follow through their neighbors.
212            if !node.category().is_passthrough() && pred(node) {
213                found.insert(node.component_id());
214                if !follow_after_match {
215                    continue;
216                }
217            }
218
219            let neighbors = self.graph.neighbors_directed(index, direction);
220            stack.extend(neighbors);
221        }
222
223        Ok(found)
224    }
225
226    /// Whether any component matching the given predicate is reachable from
227    /// the component with the given `component_id`, in the given direction.
228    /// Stops at the first match, unlike [`ComponentGraph::find_all`], which
229    /// collects them all. Pass-through nodes are transparent here too: they
230    /// never match, but the search follows through their neighbors.
231    pub(crate) fn reaches_any(
232        &self,
233        from: u64,
234        pred: impl Fn(&N) -> bool,
235        direction: petgraph::Direction,
236    ) -> Result<bool, Error> {
237        let index = self.node_indices.get(&from).ok_or_else(|| {
238            Error::component_not_found(format!("Component with id {from} not found."))
239        })?;
240        let mut stack = vec![*index];
241        let mut visited = HashSet::new();
242
243        while let Some(index) = stack.pop() {
244            // Skip nodes already expanded: a DAG with diamonds reaches the
245            // same node by multiple paths, and re-expanding it is redundant
246            // (and exponential on chained diamonds).
247            if !visited.insert(index) {
248                continue;
249            }
250            let node = &self.graph[index];
251            if !node.category().is_passthrough() && pred(node) {
252                return Ok(true);
253            }
254            stack.extend(self.graph.neighbors_directed(index, direction));
255        }
256
257        Ok(false)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::ComponentCategory;
265    use crate::ComponentGraphConfig;
266    use crate::InverterType;
267    use crate::component_category::BatteryType;
268    use crate::component_category::CategoryPredicates;
269    use crate::error::Error;
270    use crate::graph::test_utils::ComponentGraphBuilder;
271    use crate::graph::test_utils::{TestComponent, TestConnection};
272
273    fn nodes_and_edges() -> (Vec<TestComponent>, Vec<TestConnection>) {
274        let components = vec![
275            TestComponent::new(6, ComponentCategory::Meter),
276            TestComponent::new(1, ComponentCategory::GridConnectionPoint),
277            TestComponent::new(7, ComponentCategory::Inverter(InverterType::Battery)),
278            TestComponent::new(3, ComponentCategory::Meter),
279            TestComponent::new(5, ComponentCategory::Battery(BatteryType::Unspecified)),
280            TestComponent::new(8, ComponentCategory::Battery(BatteryType::LiIon)),
281            TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery)),
282            TestComponent::new(2, ComponentCategory::Meter),
283        ];
284        let connections = vec![
285            TestConnection::new(3, 4),
286            TestConnection::new(1, 2),
287            TestConnection::new(7, 8),
288            TestConnection::new(4, 5),
289            TestConnection::new(2, 3),
290            TestConnection::new(6, 7),
291            TestConnection::new(2, 6),
292        ];
293
294        (components, connections)
295    }
296
297    #[test]
298    fn test_component() -> Result<(), Error> {
299        let config = ComponentGraphConfig::default();
300        let (components, connections) = nodes_and_edges();
301        let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?;
302
303        assert_eq!(
304            graph.component(1),
305            Ok(&TestComponent::new(
306                1,
307                ComponentCategory::GridConnectionPoint
308            ))
309        );
310        assert_eq!(
311            graph.component(5),
312            Ok(&TestComponent::new(
313                5,
314                ComponentCategory::Battery(BatteryType::Unspecified)
315            ))
316        );
317        assert_eq!(
318            graph.component(9),
319            Err(Error::component_not_found("Component with id 9 not found."))
320        );
321
322        Ok(())
323    }
324
325    #[test]
326    fn test_components() -> Result<(), Error> {
327        let config = ComponentGraphConfig::default();
328        let (components, connections) = nodes_and_edges();
329        let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?;
330
331        assert!(graph.components().eq(&components));
332        assert!(graph.components().filter(|x| x.is_battery()).eq(&[
333            TestComponent::new(5, ComponentCategory::Battery(BatteryType::Unspecified)),
334            TestComponent::new(8, ComponentCategory::Battery(BatteryType::LiIon))
335        ]));
336
337        Ok(())
338    }
339
340    #[test]
341    fn test_connections() -> Result<(), Error> {
342        let config = ComponentGraphConfig::default();
343        let (components, connections) = nodes_and_edges();
344        let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?;
345
346        assert!(graph.connections().eq(&connections));
347
348        assert!(
349            graph
350                .connections()
351                .filter(|x| x.source() == 2)
352                .eq(&[TestConnection::new(2, 3), TestConnection::new(2, 6)])
353        );
354
355        Ok(())
356    }
357
358    #[test]
359    fn test_neighbors() -> Result<(), Error> {
360        let config = ComponentGraphConfig::default();
361        let (components, connections) = nodes_and_edges();
362        let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?;
363
364        assert!(graph.predecessors(1).is_ok_and(|x| x.eq(&[])));
365
366        assert!(
367            graph
368                .predecessors(3)
369                .is_ok_and(|x| x.eq(&[TestComponent::new(2, ComponentCategory::Meter)]))
370        );
371
372        assert!(
373            graph
374                .successors(1)
375                .is_ok_and(|x| x.eq(&[TestComponent::new(2, ComponentCategory::Meter)]))
376        );
377
378        assert!(graph.successors(2).is_ok_and(|x| {
379            x.eq(&[
380                TestComponent::new(6, ComponentCategory::Meter),
381                TestComponent::new(3, ComponentCategory::Meter),
382            ])
383        }));
384
385        assert!(graph.successors(5).is_ok_and(|x| x.eq(&[])));
386
387        assert!(
388            graph
389                .predecessors(32)
390                .is_err_and(|e| e == Error::component_not_found("Component with id 32 not found."))
391        );
392        assert!(
393            graph
394                .successors(32)
395                .is_err_and(|e| e == Error::component_not_found("Component with id 32 not found."))
396        );
397
398        Ok(())
399    }
400
401    #[test]
402    fn test_siblings() -> Result<(), Error> {
403        let mut builder = ComponentGraphBuilder::new();
404        let grid = builder.grid();
405
406        // Add a grid meter to the grid, with no successors.
407        let grid_meter = builder.meter();
408        builder.connect(grid, grid_meter);
409
410        assert_eq!(grid_meter.component_id(), 1);
411
412        // Add a battery chain with three inverters and two battery.
413        let meter_bat_chain = builder.meter_bat_chain(3, 2);
414        builder.connect(grid_meter, meter_bat_chain);
415
416        assert_eq!(meter_bat_chain.component_id(), 2);
417
418        let graph = builder.build(None)?;
419        assert_eq!(
420            graph
421                .siblings_from_predecessors(3)
422                .unwrap()
423                .collect::<Vec<_>>(),
424            [
425                &TestComponent::new(5, ComponentCategory::Inverter(InverterType::Battery)),
426                &TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery))
427            ]
428        );
429
430        assert_eq!(
431            graph
432                .siblings_from_successors(3)
433                .unwrap()
434                .collect::<Vec<_>>(),
435            [
436                &TestComponent::new(5, ComponentCategory::Inverter(InverterType::Battery)),
437                &TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery))
438            ]
439        );
440
441        assert_eq!(
442            graph
443                .siblings_from_successors(6)
444                .unwrap()
445                .collect::<Vec<_>>(),
446            Vec::<&TestComponent>::new()
447        );
448
449        assert_eq!(
450            graph
451                .siblings_from_predecessors(6)
452                .unwrap()
453                .collect::<Vec<_>>(),
454            [&TestComponent::new(
455                7,
456                ComponentCategory::Battery(BatteryType::LiIon)
457            )]
458        );
459
460        // Add two dangling meter to the grid meter
461        let dangling_meter = builder.meter();
462        builder.connect(grid_meter, dangling_meter);
463        assert_eq!(dangling_meter.component_id(), 8);
464
465        let dangling_meter = builder.meter();
466        builder.connect(grid_meter, dangling_meter);
467        assert_eq!(dangling_meter.component_id(), 9);
468
469        let graph = builder.build(None)?;
470        assert_eq!(
471            graph
472                .siblings_from_predecessors(8)
473                .unwrap()
474                .collect::<Vec<_>>(),
475            [
476                &TestComponent::new(9, ComponentCategory::Meter),
477                &TestComponent::new(2, ComponentCategory::Meter),
478            ]
479        );
480
481        Ok(())
482    }
483
484    /// `raw_predecessors` / `raw_successors` expose the graph-direct
485    /// view (including pass-through nodes), while `predecessors` /
486    /// `successors` walk past them.
487    ///
488    /// Topology: `Grid → PT → Meter → BatteryInverter → Battery`.
489    #[test]
490    fn test_raw_neighbors_includes_passthroughs() -> Result<(), Error> {
491        let mut builder = ComponentGraphBuilder::new();
492        let grid = builder.grid();
493        let pt = builder.power_transformer();
494        let meter = builder.meter();
495        let inverter = builder.battery_inverter();
496        let battery = builder.battery();
497
498        builder.connect(grid, pt);
499        builder.connect(pt, meter);
500        builder.connect(meter, inverter);
501        builder.connect(inverter, battery);
502
503        let graph = builder.build(None)?;
504
505        // Raw view sees the PT directly.
506        let raw_preds: Vec<u64> = graph
507            .raw_predecessors(meter.component_id())?
508            .map(|n| n.component_id())
509            .collect();
510        assert_eq!(raw_preds, vec![pt.component_id()]);
511
512        let raw_succs: Vec<u64> = graph
513            .raw_successors(grid.component_id())?
514            .map(|n| n.component_id())
515            .collect();
516        assert_eq!(raw_succs, vec![pt.component_id()]);
517
518        // Effective view walks past the PT.
519        let preds: Vec<u64> = graph
520            .predecessors(meter.component_id())?
521            .map(|n| n.component_id())
522            .collect();
523        assert_eq!(preds, vec![grid.component_id()]);
524
525        let succs: Vec<u64> = graph
526            .successors(grid.component_id())?
527            .map(|n| n.component_id())
528            .collect();
529        assert_eq!(succs, vec![meter.component_id()]);
530
531        // Unknown component_id behaves the same as the effective methods.
532        assert!(graph.raw_predecessors(999).is_err());
533        assert!(graph.raw_successors(999).is_err());
534
535        // Make sure the unused `battery` and `inverter` handles aren't
536        // optimised away in unrelated test setup.
537        let _ = (battery, inverter);
538        Ok(())
539    }
540
541    /// `find_all` skips pass-through nodes when checking the predicate
542    /// — even if the predicate would match. This keeps PTs out of
543    /// callers' result sets without forcing them to filter.
544    ///
545    /// Topology: `Grid → PT → Meter`.
546    #[test]
547    fn test_find_all_skips_passthroughs() -> Result<(), Error> {
548        let mut builder = ComponentGraphBuilder::new();
549        let grid = builder.grid();
550        let pt = builder.power_transformer();
551        let meter = builder.meter();
552
553        builder.connect(grid, pt);
554        builder.connect(pt, meter);
555
556        let graph = builder.build(None)?;
557
558        // Predicate matches everything; PT is excluded from the result.
559        let found = graph.find_all(
560            grid.component_id(),
561            |_| true,
562            petgraph::Direction::Outgoing,
563            true,
564        )?;
565        assert_eq!(
566            found,
567            BTreeSet::from([grid.component_id(), meter.component_id()])
568        );
569
570        // Predicate that explicitly tries to match PTs still returns nothing.
571        let found = graph.find_all(
572            grid.component_id(),
573            |n| n.category() == ComponentCategory::PowerTransformer,
574            petgraph::Direction::Outgoing,
575            true,
576        )?;
577        assert!(found.is_empty());
578        Ok(())
579    }
580
581    #[test]
582    fn test_find_all() -> Result<(), Error> {
583        let (components, connections) = nodes_and_edges();
584        let graph = ComponentGraph::try_new(
585            components.clone(),
586            connections.clone(),
587            ComponentGraphConfig::default(),
588        )?;
589
590        let found = graph.find_all(
591            graph.root_id,
592            |x| x.is_meter(),
593            petgraph::Direction::Outgoing,
594            false,
595        )?;
596        assert_eq!(found, [2].iter().cloned().collect());
597
598        let found = graph.find_all(
599            graph.root_id,
600            |x| x.is_meter(),
601            petgraph::Direction::Outgoing,
602            true,
603        )?;
604        assert_eq!(found, [2, 3, 6].iter().cloned().collect());
605
606        let found = graph.find_all(
607            graph.root_id,
608            |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false),
609            petgraph::Direction::Outgoing,
610            true,
611        )?;
612        assert_eq!(found, [2, 4, 5, 7, 8].iter().cloned().collect());
613
614        let found = graph.find_all(
615            6,
616            |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false),
617            petgraph::Direction::Outgoing,
618            true,
619        )?;
620        assert_eq!(found, [7, 8].iter().cloned().collect());
621
622        let found = graph.find_all(
623            graph.root_id,
624            |x| !x.is_grid() && !graph.is_component_meter(x.component_id()).unwrap_or(false),
625            petgraph::Direction::Outgoing,
626            false,
627        )?;
628        assert_eq!(found, [2].iter().cloned().collect());
629
630        let found = graph.find_all(
631            graph.root_id,
632            |_| true,
633            petgraph::Direction::Outgoing,
634            false,
635        )?;
636        assert_eq!(found, [1].iter().cloned().collect());
637
638        let found = graph.find_all(3, |_| true, petgraph::Direction::Outgoing, true)?;
639        assert_eq!(found, [3, 4, 5].iter().cloned().collect());
640
641        Ok(())
642    }
643
644    /// `find_all` deduplicates on a re-converging (diamond) topology: a node
645    /// reachable by two paths is expanded once, not once per path. This is the
646    /// shape the `visited` set guards — the tree topologies above never exercise
647    /// it. `follow_after_match = true` is the case that actually re-expands (a
648    /// matched node keeps expanding), so the diamond apex and its subtree must
649    /// still appear exactly once.
650    ///
651    /// Topology (ids): `Grid:0 → {Meter:1, Meter:2}`, both `→ Inverter:3 → Battery:4`.
652    #[test]
653    fn test_find_all_dedups_on_diamond() -> Result<(), Error> {
654        let mut builder = ComponentGraphBuilder::new();
655        let grid = builder.grid();
656        let meter_a = builder.meter();
657        let meter_b = builder.meter();
658        let inverter = builder.battery_inverter();
659        let battery = builder.battery();
660
661        builder.connect(grid, meter_a);
662        builder.connect(grid, meter_b);
663        // The inverter is the diamond apex: reachable via both meters.
664        builder.connect(meter_a, inverter);
665        builder.connect(meter_b, inverter);
666        builder.connect(inverter, battery);
667
668        let graph = builder.build(None)?;
669
670        // follow_after_match = true: the inverter matches yet keeps expanding, and
671        // it is reached by both meters — it and its battery must appear once each.
672        let found = graph.find_all(
673            grid.component_id(),
674            |n| !n.is_grid(),
675            petgraph::Direction::Outgoing,
676            true,
677        )?;
678        assert_eq!(
679            found,
680            BTreeSet::from([
681                meter_a.component_id(),
682                meter_b.component_id(),
683                inverter.component_id(),
684                battery.component_id(),
685            ])
686        );
687
688        // A predicate matching only the apex's subtree still reaches it through
689        // the diamond — the apex is expanded, not skipped before its successors.
690        let found = graph.find_all(
691            grid.component_id(),
692            |n| n.is_battery(),
693            petgraph::Direction::Outgoing,
694            true,
695        )?;
696        assert_eq!(found, BTreeSet::from([battery.component_id()]));
697
698        Ok(())
699    }
700}