use ndarray::Array2;
pub fn laplacian(adjacency: &Array2<f32>) -> Array2<f32> {
let n = adjacency.nrows();
debug_assert_eq!(n, adjacency.ncols(), "adjacency must be square");
let mut lap = Array2::<f32>::zeros((n, n));
for i in 0..n {
let mut degree = 0.0_f32;
for j in 0..n {
if i != j {
let w = adjacency[[i, j]];
lap[[i, j]] = -w;
degree += w;
}
}
lap[[i, i]] = degree;
}
lap
}
pub fn normalized_laplacian(adjacency: &Array2<f32>) -> Array2<f32> {
let n = adjacency.nrows();
debug_assert_eq!(n, adjacency.ncols());
let mut d_inv_sqrt = vec![0.0_f32; n];
for i in 0..n {
let mut degree = 0.0_f32;
for j in 0..n {
degree += adjacency[[i, j]];
}
d_inv_sqrt[i] = if degree > 0.0 {
1.0 / degree.sqrt()
} else {
0.0
};
}
let mut lap = Array2::<f32>::eye(n);
for i in 0..n {
for j in 0..n {
if i != j && adjacency[[i, j]] > 0.0 {
lap[[i, j]] = -d_inv_sqrt[i] * adjacency[[i, j]] * d_inv_sqrt[j];
}
}
}
lap
}
pub fn fiedler_vector(lap: &Array2<f32>, max_iter: usize, tol: f32) -> Option<(Vec<f32>, f32)> {
let n = lap.nrows();
if n < 2 {
return None;
}
let shift = 1e-4_f32;
let mut shifted = lap.clone();
for i in 0..n {
shifted[[i, i]] += shift;
}
let chol = super::linalg::cholesky(&shifted)?;
let inv_n = 1.0 / (n as f32).sqrt();
let trivial: Vec<f32> = vec![inv_n; n];
let mut x: Vec<f32> = (0..n).map(|i| ((i * 7 + 3) % 13) as f32 - 6.0).collect();
deflate(&mut x, &trivial);
normalize(&mut x);
for _ in 0..max_iter {
let y = cholesky_solve(&chol, &x);
let mut y_deflated = y;
deflate(&mut y_deflated, &trivial);
let norm = vec_norm(&y_deflated);
if norm < 1e-12 {
return None; }
for v in &mut y_deflated {
*v /= norm;
}
let dot: f32 = x.iter().zip(&y_deflated).map(|(a, b)| a * b).sum();
let change = (1.0 - dot.abs()).abs();
x = y_deflated;
if change < tol {
break;
}
}
let eigenvalue = rayleigh_quotient(lap, &x);
if eigenvalue < 1e-8 {
return None; }
Some((x, eigenvalue))
}
pub fn smallest_eigenvectors(
lap: &Array2<f32>,
k: usize,
max_iter: usize,
tol: f32,
) -> Vec<(Vec<f32>, f32)> {
let n = lap.nrows();
if n < 2 || k == 0 {
return Vec::new();
}
let shift = 1e-4_f32;
let mut shifted = lap.clone();
for i in 0..n {
shifted[[i, i]] += shift;
}
let chol = match super::linalg::cholesky(&shifted) {
Some(c) => c,
None => return Vec::new(),
};
let inv_n = 1.0 / (n as f32).sqrt();
let trivial: Vec<f32> = vec![inv_n; n];
let mut found: Vec<(Vec<f32>, f32)> = Vec::with_capacity(k);
for _ in 0..k {
let mut x: Vec<f32> = (0..n)
.map(|i| ((i * 7 + 3 + found.len() * 11) % 13) as f32 - 6.0)
.collect();
deflate(&mut x, &trivial);
for (ev, _) in &found {
deflate(&mut x, ev);
}
normalize(&mut x);
let mut converged = false;
for _ in 0..max_iter {
let mut y = cholesky_solve(&chol, &x);
deflate(&mut y, &trivial);
for (ev, _) in &found {
deflate(&mut y, ev);
}
let norm = vec_norm(&y);
if norm < 1e-12 {
break;
}
for v in &mut y {
*v /= norm;
}
let dot: f32 = x.iter().zip(&y).map(|(a, b)| a * b).sum();
let change = (1.0 - dot.abs()).abs();
x = y;
if change < tol {
converged = true;
break;
}
}
if !converged && found.len() >= 1 {
break;
}
let eigenvalue = rayleigh_quotient(lap, &x);
if eigenvalue < 1e-8 {
break; }
found.push((x, eigenvalue));
}
found
}
pub fn spectral_cluster(adjacency: &Array2<f32>, k: usize) -> Option<Vec<usize>> {
let n = adjacency.nrows();
if n < k || k == 0 {
return None;
}
if k == 1 {
return Some(vec![0; n]);
}
if k == 2 {
let lap = laplacian(adjacency);
let (fv, _) = fiedler_vector(&lap, 500, 1e-7)?;
return Some(fv.iter().map(|&v| if v >= 0.0 { 0 } else { 1 }).collect());
}
let mut assignments = vec![0usize; n];
let mut next_label = 1usize;
let mut queue: Vec<(Vec<usize>, usize, usize)> = vec![((0..n).collect(), 0, k - 1)];
while let Some((nodes, _label, remaining)) = queue.pop() {
if remaining == 0 || nodes.len() <= 1 {
continue;
}
let m = nodes.len();
let mut sub_adj = Array2::<f32>::zeros((m, m));
for (si, &ni) in nodes.iter().enumerate() {
for (sj, &nj) in nodes.iter().enumerate() {
sub_adj[[si, sj]] = adjacency[[ni, nj]];
}
}
let sub_lap = laplacian(&sub_adj);
let bisection = fiedler_vector(&sub_lap, 500, 1e-7);
if let Some((fv, _)) = bisection {
let mut group_a = Vec::new();
let mut group_b = Vec::new();
for (si, &v) in fv.iter().enumerate() {
if v >= 0.0 {
group_a.push(nodes[si]);
} else {
group_b.push(nodes[si]);
}
}
let new_label = next_label;
next_label += 1;
for &ni in &group_b {
assignments[ni] = new_label;
}
if remaining > 1 {
let splits_a = (remaining - 1) * group_a.len() / nodes.len();
let splits_b = (remaining - 1) - splits_a;
if splits_a > 0 {
queue.push((group_a, _label, splits_a));
}
if splits_b > 0 {
queue.push((group_b, new_label, splits_b));
}
}
}
}
Some(assignments)
}
pub fn algebraic_connectivity(adjacency: &Array2<f32>) -> f32 {
let lap = laplacian(adjacency);
match fiedler_vector(&lap, 300, 1e-6) {
Some((_, eigenvalue)) => eigenvalue,
None => 0.0,
}
}
pub fn effective_resistance(adjacency: &Array2<f32>, i: usize, j: usize) -> f32 {
let n = adjacency.nrows();
debug_assert!(i < n && j < n);
if i == j {
return 0.0;
}
let lap = laplacian(adjacency);
let reg = 1.0 / n as f32;
let mut lap_reg = lap;
for r in 0..n {
lap_reg[[r, r]] += reg;
}
let chol = match super::linalg::cholesky(&lap_reg) {
Some(c) => c,
None => return f32::INFINITY,
};
let mut rhs = vec![0.0_f32; n];
rhs[i] = 1.0;
rhs[j] = -1.0;
let x = cholesky_solve(&chol, &rhs);
(x[i] - x[j]).abs()
}
pub fn sparsify(adjacency: &Array2<f32>, epsilon: f32) -> Array2<f32> {
let n = adjacency.nrows();
debug_assert_eq!(n, adjacency.ncols());
if n <= 3 {
return adjacency.clone(); }
let lap = laplacian(adjacency);
let reg = 1.0 / n as f32;
let mut lap_reg = lap;
for i in 0..n {
lap_reg[[i, i]] += reg;
}
let chol = match super::linalg::cholesky(&lap_reg) {
Some(c) => c,
None => return adjacency.clone(),
};
let mut edges: Vec<(usize, usize, f32, f32)> = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let w = adjacency[[i, j]];
if w <= 0.0 {
continue;
}
let mut rhs = vec![0.0_f32; n];
rhs[i] = 1.0;
rhs[j] = -1.0;
let x = cholesky_solve(&chol, &rhs);
let r_ij = (x[i] - x[j]).abs();
let leverage = w * r_ij;
edges.push((i, j, w, leverage));
}
}
if edges.is_empty() {
return adjacency.clone();
}
let mut leverages: Vec<f32> = edges.iter().map(|e| e.3).collect();
leverages.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let target_keep = ((1.0 - epsilon) * edges.len() as f32).ceil() as usize;
let threshold = if target_keep < edges.len() {
leverages[edges.len() - target_keep]
} else {
0.0 };
let mut sparse = Array2::<f32>::zeros((n, n));
for &(i, j, w, leverage) in &edges {
if leverage >= threshold {
sparse[[i, j]] = w;
sparse[[j, i]] = w;
}
}
sparse
}
pub fn spectral_centrality(adjacency: &Array2<f32>) -> Vec<(usize, f32)> {
let n = adjacency.nrows();
let lap = laplacian(adjacency);
let fv = match fiedler_vector(&lap, 300, 1e-6) {
Some((v, _)) => v,
None => return (0..n).map(|i| (i, 0.0)).collect(),
};
let mut scores: Vec<(usize, f32)> = fv.iter().enumerate().map(|(i, &v)| (i, v.abs())).collect();
scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scores
}
pub fn spectral_bisection(adjacency: &Array2<f32>) -> Option<(Vec<usize>, Vec<usize>)> {
let lap = laplacian(adjacency);
let (fv, _) = fiedler_vector(&lap, 300, 1e-6)?;
let mut group_a = Vec::new();
let mut group_b = Vec::new();
for (i, &v) in fv.iter().enumerate() {
if v >= 0.0 {
group_a.push(i);
} else {
group_b.push(i);
}
}
Some((group_a, group_b))
}
fn cholesky_solve(chol: &Array2<f32>, b: &[f32]) -> Vec<f32> {
let n = chol.nrows();
let mut y = vec![0.0_f32; n];
for i in 0..n {
let mut sum = 0.0_f32;
for j in 0..i {
sum += chol[[i, j]] * y[j];
}
y[i] = (b[i] - sum) / chol[[i, i]];
}
let mut x = vec![0.0_f32; n];
for i in (0..n).rev() {
let mut sum = 0.0_f32;
for j in (i + 1)..n {
sum += chol[[j, i]] * x[j]; }
x[i] = (y[i] - sum) / chol[[i, i]];
}
x
}
fn deflate(x: &mut [f32], basis: &[f32]) {
let dot: f32 = x.iter().zip(basis).map(|(a, b)| a * b).sum();
for (xi, &bi) in x.iter_mut().zip(basis) {
*xi -= dot * bi;
}
}
fn vec_norm(x: &[f32]) -> f32 {
x.iter().map(|v| v * v).sum::<f32>().sqrt()
}
fn normalize(x: &mut [f32]) {
let n = vec_norm(x);
if n > 1e-12 {
for v in x.iter_mut() {
*v /= n;
}
}
}
fn rayleigh_quotient(a: &Array2<f32>, x: &[f32]) -> f32 {
let n = a.nrows();
let mut num = 0.0_f32;
for i in 0..n {
let mut ax_i = 0.0_f32;
for j in 0..n {
ax_i += a[[i, j]] * x[j];
}
num += x[i] * ax_i;
}
let denom: f32 = x.iter().map(|v| v * v).sum();
if denom > 0.0 { num / denom } else { 0.0 }
}
#[allow(dead_code)]
fn kmeans(points: &[Vec<f32>], k: usize, max_iter: usize) -> Vec<usize> {
let n = points.len();
let dim = points[0].len();
if n <= k {
return (0..n).collect();
}
let mut centroids: Vec<Vec<f32>> = Vec::with_capacity(k);
centroids.push(points[0].clone());
for _ in 1..k {
let mut best_idx = 0;
let mut best_dist = f32::NEG_INFINITY;
for (i, p) in points.iter().enumerate() {
let min_dist = centroids
.iter()
.map(|c| sq_dist(p, c))
.fold(f32::INFINITY, f32::min);
if min_dist > best_dist {
best_dist = min_dist;
best_idx = i;
}
}
centroids.push(points[best_idx].clone());
}
let mut assignments = vec![0usize; n];
for _ in 0..max_iter {
let mut changed = false;
for (i, p) in points.iter().enumerate() {
let mut best_c = 0;
let mut best_d = f32::INFINITY;
for (c, centroid) in centroids.iter().enumerate() {
let d = sq_dist(p, centroid);
if d < best_d {
best_d = d;
best_c = c;
}
}
if assignments[i] != best_c {
assignments[i] = best_c;
changed = true;
}
}
if !changed {
break;
}
let mut sums = vec![vec![0.0_f32; dim]; k];
let mut counts = vec![0usize; k];
for (i, p) in points.iter().enumerate() {
let c = assignments[i];
counts[c] += 1;
for (j, &v) in p.iter().enumerate() {
sums[c][j] += v;
}
}
for c in 0..k {
if counts[c] > 0 {
for j in 0..dim {
centroids[c][j] = sums[c][j] / counts[c] as f32;
}
}
}
}
assignments
}
#[allow(dead_code)]
fn sq_dist(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn test_laplacian_triangle() {
let adj = array![[0.0_f32, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
let lap = laplacian(&adj);
for i in 0..3 {
assert!((lap[[i, i]] - 2.0).abs() < 1e-6);
for j in 0..3 {
if i != j {
assert!((lap[[i, j]] - (-1.0)).abs() < 1e-6);
}
}
}
}
#[test]
fn test_laplacian_row_sum_zero() {
let adj = array![[0.0_f32, 0.5, 0.3], [0.5, 0.0, 0.8], [0.3, 0.8, 0.0]];
let lap = laplacian(&adj);
for i in 0..3 {
let row_sum: f32 = (0..3).map(|j| lap[[i, j]]).sum();
assert!(
row_sum.abs() < 1e-6,
"Row {} sums to {} (should be 0)",
i,
row_sum
);
}
}
#[test]
fn test_normalized_laplacian_eigenvalues_bounded() {
let adj = array![[0.0_f32, 1.0, 0.5], [1.0, 0.0, 1.0], [0.5, 1.0, 0.0]];
let nlap = normalized_laplacian(&adj);
for i in 0..3 {
assert!(
(nlap[[i, i]] - 1.0).abs() < 1e-6,
"Normalized Laplacian diagonal[{}] = {}",
i,
nlap[[i, i]]
);
}
for i in 0..3 {
for j in 0..3 {
assert!(
(nlap[[i, j]] - nlap[[j, i]]).abs() < 1e-6,
"Not symmetric at [{},{}]",
i,
j
);
}
}
}
#[test]
fn test_fiedler_path_graph() {
let adj = array![
[0.0_f32, 1.0, 0.0, 0.0],
[1.0, 0.0, 1.0, 0.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 0.0]
];
let lap = laplacian(&adj);
let (fv, lambda) = fiedler_vector(&lap, 500, 1e-7).expect("path graph is connected");
assert!(
(lambda - 0.586).abs() < 0.05,
"Expected λ₂ ≈ 0.586, got {}",
lambda
);
let monotone_inc = fv[0] < fv[1] && fv[1] < fv[2] && fv[2] < fv[3];
let monotone_dec = fv[0] > fv[1] && fv[1] > fv[2] && fv[2] > fv[3];
assert!(
monotone_inc || monotone_dec,
"Fiedler vector of path graph should be monotone, got {:?}",
fv
);
}
#[test]
fn test_fiedler_complete_graph() {
let adj = array![
[0.0_f32, 1.0, 1.0, 1.0],
[1.0, 0.0, 1.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
[1.0, 1.0, 1.0, 0.0]
];
let lap = laplacian(&adj);
let (_, lambda) = fiedler_vector(&lap, 500, 1e-7).expect("K4 is connected");
assert!(
(lambda - 4.0).abs() < 0.1,
"Expected λ₂ = 4.0, got {}",
lambda
);
}
#[test]
fn test_spectral_cluster_two_cliques() {
let mut adj = Array2::<f32>::zeros((6, 6));
for &(i, j) in &[(0, 1), (0, 2), (1, 2)] {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
for &(i, j) in &[(3, 4), (3, 5), (4, 5)] {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
adj[[2, 3]] = 0.1;
adj[[3, 2]] = 0.1;
let assignments = spectral_cluster(&adj, 2).expect("should cluster");
assert_eq!(assignments[0], assignments[1], "clique1 nodes 0,1 differ");
assert_eq!(assignments[1], assignments[2], "clique1 nodes 1,2 differ");
assert_eq!(assignments[3], assignments[4], "clique2 nodes 3,4 differ");
assert_eq!(assignments[4], assignments[5], "clique2 nodes 4,5 differ");
let (a, _b) = spectral_bisection(&adj).expect("connected graph");
let a_has_0 = a.contains(&0);
let a_has_3 = a.contains(&3);
assert_ne!(a_has_0, a_has_3, "Bisection should separate the cliques");
}
#[test]
fn test_spectral_cluster_single_node() {
let adj = array![[0.0_f32]];
let assignments = spectral_cluster(&adj, 1).expect("single node");
assert_eq!(assignments, vec![0]);
}
#[test]
fn test_algebraic_connectivity_path_vs_complete() {
let path = array![[0.0_f32, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
let complete = array![[0.0_f32, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
let ac_path = algebraic_connectivity(&path);
let ac_complete = algebraic_connectivity(&complete);
assert!(
ac_complete > ac_path,
"K3 ({}) should be more connected than P3 ({})",
ac_complete,
ac_path
);
}
#[test]
fn test_effective_resistance_self() {
let adj = array![[0.0_f32, 1.0], [1.0, 0.0]];
assert_eq!(effective_resistance(&adj, 0, 0), 0.0);
}
#[test]
fn test_effective_resistance_single_edge() {
let adj = array![[0.0_f32, 1.0], [1.0, 0.0]];
let r = effective_resistance(&adj, 0, 1);
assert!(r > 0.5 && r < 1.5, "Expected R ≈ 1.0, got {}", r);
}
#[test]
fn test_effective_resistance_parallel_paths() {
let adj = array![[0.0_f32, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
let r_direct = effective_resistance(&adj, 0, 1);
let single_edge = array![[0.0_f32, 1.0], [1.0, 0.0]];
let r_single = effective_resistance(&single_edge, 0, 1);
assert!(
r_direct < r_single,
"Parallel paths ({}) should reduce resistance vs single edge ({})",
r_direct,
r_single
);
}
#[test]
fn test_sparsify_preserves_connectivity() {
let mut adj = Array2::<f32>::zeros((5, 5));
for i in 0..5 {
for j in (i + 1)..5 {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
}
let sparse = sparsify(&adj, 0.5);
for i in 0..5 {
let degree: f32 = (0..5).map(|j| sparse[[i, j]]).sum();
assert!(
degree > 0.0,
"Node {} became isolated after sparsification",
i
);
}
}
#[test]
fn test_sparsify_reduces_edges() {
let n = 10;
let mut adj = Array2::<f32>::zeros((n, n));
for i in 0..5 {
for j in (i + 1)..5 {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
}
for i in 5..10 {
for j in (i + 1)..10 {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
}
for i in 0..5 {
for j in 5..10 {
adj[[i, j]] = 0.05;
adj[[j, i]] = 0.05;
}
}
let sparse = sparsify(&adj, 0.3);
let original_edges: usize = (0..n)
.flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
.filter(|&(i, j)| adj[[i, j]] > 0.0)
.count();
let sparse_edges: usize = (0..n)
.flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
.filter(|&(i, j)| sparse[[i, j]] > 0.0)
.count();
assert!(
sparse_edges < original_edges,
"Sparsification should reduce edges: {} -> {}",
original_edges,
sparse_edges
);
}
#[test]
fn test_sparsify_small_graph_unchanged() {
let adj = array![[0.0_f32, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
let sparse = sparsify(&adj, 0.5);
assert_eq!(sparse, adj);
}
#[test]
fn test_spectral_bisection_barbell() {
let mut adj = Array2::<f32>::zeros((6, 6));
for &(i, j) in &[(0, 1), (0, 2), (1, 2)] {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
for &(i, j) in &[(3, 4), (3, 5), (4, 5)] {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
adj[[2, 3]] = 0.1;
adj[[3, 2]] = 0.1;
let (a, b) = spectral_bisection(&adj).expect("connected graph");
assert_eq!(a.len() + b.len(), 6);
let mut a_set: Vec<usize> = a.clone();
let mut b_set: Vec<usize> = b.clone();
a_set.sort();
b_set.sort();
let correct_a = (a_set == vec![0, 1, 2] && b_set == vec![3, 4, 5])
|| (a_set == vec![3, 4, 5] && b_set == vec![0, 1, 2]);
assert!(
correct_a,
"Expected clean bisection, got A={:?} B={:?}",
a, b
);
}
#[test]
fn test_spectral_centrality_returns_all_nodes() {
let adj = array![
[0.0_f32, 1.0, 0.0, 0.0],
[1.0, 0.0, 1.0, 0.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 0.0]
];
let centrality = spectral_centrality(&adj);
assert_eq!(centrality.len(), 4);
for &(_, score) in ¢rality {
assert!(score >= 0.0, "Centrality score should be non-negative");
}
for w in centrality.windows(2) {
assert!(w[0].1 >= w[1].1, "Centrality should be sorted descending");
}
let node_0_score = centrality.iter().find(|&&(i, _)| i == 0).unwrap().1;
let node_1_score = centrality.iter().find(|&&(i, _)| i == 1).unwrap().1;
assert!(
node_0_score > node_1_score,
"Endpoints should have higher |Fiedler| than interior: 0={}, 1={}",
node_0_score,
node_1_score
);
}
#[test]
fn test_kmeans_obvious_clusters() {
let points = vec![
vec![0.0, 0.0],
vec![0.1, 0.0],
vec![0.0, 0.1],
vec![10.0, 10.0],
vec![10.1, 10.0],
vec![10.0, 10.1],
];
let assignments = kmeans(&points, 2, 50);
assert_eq!(assignments[0], assignments[1]);
assert_eq!(assignments[0], assignments[2]);
assert_eq!(assignments[3], assignments[4]);
assert_eq!(assignments[3], assignments[5]);
assert_ne!(assignments[0], assignments[3]);
}
}