Skip to main content

netoptim_rs/
utils.rs

1//! Utility functions for graph operations
2
3use petgraph::graph::{DiGraph, Graph, NodeIndex};
4use petgraph::visit::{EdgeRef, IntoNodeIdentifiers};
5use petgraph::Directed;
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8
9/// Compare two graphs for structural equality.
10///
11/// Returns `true` if both graphs have the same nodes and edges with the same weights.
12pub fn graphs_equal<N, E, Ty>(g1: &Graph<N, E, Ty>, g2: &Graph<N, E, Ty>) -> bool
13where
14    N: PartialEq,
15    E: PartialEq,
16    Ty: petgraph::EdgeType,
17{
18    // Compare node count
19    if g1.node_count() != g2.node_count() {
20        return false;
21    }
22
23    // Compare edge count
24    if g1.edge_count() != g2.edge_count() {
25        return false;
26    }
27
28    // Compare node weights
29    for node in g1.node_identifiers() {
30        let weight1 = g1.node_weight(node);
31        let weight2 = g2.node_weight(node);
32        if weight1 != weight2 {
33            return false;
34        }
35    }
36
37    // Compare edges (for undirected graphs, this checks both directions)
38    for edge in g1.edge_references() {
39        let (source, target) = (edge.source(), edge.target());
40        let weight = edge.weight();
41
42        // Find corresponding edge in g2
43        let mut found = false;
44        for edge2 in g2.edge_references() {
45            if edge2.source() == source && edge2.target() == target && edge2.weight() == weight {
46                found = true;
47                break;
48            }
49        }
50        if !found {
51            return false;
52        }
53    }
54
55    true
56}
57
58/// Check if a graph contains any cycles.
59///
60/// Uses petgraph's `is_cyclic_directed` algorithm.
61pub fn has_cycle<N, E>(g: &DiGraph<N, E>) -> bool
62where
63    N: Clone,
64{
65    use petgraph::algo::is_cyclic_directed;
66    is_cyclic_directed(g)
67}
68
69/// Get all nodes reachable from a given source node.
70///
71/// Uses breadth-first search (BFS) to traverse the graph.
72pub fn get_reachable_nodes<N, E>(g: &DiGraph<N, E>, source: NodeIndex) -> HashSet<NodeIndex>
73where
74    N: Clone,
75{
76    use petgraph::visit::Bfs;
77
78    let mut reachable = HashSet::new();
79    let mut bfs = Bfs::new(g, source);
80
81    while let Some(node) = bfs.next(g) {
82        reachable.insert(node);
83    }
84
85    reachable
86}
87
88/// Check if a directed graph is strongly connected.
89///
90/// A graph is strongly connected if there is a path from every node to every other node.
91pub fn is_strongly_connected<N, E>(g: &DiGraph<N, E>) -> bool
92where
93    N: Clone,
94{
95    if g.node_count() == 0 {
96        return false;
97    }
98
99    use petgraph::visit::{Dfs, VisitMap, Visitable};
100    use petgraph::Direction;
101
102    // Check if all nodes are reachable from the first node
103    let start = g.node_indices().next().unwrap();
104    let mut dfs = Dfs::new(g, start);
105    let mut reachable = 0;
106    while dfs.next(g).is_some() {
107        reachable += 1;
108    }
109
110    if reachable != g.node_count() {
111        return false;
112    }
113
114    // Check reverse connectivity using transpose graph
115    let _dfs = Dfs::new(&g, start);
116    let mut reverse_reachable = 0;
117
118    // Manual DFS on reverse edges
119    let mut visited = g.visit_map();
120    let mut stack = vec![start];
121    while let Some(node) = stack.pop() {
122        if visited.is_visited(&node) {
123            continue;
124        }
125        visited.visit(node);
126        reverse_reachable += 1;
127
128        for neighbor in g.neighbors_directed(node, Direction::Incoming) {
129            if !visited.is_visited(&neighbor) {
130                stack.push(neighbor);
131            }
132        }
133    }
134
135    reverse_reachable == g.node_count()
136}
137
138/// Count the number of connected components in an undirected graph.
139pub fn count_connected_components<N, E>(g: &Graph<N, E>) -> usize
140where
141    N: Clone,
142{
143    use petgraph::algo::connected_components;
144    connected_components(g)
145}
146
147/// Get the degree (number of edges) of each node in an undirected graph.
148pub fn get_node_degrees<N, E, Ty>(g: &Graph<N, E, Ty>) -> Vec<usize>
149where
150    Ty: petgraph::EdgeType,
151{
152    g.node_indices().map(|node| g.edges(node).count()).collect()
153}
154
155/// Get the in-degree and out-degree of each node in a directed graph.
156///
157/// Returns a vector of tuples `(in_degree, out_degree)` for each node.
158pub fn get_in_out_degrees<N, E>(g: &DiGraph<N, E>) -> Vec<(usize, usize)> {
159    g.node_indices()
160        .map(|node| {
161            let in_degree = g
162                .edges_directed(node, petgraph::Direction::Incoming)
163                .count();
164            let out_degree = g
165                .edges_directed(node, petgraph::Direction::Outgoing)
166                .count();
167            (in_degree, out_degree)
168        })
169        .collect()
170}
171
172/// Serialize a graph to JSON format.
173///
174/// Returns a JSON string representation of the graph.
175pub fn serialize_graph<N, E, Ty>(g: &Graph<N, E, Ty>) -> Result<String, serde_json::Error>
176where
177    N: Serialize + Clone,
178    E: Serialize + Clone,
179    Ty: petgraph::EdgeType + 'static,
180{
181    #[derive(Serialize)]
182    #[allow(dead_code)]
183    struct GraphJSON<N, E> {
184        nodes: Vec<(usize, N)>,
185        edges: Vec<(usize, usize, E)>,
186        directed: bool,
187    }
188
189    let nodes: Vec<(usize, N)> = g
190        .node_indices()
191        .map(|node| (node.index(), g[node].clone()))
192        .collect();
193
194    let edges: Vec<(usize, usize, E)> = g
195        .edge_indices()
196        .map(|edge| {
197            let (source, target) = g.edge_endpoints(edge).unwrap();
198            (source.index(), target.index(), g[edge].clone())
199        })
200        .collect();
201
202    let graph_json = GraphJSON {
203        nodes,
204        edges,
205        directed: false,
206    };
207
208    serde_json::to_string_pretty(&graph_json)
209}
210
211/// Deserialize a graph from JSON format.
212///
213/// Takes a JSON string and returns a Graph.
214pub fn deserialize_graph<N, E>(json: &str) -> Result<Graph<N, E>, serde_json::Error>
215where
216    N: for<'de> Deserialize<'de>,
217    E: for<'de> Deserialize<'de>,
218{
219    #[derive(Deserialize)]
220    #[allow(dead_code)]
221    struct GraphJSON<N, E> {
222        nodes: Vec<(usize, N)>,
223        edges: Vec<(usize, usize, E)>,
224        directed: bool,
225    }
226
227    let graph_json: GraphJSON<N, E> = serde_json::from_str(json)?;
228
229    let mut graph = Graph::new();
230    let mut node_indices = Vec::new();
231
232    // Add nodes
233    for (_original_index, weight) in graph_json.nodes {
234        let idx = graph.add_node(weight);
235        node_indices.push(idx);
236    }
237
238    // Add edges
239    for (source_idx, target_idx, weight) in graph_json.edges {
240        let source = node_indices[source_idx];
241        let target = node_indices[target_idx];
242        graph.add_edge(source, target, weight);
243    }
244
245    Ok(graph)
246}
247
248/// Generate a DOT format representation of the graph for visualization with Graphviz.
249pub fn to_dot<N, E, Ty>(g: &Graph<N, E, Ty>) -> String
250where
251    N: std::fmt::Display,
252    E: std::fmt::Display,
253    Ty: petgraph::EdgeType + 'static,
254{
255    let is_directed = std::any::TypeId::of::<Ty>() == std::any::TypeId::of::<Directed>();
256    let edge_connector = if is_directed { "->" } else { "--" };
257
258    let mut dot = String::new();
259
260    if is_directed {
261        dot.push_str("digraph G {\n");
262    } else {
263        dot.push_str("graph G {\n");
264    }
265
266    // Add nodes
267    for node in g.node_indices() {
268        dot.push_str(&format!("    {} [label=\"{}\"];", node.index(), g[node]));
269    }
270
271    // Add edges
272    for edge in g.edge_references() {
273        let (source, target) = (edge.source(), edge.target());
274        dot.push_str(&format!(
275            "    {} {} {} [label=\"{}\"];",
276            source.index(),
277            edge_connector,
278            target.index(),
279            edge.weight()
280        ));
281    }
282
283    dot.push('}');
284    dot
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_graphs_equal() {
293        let g1 = Graph::<(), i32>::from_edges([(0, 1, 5), (1, 2, 3)]);
294        let g2 = Graph::<(), i32>::from_edges([(0, 1, 5), (1, 2, 3)]);
295        let g3 = Graph::<(), i32>::from_edges([(0, 1, 5), (1, 2, 4)]);
296
297        assert!(graphs_equal(&g1, &g2));
298        assert!(!graphs_equal(&g1, &g3));
299    }
300
301    #[test]
302    fn test_has_cycle() {
303        let cyclic = DiGraph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1), (2, 0, 1)]);
304        let acyclic = DiGraph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1)]);
305
306        assert!(has_cycle(&cyclic));
307        assert!(!has_cycle(&acyclic));
308    }
309
310    #[test]
311    fn test_get_reachable_nodes() {
312        let graph = DiGraph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1), (0, 3, 1), (4, 5, 1)]);
313
314        let reachable = get_reachable_nodes(&graph, NodeIndex::new(0));
315        assert_eq!(reachable.len(), 4);
316        assert!(reachable.contains(&NodeIndex::new(0)));
317        assert!(reachable.contains(&NodeIndex::new(1)));
318        assert!(reachable.contains(&NodeIndex::new(2)));
319        assert!(reachable.contains(&NodeIndex::new(3)));
320    }
321
322    #[test]
323    fn test_is_strongly_connected() {
324        let strongly_connected = DiGraph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1), (2, 0, 1)]);
325
326        let not_strongly_connected = DiGraph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1)]);
327
328        assert!(is_strongly_connected(&strongly_connected));
329        assert!(!is_strongly_connected(&not_strongly_connected));
330    }
331
332    #[test]
333    fn test_count_connected_components() {
334        let graph = Graph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1), (3, 4, 1)]);
335
336        assert_eq!(count_connected_components(&graph), 2);
337    }
338
339    #[test]
340    fn test_get_node_degrees() {
341        use petgraph::Undirected;
342        let graph = Graph::<(), i32, Undirected>::from_edges([(0, 1, 1), (1, 2, 1)]);
343        let degrees = get_node_degrees(&graph);
344
345        assert_eq!(degrees[0], 1);
346        assert_eq!(degrees[1], 2);
347        assert_eq!(degrees[2], 1);
348    }
349
350    #[test]
351    fn test_get_in_out_degrees() {
352        let graph = DiGraph::<(), i32>::from_edges([(0, 1, 1), (1, 2, 1), (2, 1, 1)]);
353
354        let degrees = get_in_out_degrees(&graph);
355        assert_eq!(degrees[0], (0, 1));
356        assert_eq!(degrees[1], (2, 1));
357        assert_eq!(degrees[2], (1, 1));
358    }
359
360    #[test]
361    fn test_serialize_deserialize_graph() {
362        let graph = Graph::<i32, f64>::from_edges([(0, 1, 1.5), (1, 2, 2.5)]);
363
364        let json = serialize_graph(&graph).unwrap();
365        let deserialized = deserialize_graph::<i32, f64>(&json).unwrap();
366
367        assert!(graphs_equal(&graph, &deserialized));
368    }
369
370    #[test]
371    fn test_to_dot() {
372        let graph = DiGraph::<i32, f64>::from_edges([(0, 1, 1.5), (1, 2, 2.5)]);
373        let dot = to_dot(&graph);
374
375        assert!(dot.contains("digraph G"));
376        assert!(dot.contains("->"));
377    }
378}