Skip to main content

scirs2_graph/partitioning/
types.rs

1//! Types for graph partitioning algorithms.
2//!
3//! This module defines configuration and result types used across
4//! spectral, multilevel, and streaming graph partitioning methods.
5
6use std::fmt;
7
8/// Method used for graph partitioning.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum PartitionMethod {
12    /// Spectral bisection via Fiedler vector of the graph Laplacian.
13    SpectralBisection,
14    /// METIS-style multilevel partitioning with Kernighan-Lin refinement.
15    MultilevelKL,
16    /// Linear Deterministic Greedy streaming partitioner for very large graphs.
17    Streaming,
18}
19
20impl fmt::Display for PartitionMethod {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::SpectralBisection => write!(f, "SpectralBisection"),
24            Self::MultilevelKL => write!(f, "MultilevelKL"),
25            Self::Streaming => write!(f, "Streaming"),
26        }
27    }
28}
29
30/// Configuration for graph partitioning.
31#[derive(Debug, Clone)]
32pub struct PartitionConfig {
33    /// Number of partitions (k-way). Must be >= 2.
34    pub n_partitions: usize,
35    /// Maximum allowed imbalance ratio (0.03 = 3% deviation from perfect balance).
36    pub balance_tolerance: f64,
37    /// Stop coarsening when the graph has fewer nodes than this threshold.
38    pub coarsening_threshold: usize,
39    /// Maximum number of Kernighan-Lin refinement passes per uncoarsening level.
40    pub kl_max_passes: usize,
41    /// Partitioning method to use.
42    pub method: PartitionMethod,
43}
44
45impl Default for PartitionConfig {
46    fn default() -> Self {
47        Self {
48            n_partitions: 2,
49            balance_tolerance: 0.03,
50            coarsening_threshold: 100,
51            kl_max_passes: 10,
52            method: PartitionMethod::MultilevelKL,
53        }
54    }
55}
56
57/// Result of a graph partitioning operation.
58#[derive(Debug, Clone)]
59pub struct PartitionResult {
60    /// Partition assignment for each node (0-indexed partition IDs).
61    pub assignments: Vec<usize>,
62    /// Number of edges crossing partition boundaries.
63    pub edge_cut: usize,
64    /// Number of nodes in each partition.
65    pub partition_sizes: Vec<usize>,
66    /// Maximum imbalance: max deviation from perfect balance as a ratio.
67    /// For example, 0.05 means the largest partition is 5% larger than ideal.
68    pub imbalance: f64,
69}
70
71impl PartitionResult {
72    /// Compute a `PartitionResult` from assignments and an adjacency matrix.
73    pub(crate) fn from_assignments(
74        assignments: &[usize],
75        adj: &scirs2_core::ndarray::Array2<f64>,
76        n_partitions: usize,
77    ) -> Self {
78        let n = assignments.len();
79        let mut partition_sizes = vec![0usize; n_partitions];
80        for &a in assignments {
81            if a < n_partitions {
82                partition_sizes[a] += 1;
83            }
84        }
85
86        // Edge cut: count edges crossing partitions (each undirected edge counted once)
87        let mut edge_cut = 0usize;
88        for i in 0..n {
89            for j in (i + 1)..n {
90                if adj[[i, j]].abs() > 1e-15 && assignments[i] != assignments[j] {
91                    edge_cut += 1;
92                }
93            }
94        }
95
96        // Imbalance: max deviation from perfect balance
97        let ideal = n as f64 / n_partitions as f64;
98        let imbalance = if ideal > 0.0 {
99            partition_sizes
100                .iter()
101                .map(|&s| ((s as f64) - ideal).abs() / ideal)
102                .fold(0.0f64, f64::max)
103        } else {
104            0.0
105        };
106
107        Self {
108            assignments: assignments.to_vec(),
109            edge_cut,
110            partition_sizes,
111            imbalance,
112        }
113    }
114}
115
116impl fmt::Display for PartitionResult {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(
119            f,
120            "PartitionResult {{ partitions: {}, edge_cut: {}, sizes: {:?}, imbalance: {:.4} }}",
121            self.partition_sizes.len(),
122            self.edge_cut,
123            self.partition_sizes,
124            self.imbalance
125        )
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use scirs2_core::ndarray::Array2;
133
134    #[test]
135    fn test_default_config() {
136        let cfg = PartitionConfig::default();
137        assert_eq!(cfg.n_partitions, 2);
138        assert!((cfg.balance_tolerance - 0.03).abs() < 1e-10);
139        assert_eq!(cfg.coarsening_threshold, 100);
140        assert_eq!(cfg.kl_max_passes, 10);
141        assert_eq!(cfg.method, PartitionMethod::MultilevelKL);
142    }
143
144    #[test]
145    fn test_partition_result_from_assignments() {
146        // 4-node path: 0-1-2-3
147        let mut adj = Array2::<f64>::zeros((4, 4));
148        adj[[0, 1]] = 1.0;
149        adj[[1, 0]] = 1.0;
150        adj[[1, 2]] = 1.0;
151        adj[[2, 1]] = 1.0;
152        adj[[2, 3]] = 1.0;
153        adj[[3, 2]] = 1.0;
154
155        let assignments = vec![0, 0, 1, 1];
156        let result = PartitionResult::from_assignments(&assignments, &adj, 2);
157        assert_eq!(result.partition_sizes, vec![2, 2]);
158        assert_eq!(result.edge_cut, 1); // only edge 1-2 crosses
159        assert!(result.imbalance < 1e-10); // perfectly balanced
160    }
161
162    #[test]
163    fn test_partition_method_display() {
164        assert_eq!(
165            format!("{}", PartitionMethod::SpectralBisection),
166            "SpectralBisection"
167        );
168        assert_eq!(format!("{}", PartitionMethod::MultilevelKL), "MultilevelKL");
169        assert_eq!(format!("{}", PartitionMethod::Streaming), "Streaming");
170    }
171
172    #[test]
173    fn test_partition_result_display() {
174        let result = PartitionResult {
175            assignments: vec![0, 0, 1, 1],
176            edge_cut: 1,
177            partition_sizes: vec![2, 2],
178            imbalance: 0.0,
179        };
180        let s = format!("{}", result);
181        assert!(s.contains("edge_cut: 1"));
182        assert!(s.contains("partitions: 2"));
183    }
184}