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