use crate::algorithms::clustering::kmeans;
use crate::algorithms::euclidean_sq;
#[must_use]
pub fn spectral(data: &[Vec<f64>], k: usize, gamma: f64, seed: u64) -> Vec<usize> {
let n = data.len();
if n == 0 || k == 0 {
return Vec::new();
}
let normalized = normalized_affinity(data, gamma);
let (values, vectors) = jacobi_eigen(&normalized, n);
let embedding = spectral_embedding(&vectors, &values, n, k.min(n));
kmeans(&embedding, k, 300, seed).labels
}
fn normalized_affinity(data: &[Vec<f64>], gamma: f64) -> Vec<f64> {
let n = data.len();
let mut w = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let value = match (data.get(i), data.get(j)) {
(Some(pi), Some(pj)) => (-gamma * euclidean_sq(pi, pj)).exp(),
_ => 0.0,
};
put(&mut w, n, i, j, value);
}
}
let inv_sqrt_deg: Vec<f64> = (0..n)
.map(|i| {
let deg: f64 = (0..n).map(|j| at(&w, n, i, j)).sum();
if deg > 0.0 { 1.0 / deg.sqrt() } else { 0.0 }
})
.collect();
let mut normalized = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let di = inv_sqrt_deg.get(i).copied().unwrap_or(0.0);
let dj = inv_sqrt_deg.get(j).copied().unwrap_or(0.0);
put(&mut normalized, n, i, j, di * at(&w, n, i, j) * dj);
}
}
normalized
}
fn spectral_embedding(vectors: &[f64], values: &[f64], n: usize, k: usize) -> Vec<Vec<f64>> {
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
let va = values.get(b).copied().unwrap_or(f64::NEG_INFINITY);
let vb = values.get(a).copied().unwrap_or(f64::NEG_INFINITY);
va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
});
let top: Vec<usize> = order.into_iter().take(k).collect();
(0..n)
.map(|row| {
let mut coords: Vec<f64> = top
.iter()
.map(|&col| vectors.get(row * n + col).copied().unwrap_or(0.0))
.collect();
let norm = coords.iter().map(|v| v * v).sum::<f64>().sqrt();
if norm > 0.0 {
for c in &mut coords {
*c /= norm;
}
}
coords
})
.collect()
}
const JACOBI_SWEEPS: usize = 100;
const JACOBI_TOL: f64 = 1e-12;
fn jacobi_eigen(matrix: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
let mut a = matrix.to_vec();
let mut v = identity(n);
for _ in 0..JACOBI_SWEEPS {
let mut off = 0.0_f64;
for p in 0..n {
for q in (p + 1)..n {
off += at(&a, n, p, q).abs();
}
}
if off < JACOBI_TOL {
break;
}
for p in 0..n {
for q in (p + 1)..n {
rotate(&mut a, &mut v, n, p, q);
}
}
}
let values = (0..n).map(|i| at(&a, n, i, i)).collect();
(values, v)
}
#[allow(clippy::many_single_char_names)]
fn rotate(a: &mut [f64], v: &mut [f64], n: usize, p: usize, q: usize) {
let apq = at(a, n, p, q);
if apq.abs() < JACOBI_TOL {
return;
}
let app = at(a, n, p, p);
let aqq = at(a, n, q, q);
let theta = (aqq - app) / (2.0 * apq);
let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
for i in 0..n {
let aip = at(a, n, i, p);
let aiq = at(a, n, i, q);
put(a, n, i, p, c * aip - s * aiq);
put(a, n, i, q, s * aip + c * aiq);
}
for i in 0..n {
let api = at(a, n, p, i);
let aqi = at(a, n, q, i);
put(a, n, p, i, c * api - s * aqi);
put(a, n, q, i, s * api + c * aqi);
}
for i in 0..n {
let vip = at(v, n, i, p);
let viq = at(v, n, i, q);
put(v, n, i, p, c * vip - s * viq);
put(v, n, i, q, s * vip + c * viq);
}
}
fn identity(n: usize) -> Vec<f64> {
let mut v = vec![0.0_f64; n * n];
for i in 0..n {
put(&mut v, n, i, i, 1.0);
}
v
}
fn at(matrix: &[f64], n: usize, i: usize, j: usize) -> f64 {
matrix.get(i * n + j).copied().unwrap_or(0.0)
}
fn put(matrix: &mut [f64], n: usize, i: usize, j: usize, value: f64) {
if let Some(slot) = matrix.get_mut(i * n + j) {
*slot = value;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separates_two_far_pairs() {
let data = vec![vec![0.0], vec![0.1], vec![9.0], vec![9.1]];
let labels = spectral(&data, 2, 1.0, 42);
assert_eq!(labels.first(), labels.get(1), "near pair split");
assert_ne!(labels.first(), labels.get(2), "far pair merged");
}
#[test]
fn deterministic_for_fixed_seed() {
let data = vec![vec![0.0], vec![0.1], vec![9.0], vec![9.1]];
assert_eq!(spectral(&data, 2, 1.0, 5), spectral(&data, 2, 1.0, 5));
}
#[test]
fn jacobi_recovers_diagonal_eigenvalues() {
let m = vec![3.0, 0.0, 0.0, 1.0];
let (values, _) = jacobi_eigen(&m, 2);
assert!(values.contains(&3.0) || (values.iter().any(|v| (v - 3.0).abs() < 1e-9)));
}
}