Skip to main content

frequenz_microgrid_component_graph/graph/
creation.rs

1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4//! Methods for creating [`ComponentGraph`] instances from given components and
5//! connections.
6
7use petgraph::graph::DiGraph;
8
9use crate::{ComponentGraphConfig, Edge, Error, Node, component_category::CategoryPredicates};
10
11use super::{ComponentGraph, EdgeMap, NodeIndexMap};
12
13/// `ComponentGraph` instantiation.
14impl<N, E> ComponentGraph<N, E>
15where
16    N: Node,
17    E: Edge,
18{
19    /// Creates a new [`ComponentGraph`] from the given components and connections.
20    ///
21    /// Returns an error if the graph is invalid.
22    pub fn try_new<NodeIterator: IntoIterator<Item = N>, EdgeIterator: IntoIterator<Item = E>>(
23        components: NodeIterator,
24        connections: EdgeIterator,
25        config: ComponentGraphConfig,
26    ) -> Result<Self, Error> {
27        let (graph, indices) = Self::create_graph(components, &config)?;
28        let root_id = Self::find_root(&graph)?.component_id();
29
30        let mut cg = Self {
31            graph,
32            node_indices: indices,
33            root_id,
34            edges: EdgeMap::new(),
35            config,
36        };
37        cg.add_connections(connections)?;
38
39        cg.validate()?;
40
41        // Notify operators that any pass-through nodes in the graph
42        // will be treated as transparent by validators and formula
43        // generators. Logged once per node, after validation, so we
44        // only warn for components that actually end up in the graph.
45        for component in cg.components() {
46            if component.category().is_passthrough() {
47                tracing::warn!(
48                    "Component {cid} ({category}) is a pass-through category and \
49                     will be treated as transparent in validators and formula generators.",
50                    cid = component.component_id(),
51                    category = component.category(),
52                );
53            }
54        }
55
56        Ok(cg)
57    }
58
59    fn find_root(graph: &DiGraph<N, ()>) -> Result<&N, Error> {
60        let mut roots_iter = graph.raw_nodes().iter().filter(|n| n.weight.is_grid());
61
62        let root = roots_iter
63            .next()
64            .map(|n| &n.weight)
65            .ok_or_else(|| Error::invalid_graph("No grid component found."))?;
66
67        if roots_iter.next().is_some() {
68            return Err(Error::invalid_graph("Multiple grid components found."));
69        }
70
71        Ok(root)
72    }
73
74    fn create_graph(
75        components: impl IntoIterator<Item = N>,
76        config: &ComponentGraphConfig,
77    ) -> Result<(DiGraph<N, ()>, NodeIndexMap), Error> {
78        let mut graph = DiGraph::new();
79        let mut indices = NodeIndexMap::new();
80
81        for component in components {
82            let cid = component.component_id();
83
84            if component.is_unspecified() {
85                return Err(Error::invalid_component(format!(
86                    "ComponentCategory not specified for component: {cid}"
87                )));
88            }
89            if component.is_unspecified_inverter(config) {
90                return Err(Error::invalid_component(format!(
91                    "InverterType not specified for inverter: {cid}"
92                )));
93            }
94            if indices.contains_key(&cid) {
95                return Err(Error::invalid_graph(format!(
96                    "Duplicate component ID found: {cid}"
97                )));
98            }
99
100            let idx = graph.add_node(component);
101            indices.insert(cid, idx);
102        }
103
104        Ok((graph, indices))
105    }
106
107    fn add_connections(&mut self, connections: impl IntoIterator<Item = E>) -> Result<(), Error> {
108        for connection in connections {
109            let sid = connection.source();
110            let did = connection.destination();
111
112            if sid == did {
113                return Err(Error::invalid_connection(format!(
114                    "Connection:({sid}, {did}) Can't connect a component to itself."
115                )));
116            }
117            for cid in [sid, did] {
118                if !self.node_indices.contains_key(&cid) {
119                    return Err(Error::invalid_connection(format!(
120                        "Connection:({sid}, {did}) Can't find a component with ID {cid}"
121                    )));
122                }
123            }
124
125            let source_idx = self.node_indices[&connection.source()];
126            let dest_idx = self.node_indices[&connection.destination()];
127            self.edges.insert((source_idx, dest_idx), connection);
128            self.graph.update_edge(source_idx, dest_idx, ());
129        }
130
131        Ok(())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::ComponentCategory;
139    use crate::InverterType;
140    use crate::graph::test_utils::{ComponentGraphBuilder, ComponentHandle};
141
142    fn nodes_and_edges() -> (ComponentGraphBuilder, ComponentHandle) {
143        let mut builder = ComponentGraphBuilder::new();
144
145        let grid_meter = builder.meter();
146        let meter_bat_chain = builder.meter_bat_chain(1, 1);
147        builder.connect(grid_meter, meter_bat_chain);
148
149        let meter_bat_chain = builder.meter_bat_chain(1, 1);
150        builder.connect(grid_meter, meter_bat_chain);
151
152        (builder, grid_meter)
153    }
154
155    #[test]
156    fn test_component_validation() {
157        let (mut builder, grid_meter) = nodes_and_edges();
158
159        assert!(
160            builder
161                .build(None)
162                .is_err_and(|e| e == Error::invalid_graph("No grid component found.")),
163        );
164
165        let grid = builder.grid();
166        builder.connect(grid, grid_meter);
167
168        assert!(builder.build(None).is_ok());
169
170        builder.add_component_with_id(2, ComponentCategory::Meter);
171        assert!(
172            builder
173                .build(None)
174                .is_err_and(|e| e == Error::invalid_graph("Duplicate component ID found: 2"))
175        );
176
177        builder.pop_component();
178        builder.add_component(ComponentCategory::Unspecified);
179        assert!(
180            builder.build(None).is_err_and(|e| e
181                == Error::invalid_component("ComponentCategory not specified for component: 8"))
182        );
183
184        builder.pop_component();
185        let unspec_inv =
186            builder.add_component(ComponentCategory::Inverter(InverterType::Unspecified));
187        builder.connect(grid_meter, unspec_inv);
188
189        // With default config, unspecified inverter types are not accepted.
190        assert!(builder.build(None).is_err_and(
191            |e| e == Error::invalid_component("InverterType not specified for inverter: 9")
192        ));
193        // With `allow_unspecified_inverters=true`, unspecified inverter types
194        // are treated as battery inverters.
195        let unspec_inv_config = ComponentGraphConfig::builder()
196            .allow_unspecified_inverters(true)
197            .build();
198
199        assert!(builder.build(Some(unspec_inv_config.clone())).is_ok());
200
201        assert!(
202            builder
203                .pop_component()
204                .unwrap()
205                .is_battery_inverter(&unspec_inv_config)
206        );
207        builder.pop_connection();
208        builder.add_component(ComponentCategory::GridConnectionPoint);
209        assert!(
210            builder
211                .build(None)
212                .is_err_and(|e| e == Error::invalid_graph("Multiple grid components found."))
213        );
214
215        builder.pop_component();
216        assert!(builder.build(None).is_ok());
217    }
218
219    #[test]
220    fn test_connection_validation() {
221        let (mut builder, grid_meter) = nodes_and_edges();
222
223        let grid = builder.grid();
224        builder.connect(grid, grid_meter);
225
226        builder.connect(grid, grid);
227        assert!(builder.build(None).is_err_and(|e| e
228            == Error::invalid_connection(
229                "Connection:(7, 7) Can't connect a component to itself."
230            )));
231        builder.pop_connection();
232
233        builder.connect(grid_meter, ComponentHandle::new(9));
234        assert!(builder.build(None).is_err_and(|e| e
235            == Error::invalid_connection("Connection:(0, 9) Can't find a component with ID 9")));
236
237        builder.pop_connection();
238        assert!(builder.build(None).is_ok());
239    }
240}