Skip to main content

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