mod chebyshev;
mod clustering;
mod graph_filter;
mod wavelets;
pub use chebyshev::{ChebyshevExpansion, ChebyshevPolynomial};
pub use clustering::{ClusteringConfig, SpectralClustering};
pub use graph_filter::{FilterType, GraphFilter, SpectralFilter};
pub use wavelets::{GraphWavelet, SpectralWaveletTransform, WaveletScale};
#[derive(Debug, Clone)]
pub struct ScaledLaplacian {
pub entries: Vec<(usize, usize, f64)>,
pub n: usize,
pub lambda_max: f64,
}
impl ScaledLaplacian {
pub fn from_adjacency(adj: &[f64], n: usize) -> Self {
let mut degrees = vec![0.0; n];
for i in 0..n {
for j in 0..n {
degrees[i] += adj[i * n + j];
}
}
let mut entries = Vec::new();
for i in 0..n {
if degrees[i] > 0.0 {
entries.push((i, i, degrees[i]));
}
for j in 0..n {
if i != j && adj[i * n + j] != 0.0 {
entries.push((i, j, -adj[i * n + j]));
}
}
}
let lambda_max = Self::estimate_lambda_max(&entries, n, 20);
let scale = 2.0 / lambda_max;
let scaled_entries: Vec<(usize, usize, f64)> = entries
.iter()
.map(|&(i, j, v)| {
if i == j {
(i, j, scale * v - 1.0)
} else {
(i, j, scale * v)
}
})
.collect();
Self {
entries: scaled_entries,
n,
lambda_max,
}
}
pub fn from_sparse_adjacency(edges: &[(usize, usize, f64)], n: usize) -> Self {
let mut degrees = vec![0.0; n];
for &(i, j, w) in edges {
degrees[i] += w;
if i != j {
degrees[j] += w; }
}
let mut entries = Vec::new();
for i in 0..n {
if degrees[i] > 0.0 {
entries.push((i, i, degrees[i]));
}
}
for &(i, j, w) in edges {
if w != 0.0 {
entries.push((i, j, -w));
if i != j {
entries.push((j, i, -w));
}
}
}
let lambda_max = Self::estimate_lambda_max(&entries, n, 20);
let scale = 2.0 / lambda_max;
let scaled_entries: Vec<(usize, usize, f64)> = entries
.iter()
.map(|&(i, j, v)| {
if i == j {
(i, j, scale * v - 1.0)
} else {
(i, j, scale * v)
}
})
.collect();
Self {
entries: scaled_entries,
n,
lambda_max,
}
}
fn estimate_lambda_max(entries: &[(usize, usize, f64)], n: usize, iters: usize) -> f64 {
let mut x = vec![1.0 / (n as f64).sqrt(); n];
let mut lambda = 1.0;
for _ in 0..iters {
let mut y = vec![0.0; n];
for &(i, j, v) in entries {
y[i] += v * x[j];
}
let mut dot = 0.0;
let mut norm_sq = 0.0;
for i in 0..n {
dot += x[i] * y[i];
norm_sq += y[i] * y[i];
}
lambda = dot;
let norm = norm_sq.sqrt().max(1e-15);
for i in 0..n {
x[i] = y[i] / norm;
}
}
lambda.abs().max(1.0)
}
pub fn apply(&self, x: &[f64]) -> Vec<f64> {
let mut y = vec![0.0; self.n];
for &(i, j, v) in &self.entries {
if j < x.len() {
y[i] += v * x[j];
}
}
y
}
pub fn lambda_max(&self) -> f64 {
self.lambda_max
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LaplacianNorm {
Unnormalized,
Symmetric,
RandomWalk,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scaled_laplacian() {
let adj = vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
let laplacian = ScaledLaplacian::from_adjacency(&adj, 3);
assert_eq!(laplacian.n, 3);
assert!(laplacian.lambda_max > 0.0);
let x = vec![1.0, 0.0, -1.0];
let y = laplacian.apply(&x);
assert_eq!(y.len(), 3);
}
#[test]
fn test_sparse_laplacian() {
let edges = vec![(0, 1, 1.0), (1, 2, 1.0)];
let laplacian = ScaledLaplacian::from_sparse_adjacency(&edges, 3);
assert_eq!(laplacian.n, 3);
assert!(laplacian.lambda_max > 0.0);
}
}