ruvector_math/spectral/
mod.rs

1//! Spectral Methods for Graph Analysis
2//!
3//! Chebyshev polynomials and spectral graph theory for efficient
4//! diffusion and filtering without eigendecomposition.
5//!
6//! ## Key Capabilities
7//!
8//! - **Chebyshev Graph Filtering**: O(Km) filtering where K is polynomial degree
9//! - **Graph Diffusion**: Heat kernel approximation via Chebyshev expansion
10//! - **Spectral Clustering**: Efficient k-way partitioning
11//! - **Wavelet Transforms**: Multi-scale graph analysis
12//!
13//! ## Integration with Mincut
14//!
15//! Spectral methods pair naturally with mincut:
16//! - Mincut identifies partition boundaries
17//! - Chebyshev smooths attention within partitions
18//! - Spectral clustering provides initial segmentation hints
19//!
20//! ## Mathematical Background
21//!
22//! Chebyshev polynomials T_k(x) satisfy:
23//! - T_0(x) = 1
24//! - T_1(x) = x
25//! - T_{k+1}(x) = 2x·T_k(x) - T_{k-1}(x)
26//!
27//! This recurrence enables O(K) evaluation of degree-K polynomial filters.
28
29mod chebyshev;
30mod graph_filter;
31mod wavelets;
32mod clustering;
33
34pub use chebyshev::{ChebyshevPolynomial, ChebyshevExpansion};
35pub use graph_filter::{SpectralFilter, FilterType, GraphFilter};
36pub use wavelets::{GraphWavelet, WaveletScale, SpectralWaveletTransform};
37pub use clustering::{SpectralClustering, ClusteringConfig};
38
39/// Scaled Laplacian for Chebyshev approximation
40/// L_scaled = 2L/λ_max - I (eigenvalues in [-1, 1])
41#[derive(Debug, Clone)]
42pub struct ScaledLaplacian {
43    /// Sparse representation: (row, col, value)
44    pub entries: Vec<(usize, usize, f64)>,
45    /// Matrix dimension
46    pub n: usize,
47    /// Estimated maximum eigenvalue
48    pub lambda_max: f64,
49}
50
51impl ScaledLaplacian {
52    /// Build from adjacency matrix (dense)
53    pub fn from_adjacency(adj: &[f64], n: usize) -> Self {
54        // Compute degree and Laplacian
55        let mut degrees = vec![0.0; n];
56        for i in 0..n {
57            for j in 0..n {
58                degrees[i] += adj[i * n + j];
59            }
60        }
61
62        // Build sparse Laplacian entries
63        let mut entries = Vec::new();
64        for i in 0..n {
65            // Diagonal: degree
66            if degrees[i] > 0.0 {
67                entries.push((i, i, degrees[i]));
68            }
69            // Off-diagonal: -adjacency
70            for j in 0..n {
71                if i != j && adj[i * n + j] != 0.0 {
72                    entries.push((i, j, -adj[i * n + j]));
73                }
74            }
75        }
76
77        // Estimate λ_max via power iteration
78        let lambda_max = Self::estimate_lambda_max(&entries, n, 20);
79
80        // Scale to [-1, 1]: L_scaled = 2L/λ_max - I
81        let scale = 2.0 / lambda_max;
82        let scaled_entries: Vec<(usize, usize, f64)> = entries
83            .iter()
84            .map(|&(i, j, v)| {
85                if i == j {
86                    (i, j, scale * v - 1.0)
87                } else {
88                    (i, j, scale * v)
89                }
90            })
91            .collect();
92
93        Self {
94            entries: scaled_entries,
95            n,
96            lambda_max,
97        }
98    }
99
100    /// Build from sparse adjacency list
101    pub fn from_sparse_adjacency(edges: &[(usize, usize, f64)], n: usize) -> Self {
102        // Compute degrees
103        let mut degrees = vec![0.0; n];
104        for &(i, j, w) in edges {
105            degrees[i] += w;
106            if i != j {
107                degrees[j] += w; // Symmetric
108            }
109        }
110
111        // Build Laplacian entries
112        let mut entries = Vec::new();
113        for i in 0..n {
114            if degrees[i] > 0.0 {
115                entries.push((i, i, degrees[i]));
116            }
117        }
118        for &(i, j, w) in edges {
119            if w != 0.0 {
120                entries.push((i, j, -w));
121                if i != j {
122                    entries.push((j, i, -w));
123                }
124            }
125        }
126
127        let lambda_max = Self::estimate_lambda_max(&entries, n, 20);
128        let scale = 2.0 / lambda_max;
129
130        let scaled_entries: Vec<(usize, usize, f64)> = entries
131            .iter()
132            .map(|&(i, j, v)| {
133                if i == j {
134                    (i, j, scale * v - 1.0)
135                } else {
136                    (i, j, scale * v)
137                }
138            })
139            .collect();
140
141        Self {
142            entries: scaled_entries,
143            n,
144            lambda_max,
145        }
146    }
147
148    /// Estimate maximum eigenvalue via power iteration
149    fn estimate_lambda_max(entries: &[(usize, usize, f64)], n: usize, iters: usize) -> f64 {
150        let mut x = vec![1.0 / (n as f64).sqrt(); n];
151        let mut lambda = 1.0;
152
153        for _ in 0..iters {
154            // y = L * x
155            let mut y = vec![0.0; n];
156            for &(i, j, v) in entries {
157                y[i] += v * x[j];
158            }
159
160            // Estimate eigenvalue
161            let mut dot = 0.0;
162            let mut norm_sq = 0.0;
163            for i in 0..n {
164                dot += x[i] * y[i];
165                norm_sq += y[i] * y[i];
166            }
167            lambda = dot;
168
169            // Normalize
170            let norm = norm_sq.sqrt().max(1e-15);
171            for i in 0..n {
172                x[i] = y[i] / norm;
173            }
174        }
175
176        lambda.abs().max(1.0)
177    }
178
179    /// Apply scaled Laplacian to vector: y = L_scaled * x
180    pub fn apply(&self, x: &[f64]) -> Vec<f64> {
181        let mut y = vec![0.0; self.n];
182        for &(i, j, v) in &self.entries {
183            if j < x.len() {
184                y[i] += v * x[j];
185            }
186        }
187        y
188    }
189
190    /// Get original (unscaled) maximum eigenvalue estimate
191    pub fn lambda_max(&self) -> f64 {
192        self.lambda_max
193    }
194}
195
196/// Normalized Laplacian (symmetric or random walk)
197#[derive(Debug, Clone, Copy, PartialEq)]
198pub enum LaplacianNorm {
199    /// Unnormalized: L = D - A
200    Unnormalized,
201    /// Symmetric: L_sym = D^{-1/2} L D^{-1/2}
202    Symmetric,
203    /// Random walk: L_rw = D^{-1} L
204    RandomWalk,
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_scaled_laplacian() {
213        // Simple 3-node path graph: 0 -- 1 -- 2
214        let adj = vec![
215            0.0, 1.0, 0.0,
216            1.0, 0.0, 1.0,
217            0.0, 1.0, 0.0,
218        ];
219
220        let laplacian = ScaledLaplacian::from_adjacency(&adj, 3);
221
222        assert_eq!(laplacian.n, 3);
223        assert!(laplacian.lambda_max > 0.0);
224
225        // Apply to vector
226        let x = vec![1.0, 0.0, -1.0];
227        let y = laplacian.apply(&x);
228        assert_eq!(y.len(), 3);
229    }
230
231    #[test]
232    fn test_sparse_laplacian() {
233        // Same path graph as sparse edges
234        let edges = vec![(0, 1, 1.0), (1, 2, 1.0)];
235        let laplacian = ScaledLaplacian::from_sparse_adjacency(&edges, 3);
236
237        assert_eq!(laplacian.n, 3);
238        assert!(laplacian.lambda_max > 0.0);
239    }
240}