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