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        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::ComponentCategory;
124    use crate::InverterType;
125    use crate::graph::test_utils::{ComponentGraphBuilder, ComponentHandle};
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!(
145            builder
146                .build(None)
147                .is_err_and(|e| e == Error::invalid_graph("No grid component found.")),
148        );
149
150        let grid = builder.grid();
151        builder.connect(grid, grid_meter);
152
153        assert!(builder.build(None).is_ok());
154
155        builder.add_component_with_id(2, ComponentCategory::Meter);
156        assert!(
157            builder
158                .build(None)
159                .is_err_and(|e| e == Error::invalid_graph("Duplicate component ID found: 2"))
160        );
161
162        builder.pop_component();
163        builder.add_component(ComponentCategory::Unspecified);
164        assert!(
165            builder.build(None).is_err_and(|e| e
166                == Error::invalid_component("ComponentCategory not specified for component: 8"))
167        );
168
169        builder.pop_component();
170        let unspec_inv =
171            builder.add_component(ComponentCategory::Inverter(InverterType::Unspecified));
172        builder.connect(grid_meter, unspec_inv);
173
174        // With default config, unspecified inverter types are not accepted.
175        assert!(builder.build(None).is_err_and(
176            |e| e == Error::invalid_component("InverterType not specified for inverter: 9")
177        ));
178        // With `allow_unspecified_inverters=true`, unspecified inverter types
179        // are treated as battery inverters.
180        let unspec_inv_config = ComponentGraphConfig {
181            allow_unspecified_inverters: true,
182            ..Default::default()
183        };
184
185        assert!(builder.build(Some(unspec_inv_config.clone())).is_ok());
186
187        assert!(
188            builder
189                .pop_component()
190                .unwrap()
191                .is_battery_inverter(&unspec_inv_config)
192        );
193        builder.pop_connection();
194        builder.add_component(ComponentCategory::GridConnectionPoint);
195        assert!(
196            builder
197                .build(None)
198                .is_err_and(|e| e == Error::invalid_graph("Multiple grid components found."))
199        );
200
201        builder.pop_component();
202        assert!(builder.build(None).is_ok());
203    }
204
205    #[test]
206    fn test_connection_validation() {
207        let (mut builder, grid_meter) = nodes_and_edges();
208
209        let grid = builder.grid();
210        builder.connect(grid, grid_meter);
211
212        builder.connect(grid, grid);
213        assert!(builder.build(None).is_err_and(|e| e
214            == Error::invalid_connection(
215                "Connection:(7, 7) Can't connect a component to itself."
216            )));
217        builder.pop_connection();
218
219        builder.connect(grid_meter, ComponentHandle::new(9));
220        assert!(builder.build(None).is_err_and(|e| e
221            == Error::invalid_connection("Connection:(0, 9) Can't find a component with ID 9")));
222
223        builder.pop_connection();
224        assert!(builder.build(None).is_ok());
225    }
226}