use crate::error::{GraphError, GraphResult};
#[derive(Debug, Clone)]
pub struct SpectralConfig {
pub n_clusters: usize,
pub n_eigenvectors: usize,
pub gamma: f64,
pub n_iter_kmeans: usize,
pub n_iter_power: usize,
}
impl Default for SpectralConfig {
fn default() -> Self {
Self {
n_clusters: 2,
n_eigenvectors: 2,
gamma: 1.0,
n_iter_kmeans: 50,
n_iter_power: 100,
}
}
}
pub struct SpectralClustering {
labels: Vec<usize>,
eigenvalues: Vec<f64>,
n_clusters: usize,
}
impl SpectralClustering {
pub fn fit_affinity(affinity: &[f64], n: usize, config: &SpectralConfig) -> GraphResult<Self> {
validate_config(n, config)?;
if affinity.len() != n * n {
return Err(GraphError::InvalidPlan(format!(
"affinity length {} != n*n {}",
affinity.len(),
n * n
)));
}
let mut degree = vec![0.0_f64; n];
for i in 0..n {
let s: f64 = affinity[i * n..(i + 1) * n].iter().sum();
degree[i] = s;
}
let d_inv_sqrt: Vec<f64> = degree
.iter()
.map(|&d| if d > 1e-14 { 1.0 / d.sqrt() } else { 0.0 })
.collect();
let mut m = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
m[i * n + j] = d_inv_sqrt[i] * affinity[i * n + j] * d_inv_sqrt[j];
}
}
let k = config.n_eigenvectors;
let (eigvecs, eigenvalues) = power_iteration_deflation(&m, n, k, config.n_iter_power);
let mut u = eigvecs; let mut u_row = vec![0.0_f64; n * k];
for i in 0..n {
for c in 0..k {
u_row[i * k + c] = u[c * n + i];
}
}
for i in 0..n {
let row = &mut u_row[i * k..(i + 1) * k];
let norm = row.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-12);
for v in row.iter_mut() {
*v /= norm;
}
}
let seed = affinity
.iter()
.take(16)
.fold(0u64, |acc, &v| acc.wrapping_add(v.to_bits()));
let labels = kmeans(&u_row, n, k, config.n_clusters, config.n_iter_kmeans, seed);
let eigenvalues_out: Vec<f64> = eigenvalues;
u = Vec::new();
let _ = u;
Ok(Self {
labels,
eigenvalues: eigenvalues_out,
n_clusters: config.n_clusters,
})
}
pub fn fit_data(
data: &[f64],
n: usize,
dim: usize,
config: &SpectralConfig,
) -> GraphResult<Self> {
if dim == 0 {
return Err(GraphError::InvalidPlan("dim must be > 0".to_owned()));
}
if data.len() != n * dim {
return Err(GraphError::InvalidPlan(format!(
"data length {} != n*dim {}",
data.len(),
n * dim
)));
}
let mut affinity = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let sq_dist: f64 = (0..dim)
.map(|d| {
let diff = data[i * dim + d] - data[j * dim + d];
diff * diff
})
.sum();
affinity[i * n + j] = (-config.gamma * sq_dist).exp();
}
}
Self::fit_affinity(&affinity, n, config)
}
#[must_use]
pub fn labels(&self) -> &[usize] {
&self.labels
}
#[must_use]
pub fn eigenvalues(&self) -> &[f64] {
&self.eigenvalues
}
#[must_use]
pub fn n_clusters(&self) -> usize {
self.n_clusters
}
}
fn validate_config(n: usize, config: &SpectralConfig) -> GraphResult<()> {
if n == 0 {
return Err(GraphError::InvalidPlan("n must be > 0".to_owned()));
}
if config.n_clusters == 0 || config.n_clusters > n {
return Err(GraphError::InvalidPlan(format!(
"n_clusters {} out of range [1, {}]",
config.n_clusters, n
)));
}
if config.n_eigenvectors == 0 {
return Err(GraphError::InvalidPlan(
"n_eigenvectors must be > 0".to_owned(),
));
}
Ok(())
}
fn matvec(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
let mut y = vec![0.0_f64; n];
for i in 0..n {
let mut s = 0.0_f64;
for j in 0..n {
s += a[i * n + j] * x[j];
}
y[i] = s;
}
y
}
#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
fn l2_normalise(v: &mut [f64]) -> f64 {
let norm = dot(v, v).sqrt().max(1e-14);
for x in v.iter_mut() {
*x /= norm;
}
norm
}
fn power_iteration_deflation(m: &[f64], n: usize, k: usize, n_iter: usize) -> (Vec<f64>, Vec<f64>) {
let k_actual = k.min(n);
let mut eigvecs = Vec::with_capacity(k_actual * n);
let mut eigenvalues = Vec::with_capacity(k_actual);
for c in 0..k_actual {
let mut v: Vec<f64> = (0..n)
.map(|i| if i == c % n { 1.0 } else { 0.01 })
.collect();
l2_normalise(&mut v);
let mut eigenvalue = 0.0_f64;
for _ in 0..n_iter {
let mut mv = matvec(m, &v, n);
for prev in 0..c {
let prev_vec = &eigvecs[prev * n..(prev + 1) * n];
let coeff = dot(&mv, prev_vec);
for (x, &pv) in mv.iter_mut().zip(prev_vec.iter()) {
*x -= coeff * pv;
}
}
eigenvalue = l2_normalise(&mut mv);
v = mv;
}
eigvecs.extend_from_slice(&v);
eigenvalues.push(eigenvalue);
}
(eigvecs, eigenvalues)
}
fn kmeans(
u: &[f64],
n: usize,
k_embed: usize,
k_clusters: usize,
n_iter: usize,
seed: u64,
) -> Vec<usize> {
if k_clusters == 0 || n == 0 {
return vec![0; n];
}
let mut centroids = vec![0.0_f64; k_clusters * k_embed];
let step = (n / k_clusters).max(1);
let start = (seed as usize) % n.max(1);
for c in 0..k_clusters {
let row_idx = (start + c * step) % n;
for d in 0..k_embed {
centroids[c * k_embed + d] = u[row_idx * k_embed + d];
}
}
let mut labels = vec![0_usize; n];
for _iter in 0..n_iter {
let mut changed = false;
for i in 0..n {
let row = &u[i * k_embed..(i + 1) * k_embed];
let mut best = 0_usize;
let mut best_dist = f64::INFINITY;
for c in 0..k_clusters {
let centroid = ¢roids[c * k_embed..(c + 1) * k_embed];
let dist: f64 = row
.iter()
.zip(centroid.iter())
.map(|(&a, &b)| (a - b) * (a - b))
.sum();
if dist < best_dist {
best_dist = dist;
best = c;
}
}
if labels[i] != best {
labels[i] = best;
changed = true;
}
}
if !changed {
break;
}
let mut new_centroids = vec![0.0_f64; k_clusters * k_embed];
let mut counts = vec![0_usize; k_clusters];
for i in 0..n {
let c = labels[i];
counts[c] += 1;
let row = &u[i * k_embed..(i + 1) * k_embed];
for d in 0..k_embed {
new_centroids[c * k_embed + d] += row[d];
}
}
for c in 0..k_clusters {
if counts[c] > 0 {
let cnt = counts[c] as f64;
for d in 0..k_embed {
new_centroids[c * k_embed + d] /= cnt;
}
} else {
let row_idx = c % n;
for d in 0..k_embed {
new_centroids[c * k_embed + d] = u[row_idx * k_embed + d];
}
}
}
centroids = new_centroids;
}
labels
}
#[cfg(test)]
mod tests {
use super::*;
fn two_block_affinity(n: usize) -> Vec<f64> {
let half = n / 2;
let mut a = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let same_block = (i < half && j < half) || (i >= half && j >= half);
a[i * n + j] = if same_block { 1.0 } else { 0.0 };
}
}
a
}
#[test]
fn labels_len() {
let n = 6;
let a = two_block_affinity(n);
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
assert_eq!(sc.labels().len(), n);
}
#[test]
fn labels_in_range() {
let n = 8;
let a = two_block_affinity(n);
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
for &l in sc.labels() {
assert!(l < 2, "label {l} out of range");
}
}
#[test]
fn two_clusters_separated() {
let n = 8;
let a = two_block_affinity(n);
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
n_iter_power: 200,
n_iter_kmeans: 100,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
let labels = sc.labels();
let half = n / 2;
let label_0 = labels[0];
for &l in &labels[..half] {
assert_eq!(l, label_0, "first-half labels should match");
}
let label_1 = labels[half];
for &l in &labels[half..] {
assert_eq!(l, label_1, "second-half labels should match");
}
assert_ne!(label_0, label_1);
}
#[test]
fn single_cluster() {
let n = 5;
let a = vec![1.0_f64; n * n]; let cfg = SpectralConfig {
n_clusters: 1,
n_eigenvectors: 1,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
for &l in sc.labels() {
assert_eq!(l, 0);
}
}
#[test]
fn affinity_symmetric() {
let n = 4;
let a = two_block_affinity(n);
let mut at = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
at[j * n + i] = a[i * n + j];
}
}
let cfg = SpectralConfig::default();
let sc1 =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
let sc2 =
SpectralClustering::fit_affinity(&at, n, &cfg).expect("fit_affinity should succeed");
assert_eq!(sc1.labels(), sc2.labels());
}
#[test]
fn n_clusters_1_works() {
let n = 4;
let a = two_block_affinity(n);
let cfg = SpectralConfig {
n_clusters: 1,
n_eigenvectors: 1,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
assert_eq!(sc.n_clusters(), 1);
assert!(sc.labels().iter().all(|&l| l == 0));
}
#[test]
fn affinity_diagonal_ignored() {
let n = 6;
let mut a = two_block_affinity(n);
for i in 0..n {
a[i * n + i] = 100.0;
}
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
assert_eq!(sc.labels().len(), n);
for &l in sc.labels() {
assert!(l < 2);
}
}
#[test]
fn eigenvalues_finite() {
let n = 6;
let a = two_block_affinity(n);
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
for &ev in sc.eigenvalues() {
assert!(ev.is_finite(), "eigenvalue {ev} is not finite");
}
}
#[test]
fn n_gt_n_eigenvectors_ok() {
let n = 10;
let a = two_block_affinity(n);
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
..Default::default()
};
let sc =
SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
assert_eq!(sc.labels().len(), n);
}
#[test]
fn fit_data_rbf() {
let n = 8;
let dim = 2;
let mut data = vec![0.0_f64; n * dim];
for i in 0..4 {
data[i * dim] = i as f64 * 0.1;
data[i * dim + 1] = 0.0;
}
for i in 4..8 {
data[i * dim] = 10.0 + (i - 4) as f64 * 0.1;
data[i * dim + 1] = 0.0;
}
let cfg = SpectralConfig {
n_clusters: 2,
n_eigenvectors: 2,
gamma: 1.0,
n_iter_kmeans: 100,
n_iter_power: 200,
};
let sc =
SpectralClustering::fit_data(&data, n, dim, &cfg).expect("fit_data should succeed");
assert_eq!(sc.labels().len(), n);
for &l in sc.labels() {
assert!(l < 2);
}
}
}