1use crate::error::{GraphError, GraphResult};
14
15#[derive(Debug, Clone)]
19pub struct SpectralConfig {
20 pub n_clusters: usize,
22 pub n_eigenvectors: usize,
24 pub gamma: f64,
26 pub n_iter_kmeans: usize,
28 pub n_iter_power: usize,
30}
31
32impl Default for SpectralConfig {
33 fn default() -> Self {
34 Self {
35 n_clusters: 2,
36 n_eigenvectors: 2,
37 gamma: 1.0,
38 n_iter_kmeans: 50,
39 n_iter_power: 100,
40 }
41 }
42}
43
44pub struct SpectralClustering {
48 labels: Vec<usize>,
49 eigenvalues: Vec<f64>,
50 n_clusters: usize,
51}
52
53impl SpectralClustering {
54 pub fn fit_affinity(affinity: &[f64], n: usize, config: &SpectralConfig) -> GraphResult<Self> {
61 validate_config(n, config)?;
62 if affinity.len() != n * n {
63 return Err(GraphError::InvalidPlan(format!(
64 "affinity length {} != n*n {}",
65 affinity.len(),
66 n * n
67 )));
68 }
69
70 let mut degree = vec![0.0_f64; n];
72 for i in 0..n {
73 let s: f64 = affinity[i * n..(i + 1) * n].iter().sum();
74 degree[i] = s;
75 }
76
77 let d_inv_sqrt: Vec<f64> = degree
79 .iter()
80 .map(|&d| if d > 1e-14 { 1.0 / d.sqrt() } else { 0.0 })
81 .collect();
82
83 let mut m = vec![0.0_f64; n * n];
85 for i in 0..n {
86 for j in 0..n {
87 m[i * n + j] = d_inv_sqrt[i] * affinity[i * n + j] * d_inv_sqrt[j];
88 }
89 }
90
91 let k = config.n_eigenvectors;
93 let (eigvecs, eigenvalues) = power_iteration_deflation(&m, n, k, config.n_iter_power);
94
95 let mut u = eigvecs; let mut u_row = vec![0.0_f64; n * k];
99 for i in 0..n {
100 for c in 0..k {
101 u_row[i * k + c] = u[c * n + i];
102 }
103 }
104 for i in 0..n {
106 let row = &mut u_row[i * k..(i + 1) * k];
107 let norm = row.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-12);
108 for v in row.iter_mut() {
109 *v /= norm;
110 }
111 }
112
113 let seed = affinity
115 .iter()
116 .take(16)
117 .fold(0u64, |acc, &v| acc.wrapping_add(v.to_bits()));
118 let labels = kmeans(&u_row, n, k, config.n_clusters, config.n_iter_kmeans, seed);
119
120 let eigenvalues_out: Vec<f64> = eigenvalues;
122 u = Vec::new();
124 let _ = u;
125
126 Ok(Self {
127 labels,
128 eigenvalues: eigenvalues_out,
129 n_clusters: config.n_clusters,
130 })
131 }
132
133 pub fn fit_data(
139 data: &[f64],
140 n: usize,
141 dim: usize,
142 config: &SpectralConfig,
143 ) -> GraphResult<Self> {
144 if dim == 0 {
145 return Err(GraphError::InvalidPlan("dim must be > 0".to_owned()));
146 }
147 if data.len() != n * dim {
148 return Err(GraphError::InvalidPlan(format!(
149 "data length {} != n*dim {}",
150 data.len(),
151 n * dim
152 )));
153 }
154
155 let mut affinity = vec![0.0_f64; n * n];
157 for i in 0..n {
158 for j in 0..n {
159 let sq_dist: f64 = (0..dim)
160 .map(|d| {
161 let diff = data[i * dim + d] - data[j * dim + d];
162 diff * diff
163 })
164 .sum();
165 affinity[i * n + j] = (-config.gamma * sq_dist).exp();
166 }
167 }
168
169 Self::fit_affinity(&affinity, n, config)
170 }
171
172 #[must_use]
176 pub fn labels(&self) -> &[usize] {
177 &self.labels
178 }
179
180 #[must_use]
183 pub fn eigenvalues(&self) -> &[f64] {
184 &self.eigenvalues
185 }
186
187 #[must_use]
189 pub fn n_clusters(&self) -> usize {
190 self.n_clusters
191 }
192}
193
194fn validate_config(n: usize, config: &SpectralConfig) -> GraphResult<()> {
198 if n == 0 {
199 return Err(GraphError::InvalidPlan("n must be > 0".to_owned()));
200 }
201 if config.n_clusters == 0 || config.n_clusters > n {
202 return Err(GraphError::InvalidPlan(format!(
203 "n_clusters {} out of range [1, {}]",
204 config.n_clusters, n
205 )));
206 }
207 if config.n_eigenvectors == 0 {
208 return Err(GraphError::InvalidPlan(
209 "n_eigenvectors must be > 0".to_owned(),
210 ));
211 }
212 Ok(())
213}
214
215fn matvec(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
217 let mut y = vec![0.0_f64; n];
218 for i in 0..n {
219 let mut s = 0.0_f64;
220 for j in 0..n {
221 s += a[i * n + j] * x[j];
222 }
223 y[i] = s;
224 }
225 y
226}
227
228#[inline]
230fn dot(a: &[f64], b: &[f64]) -> f64 {
231 a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
232}
233
234fn l2_normalise(v: &mut [f64]) -> f64 {
236 let norm = dot(v, v).sqrt().max(1e-14);
237 for x in v.iter_mut() {
238 *x /= norm;
239 }
240 norm
241}
242
243fn power_iteration_deflation(m: &[f64], n: usize, k: usize, n_iter: usize) -> (Vec<f64>, Vec<f64>) {
248 let k_actual = k.min(n);
249 let mut eigvecs = Vec::with_capacity(k_actual * n);
250 let mut eigenvalues = Vec::with_capacity(k_actual);
251
252 for c in 0..k_actual {
253 let mut v: Vec<f64> = (0..n)
255 .map(|i| if i == c % n { 1.0 } else { 0.01 })
256 .collect();
257 l2_normalise(&mut v);
258
259 let mut eigenvalue = 0.0_f64;
260 for _ in 0..n_iter {
261 let mut mv = matvec(m, &v, n);
263
264 for prev in 0..c {
266 let prev_vec = &eigvecs[prev * n..(prev + 1) * n];
267 let coeff = dot(&mv, prev_vec);
268 for (x, &pv) in mv.iter_mut().zip(prev_vec.iter()) {
269 *x -= coeff * pv;
270 }
271 }
272
273 eigenvalue = l2_normalise(&mut mv);
274 v = mv;
275 }
276
277 eigvecs.extend_from_slice(&v);
278 eigenvalues.push(eigenvalue);
279 }
280
281 (eigvecs, eigenvalues)
282}
283
284fn kmeans(
287 u: &[f64],
288 n: usize,
289 k_embed: usize,
290 k_clusters: usize,
291 n_iter: usize,
292 seed: u64,
293) -> Vec<usize> {
294 if k_clusters == 0 || n == 0 {
295 return vec![0; n];
296 }
297
298 let mut centroids = vec![0.0_f64; k_clusters * k_embed];
300 let step = (n / k_clusters).max(1);
301 let start = (seed as usize) % n.max(1);
303 for c in 0..k_clusters {
304 let row_idx = (start + c * step) % n;
305 for d in 0..k_embed {
306 centroids[c * k_embed + d] = u[row_idx * k_embed + d];
307 }
308 }
309
310 let mut labels = vec![0_usize; n];
311
312 for _iter in 0..n_iter {
313 let mut changed = false;
315 for i in 0..n {
316 let row = &u[i * k_embed..(i + 1) * k_embed];
317 let mut best = 0_usize;
318 let mut best_dist = f64::INFINITY;
319 for c in 0..k_clusters {
320 let centroid = ¢roids[c * k_embed..(c + 1) * k_embed];
321 let dist: f64 = row
322 .iter()
323 .zip(centroid.iter())
324 .map(|(&a, &b)| (a - b) * (a - b))
325 .sum();
326 if dist < best_dist {
327 best_dist = dist;
328 best = c;
329 }
330 }
331 if labels[i] != best {
332 labels[i] = best;
333 changed = true;
334 }
335 }
336
337 if !changed {
338 break;
339 }
340
341 let mut new_centroids = vec![0.0_f64; k_clusters * k_embed];
343 let mut counts = vec![0_usize; k_clusters];
344 for i in 0..n {
345 let c = labels[i];
346 counts[c] += 1;
347 let row = &u[i * k_embed..(i + 1) * k_embed];
348 for d in 0..k_embed {
349 new_centroids[c * k_embed + d] += row[d];
350 }
351 }
352 for c in 0..k_clusters {
353 if counts[c] > 0 {
354 let cnt = counts[c] as f64;
355 for d in 0..k_embed {
356 new_centroids[c * k_embed + d] /= cnt;
357 }
358 } else {
359 let row_idx = c % n;
361 for d in 0..k_embed {
362 new_centroids[c * k_embed + d] = u[row_idx * k_embed + d];
363 }
364 }
365 }
366 centroids = new_centroids;
367 }
368
369 labels
370}
371
372#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn two_block_affinity(n: usize) -> Vec<f64> {
379 let half = n / 2;
381 let mut a = vec![0.0_f64; n * n];
382 for i in 0..n {
383 for j in 0..n {
384 let same_block = (i < half && j < half) || (i >= half && j >= half);
385 a[i * n + j] = if same_block { 1.0 } else { 0.0 };
386 }
387 }
388 a
389 }
390
391 #[test]
393 fn labels_len() {
394 let n = 6;
395 let a = two_block_affinity(n);
396 let cfg = SpectralConfig {
397 n_clusters: 2,
398 n_eigenvectors: 2,
399 ..Default::default()
400 };
401 let sc =
402 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
403 assert_eq!(sc.labels().len(), n);
404 }
405
406 #[test]
408 fn labels_in_range() {
409 let n = 8;
410 let a = two_block_affinity(n);
411 let cfg = SpectralConfig {
412 n_clusters: 2,
413 n_eigenvectors: 2,
414 ..Default::default()
415 };
416 let sc =
417 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
418 for &l in sc.labels() {
419 assert!(l < 2, "label {l} out of range");
420 }
421 }
422
423 #[test]
425 fn two_clusters_separated() {
426 let n = 8;
427 let a = two_block_affinity(n);
428 let cfg = SpectralConfig {
429 n_clusters: 2,
430 n_eigenvectors: 2,
431 n_iter_power: 200,
432 n_iter_kmeans: 100,
433 ..Default::default()
434 };
435 let sc =
436 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
437 let labels = sc.labels();
438 let half = n / 2;
439 let label_0 = labels[0];
441 for &l in &labels[..half] {
442 assert_eq!(l, label_0, "first-half labels should match");
443 }
444 let label_1 = labels[half];
446 for &l in &labels[half..] {
447 assert_eq!(l, label_1, "second-half labels should match");
448 }
449 assert_ne!(label_0, label_1);
451 }
452
453 #[test]
455 fn single_cluster() {
456 let n = 5;
457 let a = vec![1.0_f64; n * n]; let cfg = SpectralConfig {
459 n_clusters: 1,
460 n_eigenvectors: 1,
461 ..Default::default()
462 };
463 let sc =
464 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
465 for &l in sc.labels() {
466 assert_eq!(l, 0);
467 }
468 }
469
470 #[test]
472 fn affinity_symmetric() {
473 let n = 4;
474 let a = two_block_affinity(n);
475 let mut at = vec![0.0_f64; n * n];
477 for i in 0..n {
478 for j in 0..n {
479 at[j * n + i] = a[i * n + j];
480 }
481 }
482 let cfg = SpectralConfig::default();
483 let sc1 =
484 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
485 let sc2 =
486 SpectralClustering::fit_affinity(&at, n, &cfg).expect("fit_affinity should succeed");
487 assert_eq!(sc1.labels(), sc2.labels());
489 }
490
491 #[test]
493 fn n_clusters_1_works() {
494 let n = 4;
495 let a = two_block_affinity(n);
496 let cfg = SpectralConfig {
497 n_clusters: 1,
498 n_eigenvectors: 1,
499 ..Default::default()
500 };
501 let sc =
502 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
503 assert_eq!(sc.n_clusters(), 1);
504 assert!(sc.labels().iter().all(|&l| l == 0));
505 }
506
507 #[test]
510 fn affinity_diagonal_ignored() {
511 let n = 6;
512 let mut a = two_block_affinity(n);
513 for i in 0..n {
515 a[i * n + i] = 100.0;
516 }
517 let cfg = SpectralConfig {
518 n_clusters: 2,
519 n_eigenvectors: 2,
520 ..Default::default()
521 };
522 let sc =
523 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
524 assert_eq!(sc.labels().len(), n);
525 for &l in sc.labels() {
526 assert!(l < 2);
527 }
528 }
529
530 #[test]
532 fn eigenvalues_finite() {
533 let n = 6;
534 let a = two_block_affinity(n);
535 let cfg = SpectralConfig {
536 n_clusters: 2,
537 n_eigenvectors: 2,
538 ..Default::default()
539 };
540 let sc =
541 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
542 for &ev in sc.eigenvalues() {
543 assert!(ev.is_finite(), "eigenvalue {ev} is not finite");
544 }
545 }
546
547 #[test]
549 fn n_gt_n_eigenvectors_ok() {
550 let n = 10;
551 let a = two_block_affinity(n);
552 let cfg = SpectralConfig {
553 n_clusters: 2,
554 n_eigenvectors: 2,
555 ..Default::default()
556 };
557 let sc =
558 SpectralClustering::fit_affinity(&a, n, &cfg).expect("fit_affinity should succeed");
559 assert_eq!(sc.labels().len(), n);
560 }
561
562 #[test]
564 fn fit_data_rbf() {
565 let n = 8;
567 let dim = 2;
568 let mut data = vec![0.0_f64; n * dim];
569 for i in 0..4 {
570 data[i * dim] = i as f64 * 0.1;
571 data[i * dim + 1] = 0.0;
572 }
573 for i in 4..8 {
574 data[i * dim] = 10.0 + (i - 4) as f64 * 0.1;
575 data[i * dim + 1] = 0.0;
576 }
577 let cfg = SpectralConfig {
578 n_clusters: 2,
579 n_eigenvectors: 2,
580 gamma: 1.0,
581 n_iter_kmeans: 100,
582 n_iter_power: 200,
583 };
584 let sc =
585 SpectralClustering::fit_data(&data, n, dim, &cfg).expect("fit_data should succeed");
586 assert_eq!(sc.labels().len(), n);
587 for &l in sc.labels() {
588 assert!(l < 2);
589 }
590 }
591}