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::{component_category::CategoryPredicates, ComponentGraphConfig, Edge, Error, Node};
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        Ok(cg)
42    }
43
44    fn find_root(graph: &DiGraph<N, ()>) -> Result<&N, Error> {
45        let mut roots_iter = graph.raw_nodes().iter().filter(|n| n.weight.is_grid());
46
47        let root = roots_iter
48            .next()
49            .map(|n| &n.weight)
50            .ok_or_else(|| Error::invalid_graph("No grid component found."))?;
51
52        if roots_iter.next().is_some() {
53            return Err(Error::invalid_graph("Multiple grid components found."));
54        }
55
56        Ok(root)
57    }
58
59    fn create_graph(
60        components: impl IntoIterator<Item = N>,
61        config: &ComponentGraphConfig,
62    ) -> Result<(DiGraph<N, ()>, NodeIndexMap), Error> {
63        let mut graph = DiGraph::new();
64        let mut indices = NodeIndexMap::new();
65
66        for component in components {
67            let cid = component.component_id();
68
69            if component.is_unspecified() {
70                return Err(Error::invalid_component(format!(
71                    "ComponentCategory not specified for component: {cid}"
72                )));
73            }
74            if component.is_unspecified_inverter(config) {
75                return Err(Error::invalid_component(format!(
76                    "InverterType not specified for inverter: {cid}"
77                )));
78            }
79            if indices.contains_key(&cid) {
80                return Err(Error::invalid_graph(format!(
81                    "Duplicate component ID found: {cid}"
82                )));
83            }
84
85            let idx = graph.add_node(component);
86            indices.insert(cid, idx);
87        }
88
89        Ok((graph, indices))
90    }
91
92    fn add_connections(&mut self, connections: impl IntoIterator<Item = E>) -> Result<(), Error> {
93        for connection in connections {
94            let sid = connection.source();
95            let did = connection.destination();
96
97            if sid == did {
98                return Err(Error::invalid_connection(format!(
99                    "Connection:({sid}, {did}) Can't connect a component to itself."
100                )));
101            }
102            for cid in [sid, did] {
103                if !self.node_indices.contains_key(&cid) {
104                    return Err(Error::invalid_connection(format!(
105                        "Connection:({sid}, {did}) Can't find a component with ID {cid}"
106                    )));
107                }
108            }
109
110            let source_idx = self.node_indices[&connection.source()];
111            let dest_idx = self.node_indices[&connection.destination()];
112            self.edges.insert((source_idx, dest_idx), connection);
113            self.graph.update_edge(source_idx, dest_idx, ());
114        }
115
116        Ok(())
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::graph::test_utils::{ComponentGraphBuilder, ComponentHandle};
124    use crate::ComponentCategory;
125    use crate::InverterType;
126
127    fn nodes_and_edges() -> (ComponentGraphBuilder, ComponentHandle) {
128        let mut builder = ComponentGraphBuilder::new();
129
130        let grid_meter = builder.meter();
131        let meter_bat_chain = builder.meter_bat_chain(1, 1);
132        builder.connect(grid_meter, meter_bat_chain);
133
134        let meter_bat_chain = builder.meter_bat_chain(1, 1);
135        builder.connect(grid_meter, meter_bat_chain);
136
137        (builder, grid_meter)
138    }
139
140    #[test]
141    fn test_component_validation() {
142        let (mut builder, grid_meter) = nodes_and_edges();
143
144        assert!(builder
145            .build(None)
146            .is_err_and(|e| e == Error::invalid_graph("No grid component found.")),);
147
148        let grid = builder.grid();
149        builder.connect(grid, grid_meter);
150
151        assert!(builder.build(None).is_ok());
152
153        builder.add_component_with_id(2, ComponentCategory::Meter);
154        assert!(builder
155            .build(None)
156            .is_err_and(|e| e == Error::invalid_graph("Duplicate component ID found: 2")));
157
158        builder.pop_component();
159        builder.add_component(ComponentCategory::Unspecified);
160        assert!(builder
161            .build(None)
162            .is_err_and(|e| e
163                == Error::invalid_component("ComponentCategory not specified for component: 8")));
164
165        builder.pop_component();
166        let unspec_inv =
167            builder.add_component(ComponentCategory::Inverter(InverterType::Unspecified));
168        builder.connect(grid_meter, unspec_inv);
169
170        // With default config, unspecified inverter types are not accepted.
171        assert!(builder.build(None).is_err_and(
172            |e| e == Error::invalid_component("InverterType not specified for inverter: 9")
173        ));
174        // With `allow_unspecified_inverters=true`, unspecified inverter types
175        // are treated as battery inverters.
176        let unspec_inv_config = ComponentGraphConfig {
177            allow_unspecified_inverters: true,
178            ..Default::default()
179        };
180
181        assert!(builder.build(Some(unspec_inv_config.clone())).is_ok());
182
183        assert!(builder
184            .pop_component()
185            .unwrap()
186            .is_battery_inverter(&unspec_inv_config));
187        builder.pop_connection();
188        builder.add_component(ComponentCategory::GridConnectionPoint);
189        assert!(builder
190            .build(None)
191            .is_err_and(|e| e == Error::invalid_graph("Multiple grid components found.")));
192
193        builder.pop_component();
194        assert!(builder.build(None).is_ok());
195    }
196
197    #[test]
198    fn test_connection_validation() {
199        let (mut builder, grid_meter) = nodes_and_edges();
200
201        let grid = builder.grid();
202        builder.connect(grid, grid_meter);
203
204        builder.connect(grid, grid);
205        assert!(builder.build(None).is_err_and(|e| e
206            == Error::invalid_connection(
207                "Connection:(7, 7) Can't connect a component to itself."
208            )));
209        builder.pop_connection();
210
211        builder.connect(grid_meter, ComponentHandle::new(9));
212        assert!(builder.build(None).is_err_and(|e| e
213            == Error::invalid_connection("Connection:(0, 9) Can't find a component with ID 9")));
214
215        builder.pop_connection();
216        assert!(builder.build(None).is_ok());
217    }
218}