scirs2_graph/
algorithms.rs

1//! Graph algorithms
2//!
3//! This module provides various algorithms for graph analysis and manipulation.
4//! The algorithms are organized into submodules by category:
5//!
6//! - `traversal`: BFS, DFS, and other traversal algorithms
7//! - `shortest_path`: Dijkstra, A*, Floyd-Warshall, etc.
8//! - `connectivity`: Connected components, articulation points, bridges
9//! - `flow`: Network flow and cut algorithms
10//! - `matching`: Bipartite matching algorithms
11//! - `coloring`: Graph coloring algorithms
12//! - `paths`: Eulerian and Hamiltonian path algorithms
13//! - `community`: Community detection algorithms
14//! - `decomposition`: Graph decomposition algorithms
15//! - `isomorphism`: Graph isomorphism and subgraph matching
16//! - `motifs`: Motif finding algorithms
17//! - `random_walk`: Random walk and PageRank algorithms
18//! - `similarity`: Node and graph similarity measures
19//! - `properties`: Graph properties like diameter, radius, center
20
21pub mod coloring;
22pub mod community;
23pub mod connectivity;
24pub mod decomposition;
25pub mod flow;
26pub mod hypergraph;
27pub mod isomorphism;
28pub mod matching;
29pub mod motifs;
30pub mod paths;
31pub mod properties;
32pub mod random_walk;
33pub mod shortest_path;
34pub mod similarity;
35pub mod transformations;
36pub mod traversal;
37
38// Re-export all public items for convenience
39pub use coloring::*;
40// Modern community detection APIs - stable for 1.0
41pub use community::{
42    // Standardized community detection algorithms - stable for 1.0
43    fluid_communities_result,
44    greedy_modularity_optimization_result,
45    hierarchical_communities_result,
46    infomap_communities,
47    label_propagation_result,
48    louvain_communities_result,
49    modularity,
50    modularity_optimization_result,
51    // Standardized result types - stable for 1.0
52    CommunityResult,
53    CommunityStructure,
54    InfomapResult,
55};
56
57#[cfg(feature = "parallel")]
58pub use community::{
59    parallel_label_propagation_result, parallel_louvain_communities_result, parallel_modularity,
60};
61
62// Legacy community detection APIs - deprecated
63#[deprecated(note = "Use `louvain_communities_result` instead")]
64#[allow(deprecated)]
65pub use community::louvain_communities;
66
67#[deprecated(note = "Use `label_propagation_result` instead")]
68#[allow(deprecated)]
69pub use community::label_propagation;
70
71#[deprecated(note = "Use `fluid_communities_result` instead")]
72#[allow(deprecated)]
73pub use community::fluid_communities;
74
75#[deprecated(note = "Use `hierarchical_communities_result` instead")]
76#[allow(deprecated)]
77pub use community::hierarchical_communities;
78
79#[deprecated(note = "Use `modularity_optimization_result` instead")]
80#[allow(deprecated)]
81pub use community::modularity_optimization;
82
83#[deprecated(note = "Use `greedy_modularity_optimization_result` instead")]
84#[allow(deprecated)]
85pub use community::greedy_modularity_optimization;
86
87#[deprecated(note = "Use `parallel_louvain_communities_result` instead")]
88#[allow(deprecated)]
89pub use community::parallel_louvain_communities;
90pub use connectivity::*;
91pub use decomposition::*;
92pub use flow::{dinic_max_flow, minimum_cut, push_relabel_max_flow};
93pub use hypergraph::*;
94pub use isomorphism::*;
95pub use matching::*;
96pub use motifs::*;
97pub use paths::*;
98pub use properties::*;
99pub use random_walk::*;
100pub use shortest_path::*;
101pub use similarity::*;
102pub use transformations::*;
103pub use traversal::*;
104
105// Additional algorithms that haven't been moved to submodules yet
106
107use crate::base::{DiGraph, EdgeWeight, Graph, IndexType, Node};
108use crate::error::{GraphError, Result};
109use petgraph::algo::toposort as petgraph_toposort;
110use petgraph::visit::EdgeRef;
111use petgraph::Direction;
112use scirs2_core::ndarray::{Array1, Array2};
113use std::cmp::Ordering;
114use std::collections::{HashMap, VecDeque};
115
116/// Kruskal's algorithm for finding minimum spanning tree
117///
118/// Returns a vector of edges that form the minimum spanning tree.
119/// Only works on undirected graphs.
120#[allow(dead_code)]
121pub fn minimum_spanning_tree<N, E, Ix>(
122    graph: &Graph<N, E, Ix>,
123) -> Result<Vec<crate::base::Edge<N, E>>>
124where
125    N: Node + std::fmt::Debug,
126    E: EdgeWeight + Into<f64> + std::cmp::PartialOrd,
127    Ix: petgraph::graph::IndexType,
128{
129    // Get all edges and sort by weight
130    let mut edges: Vec<_> = graph
131        .inner()
132        .edge_references()
133        .map(|e| {
134            let source = graph.inner()[e.source()].clone();
135            let target = graph.inner()[e.target()].clone();
136            let weight = e.weight().clone();
137            (source, target, weight)
138        })
139        .collect();
140
141    edges.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(Ordering::Equal));
142
143    // Use Union-Find to detect cycles
144    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
145    let mut parent: HashMap<N, N> = nodes.iter().map(|n| (n.clone(), n.clone())).collect();
146    let mut rank: HashMap<N, usize> = nodes.iter().map(|n| (n.clone(), 0)).collect();
147
148    fn find<N: Node>(parent: &mut HashMap<N, N>, node: &N) -> N {
149        if parent[node] != *node {
150            let root = find(parent, &parent[node].clone());
151            parent.insert(node.clone(), root.clone());
152        }
153        parent[node].clone()
154    }
155
156    fn union<N: Node>(
157        parent: &mut HashMap<N, N>,
158        rank: &mut HashMap<N, usize>,
159        x: &N,
160        y: &N,
161    ) -> bool {
162        let root_x = find(parent, x);
163        let root_y = find(parent, y);
164
165        if root_x == root_y {
166            return false; // Already in same set
167        }
168
169        // Union by rank
170        match rank[&root_x].cmp(&rank[&root_y]) {
171            Ordering::Less => {
172                parent.insert(root_x, root_y);
173            }
174            Ordering::Greater => {
175                parent.insert(root_y, root_x);
176            }
177            Ordering::Equal => {
178                parent.insert(root_y, root_x.clone());
179                *rank.get_mut(&root_x).expect("Operation failed") += 1;
180            }
181        }
182        true
183    }
184
185    let mut mst = Vec::new();
186
187    for (source, target, weight) in edges {
188        if union(&mut parent, &mut rank, &source, &target) {
189            mst.push(crate::base::Edge {
190                source,
191                target,
192                weight,
193            });
194        }
195    }
196
197    Ok(mst)
198}
199
200/// Topological sort for directed acyclic graphs
201///
202/// Returns nodes in topological order if the graph is a DAG,
203/// otherwise returns an error indicating a cycle was found.
204#[allow(dead_code)]
205pub fn topological_sort<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<Vec<N>>
206where
207    N: Node + std::fmt::Debug,
208    E: EdgeWeight,
209    Ix: IndexType,
210{
211    // Use petgraph's topological sort
212    match petgraph_toposort(graph.inner(), None) {
213        Ok(indices) => {
214            let sorted_nodes = indices
215                .into_iter()
216                .map(|idx| graph.inner()[idx].clone())
217                .collect();
218            Ok(sorted_nodes)
219        }
220        Err(_) => Err(GraphError::CycleDetected {
221            start_node: "unknown".to_string(),
222            cycle_length: 0,
223        }),
224    }
225}
226
227/// PageRank algorithm for computing node importance
228///
229/// Returns a map from nodes to their PageRank scores.
230#[allow(dead_code)]
231pub fn pagerank<N, E, Ix>(
232    graph: &DiGraph<N, E, Ix>,
233    damping_factor: f64,
234    tolerance: f64,
235    max_iterations: usize,
236) -> HashMap<N, f64>
237where
238    N: Node + std::fmt::Debug,
239    E: EdgeWeight,
240    Ix: IndexType,
241{
242    let nodes: Vec<_> = graph.inner().node_indices().collect();
243    let n = nodes.len();
244
245    if n == 0 {
246        return HashMap::new();
247    }
248
249    // Initialize PageRank values
250    let mut pr = vec![1.0 / n as f64; n];
251    let mut new_pr = vec![0.0; n];
252
253    // Create node index mapping
254    let node_to_idx: HashMap<_, _> = nodes.iter().enumerate().map(|(i, &n)| (n, i)).collect();
255
256    // Iterate until convergence
257    for _ in 0..max_iterations {
258        // Reset new PageRank values
259        for pr in new_pr.iter_mut().take(n) {
260            *pr = (1.0 - damping_factor) / n as f64;
261        }
262
263        // Calculate contributions from incoming edges
264        for (i, &node_idx) in nodes.iter().enumerate() {
265            let out_degree = graph
266                .inner()
267                .edges_directed(node_idx, Direction::Outgoing)
268                .count();
269
270            if out_degree > 0 {
271                let contribution = damping_factor * pr[i] / out_degree as f64;
272
273                for edge in graph.inner().edges_directed(node_idx, Direction::Outgoing) {
274                    if let Some(&j) = node_to_idx.get(&edge.target()) {
275                        new_pr[j] += contribution;
276                    }
277                }
278            } else {
279                // Dangling node: distribute equally to all nodes
280                let contribution = damping_factor * pr[i] / n as f64;
281                for pr_val in new_pr.iter_mut().take(n) {
282                    *pr_val += contribution;
283                }
284            }
285        }
286
287        // Check convergence
288        let diff: f64 = pr
289            .iter()
290            .zip(&new_pr)
291            .map(|(old, new)| (old - new).abs())
292            .sum();
293
294        // Swap vectors
295        std::mem::swap(&mut pr, &mut new_pr);
296
297        if diff < tolerance {
298            break;
299        }
300    }
301
302    // Convert to HashMap
303    nodes
304        .iter()
305        .enumerate()
306        .map(|(i, &node_idx)| (graph.inner()[node_idx].clone(), pr[i]))
307        .collect()
308}
309
310/// Betweenness centrality for nodes
311///
312/// Measures the extent to which a node lies on paths between other nodes.
313#[allow(dead_code)]
314pub fn betweenness_centrality<N, E, Ix>(
315    graph: &Graph<N, E, Ix>,
316    normalized: bool,
317) -> HashMap<N, f64>
318where
319    N: Node + std::fmt::Debug,
320    E: EdgeWeight,
321    Ix: IndexType,
322{
323    let node_indices: Vec<_> = graph.inner().node_indices().collect();
324    let nodes: Vec<N> = node_indices
325        .iter()
326        .map(|&idx| graph.inner()[idx].clone())
327        .collect();
328    let n = nodes.len();
329    let mut centrality: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
330
331    // For each source node
332    for s in &nodes {
333        // Single-source shortest paths
334        let mut stack = Vec::new();
335        let mut paths: HashMap<N, Vec<N>> = HashMap::new();
336        let mut sigma: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
337        let mut dist: HashMap<N, Option<f64>> = nodes.iter().map(|n| (n.clone(), None)).collect();
338
339        sigma.insert(s.clone(), 1.0);
340        dist.insert(s.clone(), Some(0.0));
341
342        let mut queue = VecDeque::new();
343        queue.push_back(s.clone());
344
345        // BFS
346        while let Some(v) = queue.pop_front() {
347            stack.push(v.clone());
348
349            if let Ok(neighbors) = graph.neighbors(&v) {
350                for w in neighbors {
351                    // First time we reach w?
352                    if dist[&w].is_none() {
353                        dist.insert(w.clone(), Some(dist[&v].expect("Operation failed") + 1.0));
354                        queue.push_back(w.clone());
355                    }
356
357                    // Shortest path to w via v?
358                    if dist[&w] == Some(dist[&v].expect("Operation failed") + 1.0) {
359                        *sigma.get_mut(&w).expect("Operation failed") += sigma[&v];
360                        paths.entry(w.clone()).or_default().push(v.clone());
361                    }
362                }
363            }
364        }
365
366        // Accumulation
367        let mut delta: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
368
369        while let Some(w) = stack.pop() {
370            if let Some(predecessors) = paths.get(&w) {
371                for v in predecessors {
372                    *delta.get_mut(v).expect("Operation failed") +=
373                        (sigma[v] / sigma[&w]) * (1.0 + delta[&w]);
374                }
375            }
376
377            if w != *s {
378                *centrality.get_mut(&w).expect("Operation failed") += delta[&w];
379            }
380        }
381    }
382
383    // Normalization
384    if normalized && n > 2 {
385        let scale = 1.0 / ((n - 1) * (n - 2)) as f64;
386        for value in centrality.values_mut() {
387            *value *= scale;
388        }
389    }
390
391    centrality
392}
393
394/// Closeness centrality for nodes
395///
396/// Measures how close a node is to all other nodes in the graph.
397#[allow(dead_code)]
398pub fn closeness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>, normalized: bool) -> HashMap<N, f64>
399where
400    N: Node + std::fmt::Debug,
401    E: EdgeWeight
402        + Into<f64>
403        + scirs2_core::numeric::Zero
404        + scirs2_core::numeric::One
405        + std::ops::Add<Output = E>
406        + PartialOrd
407        + std::marker::Copy
408        + std::fmt::Debug
409        + std::default::Default,
410    Ix: IndexType,
411{
412    let node_indices: Vec<_> = graph.inner().node_indices().collect();
413    let nodes: Vec<N> = node_indices
414        .iter()
415        .map(|&idx| graph.inner()[idx].clone())
416        .collect();
417    let n = nodes.len();
418    let mut centrality = HashMap::new();
419
420    for node in &nodes {
421        let mut total_distance = 0.0;
422        let mut reachable_count = 0;
423
424        // Calculate shortest paths to all other nodes
425        for other in &nodes {
426            if node != other {
427                if let Ok(Some(path)) = dijkstra_path(graph, node, other) {
428                    let distance: f64 = path.total_weight.into();
429                    total_distance += distance;
430                    reachable_count += 1;
431                }
432            }
433        }
434
435        if reachable_count > 0 {
436            let closeness = reachable_count as f64 / total_distance;
437            let value = if normalized && n > 1 {
438                closeness * (reachable_count as f64 / (n - 1) as f64)
439            } else {
440                closeness
441            };
442            centrality.insert(node.clone(), value);
443        } else {
444            centrality.insert(node.clone(), 0.0);
445        }
446    }
447
448    centrality
449}
450
451/// Eigenvector centrality
452///
453/// Computes the eigenvector centrality of nodes using power iteration.
454#[allow(dead_code)]
455pub fn eigenvector_centrality<N, E, Ix>(
456    graph: &Graph<N, E, Ix>,
457    max_iter: usize,
458    tolerance: f64,
459) -> Result<HashMap<N, f64>>
460where
461    N: Node + std::fmt::Debug,
462    E: EdgeWeight + Into<f64>,
463    Ix: IndexType,
464{
465    let node_indices: Vec<_> = graph.inner().node_indices().collect();
466    let nodes: Vec<N> = node_indices
467        .iter()
468        .map(|&idx| graph.inner()[idx].clone())
469        .collect();
470    let n = nodes.len();
471
472    if n == 0 {
473        return Ok(HashMap::new());
474    }
475
476    // Create adjacency matrix
477    let mut adj_matrix = Array2::<f64>::zeros((n, n));
478    for (i, node_i) in nodes.iter().enumerate() {
479        for (j, node_j) in nodes.iter().enumerate() {
480            if let Ok(weight) = graph.edge_weight(node_i, node_j) {
481                adj_matrix[[i, j]] = weight.into();
482            }
483        }
484    }
485
486    // Initialize eigenvector
487    let mut x = Array1::<f64>::from_elem(n, 1.0 / (n as f64).sqrt());
488    let mut converged = false;
489
490    // Power iteration
491    for _ in 0..max_iter {
492        let x_new = adj_matrix.dot(&x);
493
494        // Normalize
495        let norm = x_new.dot(&x_new).sqrt();
496        if norm == 0.0 {
497            return Err(GraphError::ComputationError(
498                "Eigenvector computation failed".to_string(),
499            ));
500        }
501
502        let x_normalized = x_new / norm;
503
504        // Check convergence
505        let diff = (&x_normalized - &x).mapv(f64::abs).sum();
506        if diff < tolerance {
507            converged = true;
508            x = x_normalized;
509            break;
510        }
511
512        x = x_normalized;
513    }
514
515    if !converged {
516        return Err(GraphError::ComputationError(
517            "Eigenvector centrality did not converge".to_string(),
518        ));
519    }
520
521    // Convert to HashMap
522    Ok(nodes
523        .into_iter()
524        .enumerate()
525        .map(|(i, node)| (node, x[i]))
526        .collect())
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532    use crate::generators::create_graph;
533
534    #[test]
535    fn test_minimum_spanning_tree() {
536        let mut graph = create_graph::<&str, f64>();
537        graph.add_edge("A", "B", 1.0).expect("Operation failed");
538        graph.add_edge("B", "C", 2.0).expect("Operation failed");
539        graph.add_edge("A", "C", 3.0).expect("Operation failed");
540        graph.add_edge("C", "D", 1.0).expect("Operation failed");
541
542        let mst = minimum_spanning_tree(&graph).expect("Operation failed");
543
544        // MST should have n-1 edges
545        assert_eq!(mst.len(), 3);
546
547        // Total weight should be 4.0 (AB: 1, BC: 2, CD: 1)
548        let total_weight: f64 = mst.iter().map(|e| e.weight).sum();
549        assert_eq!(total_weight, 4.0);
550    }
551
552    #[test]
553    fn test_topological_sort() {
554        let mut graph = crate::generators::create_digraph::<&str, ()>();
555        graph.add_edge("A", "B", ()).expect("Operation failed");
556        graph.add_edge("A", "C", ()).expect("Operation failed");
557        graph.add_edge("B", "D", ()).expect("Operation failed");
558        graph.add_edge("C", "D", ()).expect("Operation failed");
559
560        let sorted = topological_sort(&graph).expect("Operation failed");
561
562        // A should come before B and C
563        let a_pos = sorted
564            .iter()
565            .position(|n| n == &"A")
566            .expect("Operation failed");
567        let b_pos = sorted
568            .iter()
569            .position(|n| n == &"B")
570            .expect("Operation failed");
571        let c_pos = sorted
572            .iter()
573            .position(|n| n == &"C")
574            .expect("Operation failed");
575        let d_pos = sorted
576            .iter()
577            .position(|n| n == &"D")
578            .expect("Operation failed");
579
580        assert!(a_pos < b_pos);
581        assert!(a_pos < c_pos);
582        assert!(b_pos < d_pos);
583        assert!(c_pos < d_pos);
584    }
585
586    #[test]
587    fn test_pagerank() {
588        let mut graph = crate::generators::create_digraph::<&str, ()>();
589        graph.add_edge("A", "B", ()).expect("Operation failed");
590        graph.add_edge("A", "C", ()).expect("Operation failed");
591        graph.add_edge("B", "C", ()).expect("Operation failed");
592        graph.add_edge("C", "A", ()).expect("Operation failed");
593
594        let pr = pagerank(&graph, 0.85, 1e-6, 100);
595
596        // All nodes should have positive PageRank
597        assert!(pr.values().all(|&v| v > 0.0));
598
599        // Sum should be approximately 1.0
600        let sum: f64 = pr.values().sum();
601        assert!((sum - 1.0).abs() < 0.01);
602    }
603}