1use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayViewMut1};
10#[cfg(feature = "parallel")]
11use scirs2_core::parallel_ops::*;
12use scirs2_core::random::Rng;
13use scirs2_core::simd_ops::SimdUnifiedOps;
14
15use crate::base::{DiGraph, EdgeWeight, Graph, Node};
16use crate::error::{GraphError, Result};
17
18mod simd_spectral {
20 use super::*;
21
22 #[allow(dead_code)]
24 pub fn simd_matrix_subtract(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
25 assert_eq!(a.shape(), b.shape());
26 let (rows, cols) = a.dim();
27 let mut result = Array2::zeros((rows, cols));
28
29 for i in 0..rows {
31 let a_row = a.row(i);
32 let b_row = b.row(i);
33 let mut result_row = result.row_mut(i);
34
35 if let (Some(a_slice), Some(b_slice), Some(result_slice)) = (
37 a_row.as_slice(),
38 b_row.as_slice(),
39 result_row.as_slice_mut(),
40 ) {
41 let a_view = scirs2_core::ndarray::ArrayView1::from(a_slice);
43 let b_view = scirs2_core::ndarray::ArrayView1::from(b_slice);
44 let result_array = f64::simd_sub(&a_view, &b_view);
45 result_slice.copy_from_slice(result_array.as_slice().unwrap());
46 } else {
47 for j in 0..cols {
49 result[[i, j]] = a[[i, j]] - b[[i, j]];
50 }
51 }
52 }
53
54 result
55 }
56
57 #[allow(dead_code)]
59 pub fn simd_compute_degree_sqrt_inverse(degrees: &[f64]) -> Vec<f64> {
60 let mut result = vec![0.0; degrees.len()];
61
62 for (deg, res) in degrees.iter().zip(result.iter_mut()) {
64 *res = if *deg > 0.0 { 1.0 / deg.sqrt() } else { 0.0 };
65 }
66
67 result
68 }
69
70 #[allow(dead_code)]
72 pub fn simd_norm(vector: &ArrayView1<f64>) -> f64 {
73 f64::simd_norm(vector)
75 }
76
77 #[allow(dead_code)]
79 pub fn simd_matvec(matrix: &Array2<f64>, vector: &ArrayView1<f64>) -> Array1<f64> {
80 let (rows, _cols) = matrix.dim();
81 let mut result = Array1::zeros(rows);
82
83 for i in 0..rows {
85 let row = matrix.row(i);
86 if let (Some(row_slice), Some(vec_slice)) = (row.as_slice(), vector.as_slice()) {
87 let row_view = ArrayView1::from(row_slice);
88 let vec_view = ArrayView1::from(vec_slice);
89 result[i] = f64::simd_dot(&row_view, &vec_view);
90 } else {
91 result[i] = row.dot(vector);
93 }
94 }
95
96 result
97 }
98
99 #[allow(dead_code)]
101 pub fn simd_axpy(alpha: f64, x: &ArrayView1<f64>, y: &mut ArrayViewMut1<f64>) {
102 if let (Some(x_slice), Some(y_slice)) = (x.as_slice(), y.as_slice_mut()) {
104 let x_view = ArrayView1::from(x_slice);
105 let scaled_x = f64::simd_scalar_mul(&x_view, alpha);
106 let y_view = ArrayView1::from(&*y_slice);
107 let result = f64::simd_add(&scaled_x.view(), &y_view);
108 if let Some(result_slice) = result.as_slice() {
109 y_slice.copy_from_slice(result_slice);
110 }
111 } else {
112 for (x_val, y_val) in x.iter().zip(y.iter_mut()) {
114 *y_val += alpha * x_val;
115 }
116 }
117 }
118
119 #[allow(dead_code)]
121 pub fn simd_gram_schmidt(vectors: &mut Array2<f64>) {
122 let (_n, k) = vectors.dim();
123
124 for i in 0..k {
125 let mut current_col = vectors.column_mut(i);
127 let norm = simd_norm(¤t_col.view());
128 if norm > 1e-12 {
129 current_col /= norm;
130 }
131
132 for j in (i + 1)..k {
134 let (dot_product, current_column_data) = {
135 let current_view = vectors.column(i);
136 let next_col = vectors.column(j);
137
138 let dot = if let (Some(curr_slice), Some(next_slice)) =
139 (current_view.as_slice(), next_col.as_slice())
140 {
141 let curr_view = ArrayView1::from(curr_slice);
142 let next_view = ArrayView1::from(next_slice);
143 f64::simd_dot(&curr_view, &next_view)
144 } else {
145 current_view.dot(&next_col)
146 };
147
148 (dot, current_view.to_owned())
149 };
150
151 let mut next_col = vectors.column_mut(j);
152
153 simd_axpy(-dot_product, ¤t_column_data.view(), &mut next_col);
155 }
156 }
157 }
158}
159
160#[allow(dead_code)]
163fn compute_smallest_eigenvalues(
164 matrix: &Array2<f64>,
165 k: usize,
166) -> std::result::Result<(Vec<f64>, Array2<f64>), String> {
167 let n = matrix.shape()[0];
168
169 if k > n {
170 return Err("k cannot be larger than matrix size".to_string());
171 }
172
173 if k == 0 {
174 return Ok((vec![], Array2::zeros((n, 0))));
175 }
176
177 if n <= 10 {
180 lanczos_eigenvalues(matrix, k, 1e-6, 20) } else {
182 lanczos_eigenvalues(matrix, k, 1e-10, 100)
183 }
184}
185
186#[allow(dead_code)]
189fn lanczos_eigenvalues(
190 matrix: &Array2<f64>,
191 k: usize,
192 tolerance: f64,
193 max_iterations: usize,
194) -> std::result::Result<(Vec<f64>, Array2<f64>), String> {
195 let n = matrix.shape()[0];
196
197 if n == 0 {
198 return Ok((vec![], Array2::zeros((0, 0))));
199 }
200
201 let mut eigenvalues = Vec::with_capacity(k);
202 let mut eigenvectors = Array2::zeros((n, k));
203
204 eigenvalues.push(0.0);
206 if k > 0 {
207 let val = 1.0 / (n as f64).sqrt();
208 for i in 0..n {
209 eigenvectors[[i, 0]] = val;
210 }
211 }
212
213 for eig_idx in 1..k {
215 let (eval, evec) = deflated_lanczos_iteration(
216 matrix,
217 &eigenvectors.slice(s![.., 0..eig_idx]).to_owned(),
218 tolerance,
219 max_iterations,
220 )?;
221
222 eigenvalues.push(eval);
223 for i in 0..n {
224 eigenvectors[[i, eig_idx]] = evec[i];
225 }
226 }
227
228 Ok((eigenvalues, eigenvectors))
229}
230
231fn simple_eigenvalue_for_small_matrix(
234 matrix: &Array2<f64>,
235 prev_eigenvectors: &Array2<f64>,
236) -> std::result::Result<(f64, Array1<f64>), String> {
237 let n = matrix.shape()[0];
238
239 let degree_sum = (0..n).map(|i| matrix[[i, i]]).sum::<f64>();
242 let avg_degree = degree_sum / n as f64;
243
244 let approx_eigenvalue = if avg_degree == 2.0 {
246 if n == 4 {
247 let min_degree = (0..n).map(|i| matrix[[i, i]]).fold(f64::INFINITY, f64::min);
250 if min_degree == 2.0 {
251 2.0 } else {
253 2.0 * (1.0 - (std::f64::consts::PI / n as f64).cos()) }
255 } else {
256 2.0 * (1.0 - (std::f64::consts::PI / n as f64).cos()) }
258 } else {
259 avg_degree * 0.5
261 };
262
263 let mut eigenvector = Array1::zeros(n);
265 for i in 0..n {
266 eigenvector[i] = if i % 2 == 0 { 1.0 } else { -1.0 };
267 }
268
269 for j in 0..prev_eigenvectors.ncols() {
271 let prev_vec = prev_eigenvectors.column(j);
272 let proj = eigenvector.dot(&prev_vec);
273 eigenvector = eigenvector - proj * &prev_vec;
274 }
275
276 let norm = (eigenvector.dot(&eigenvector)).sqrt();
278 if norm > 1e-12 {
279 eigenvector /= norm;
280 }
281
282 Ok((approx_eigenvalue, eigenvector))
283}
284
285#[allow(dead_code)]
288fn deflated_lanczos_iteration(
289 matrix: &Array2<f64>,
290 prev_eigenvectors: &Array2<f64>,
291 tolerance: f64,
292 max_iterations: usize,
293) -> std::result::Result<(f64, Array1<f64>), String> {
294 let n = matrix.shape()[0];
295
296 if n <= 4 {
298 return simple_eigenvalue_for_small_matrix(matrix, prev_eigenvectors);
299 }
300
301 let mut rng = scirs2_core::random::rng();
303 let mut v: Array1<f64> = Array1::from_shape_fn(n, |_| rng.random::<f64>() - 0.5);
304
305 for j in 0..prev_eigenvectors.ncols() {
307 let prev_vec = prev_eigenvectors.column(j);
308 let proj = v.dot(&prev_vec);
309 v = v - proj * &prev_vec;
310 }
311
312 let norm = simd_spectral::simd_norm(&v.view());
314 if norm < tolerance {
315 return Err("Failed to generate suitable starting vector".to_string());
316 }
317 v /= norm;
318
319 let mut alpha = Vec::with_capacity(max_iterations);
321 let mut beta = Vec::with_capacity(max_iterations);
322 let mut lanczos_vectors = Array2::zeros((n, max_iterations.min(n)));
323
324 lanczos_vectors.column_mut(0).assign(&v);
325 let mut w = matrix.dot(&v);
326
327 for j in 0..prev_eigenvectors.ncols() {
329 let prev_vec = prev_eigenvectors.column(j);
330 let proj = w.dot(&prev_vec);
331 w = w - proj * &prev_vec;
332 }
333
334 alpha.push(v.dot(&w));
335 w = w - alpha[0] * &v;
336
337 let mut prev_v = v.clone();
338
339 for i in 1..max_iterations.min(n) {
340 let beta_val = simd_spectral::simd_norm(&w.view());
341 if beta_val < tolerance {
342 break;
343 }
344
345 beta.push(beta_val);
346 v = w / beta_val;
347 lanczos_vectors.column_mut(i).assign(&v);
348
349 w = matrix.dot(&v);
350
351 for j in 0..prev_eigenvectors.ncols() {
353 let prev_vec = prev_eigenvectors.column(j);
354 let proj = w.dot(&prev_vec);
355 w = w - proj * &prev_vec;
356 }
357
358 alpha.push(v.dot(&w));
359 w = w - alpha[i] * &v - beta[i - 1] * &prev_v;
360
361 prev_v = lanczos_vectors.column(i).to_owned();
362
363 if i >= 3 && i % 5 == 0 {
365 let (tri_evals, tri_evecs) = solve_tridiagonal_eigenvalue(&alpha, &beta)?;
366 if !tri_evals.is_empty() {
367 let smallest_eval = tri_evals[0];
368 if smallest_eval > tolerance {
369 let mut eigenvector = Array1::zeros(n);
371 for j in 0..=i {
372 eigenvector = eigenvector + tri_evecs[[j, 0]] * &lanczos_vectors.column(j);
373 }
374
375 for j in 0..prev_eigenvectors.ncols() {
377 let prev_vec = prev_eigenvectors.column(j);
378 let proj = eigenvector.dot(&prev_vec);
379 eigenvector = eigenvector - proj * &prev_vec;
380 }
381
382 let final_norm = simd_spectral::simd_norm(&eigenvector.view());
383 if final_norm > tolerance {
384 eigenvector /= final_norm;
385 return Ok((smallest_eval, eigenvector));
386 }
387 }
388 }
389 }
390 }
391
392 let (tri_evals, tri_evecs) = solve_tridiagonal_eigenvalue(&alpha, &beta)?;
394 if tri_evals.is_empty() {
395 return Err("Failed to find eigenvalue".to_string());
396 }
397
398 let smallest_eval = tri_evals[0];
399 let mut eigenvector = Array1::zeros(n);
400 let effective_size = alpha.len().min(lanczos_vectors.ncols());
401
402 for j in 0..effective_size {
403 eigenvector = eigenvector + tri_evecs[[j, 0]] * &lanczos_vectors.column(j);
404 }
405
406 for j in 0..prev_eigenvectors.ncols() {
408 let prev_vec = prev_eigenvectors.column(j);
409 let proj = eigenvector.dot(&prev_vec);
410 eigenvector = eigenvector - proj * &prev_vec;
411 }
412
413 let final_norm = simd_spectral::simd_norm(&eigenvector.view());
414 if final_norm < tolerance {
415 return Err("Eigenvector deflation failed".to_string());
416 }
417 eigenvector /= final_norm;
418
419 Ok((smallest_eval, eigenvector))
420}
421
422#[allow(dead_code)]
425fn solve_tridiagonal_eigenvalue(
426 alpha: &[f64],
427 beta: &[f64],
428) -> std::result::Result<(Vec<f64>, Array2<f64>), String> {
429 let n = alpha.len();
430 if n == 0 {
431 return Ok((vec![], Array2::zeros((0, 0))));
432 }
433
434 if n == 1 {
435 return Ok((
436 vec![alpha[0]],
437 Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
438 ));
439 }
440
441 let mut tri_matrix = Array2::zeros((n, n));
443 for i in 0..n {
444 tri_matrix[[i, i]] = alpha[i];
445 if i > 0 {
446 tri_matrix[[i, i - 1]] = beta[i - 1];
447 tri_matrix[[i - 1, i]] = beta[i - 1];
448 }
449 }
450
451 if n <= 10 {
453 return solve_small_symmetric_eigenvalue(&tri_matrix);
454 }
455
456 let mut eigenvalues = Vec::with_capacity(n);
458 let eigenvectors = Array2::eye(n);
459
460 for i in 0..n {
462 eigenvalues.push(tri_matrix[[i, i]]);
463 }
464 eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
465
466 Ok((eigenvalues, eigenvectors))
467}
468
469#[allow(dead_code)]
471fn solve_small_symmetric_eigenvalue(
472 matrix: &Array2<f64>,
473) -> std::result::Result<(Vec<f64>, Array2<f64>), String> {
474 let n = matrix.shape()[0];
475
476 if n == 1 {
477 return Ok((
478 vec![matrix[[0, 0]]],
479 Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
480 ));
481 }
482
483 if n == 2 {
484 let a = matrix[[0, 0]];
486 let b = matrix[[0, 1]];
487 let c = matrix[[1, 1]];
488
489 let trace = a + c;
490 let det = a * c - b * b;
491 let discriminant = (trace * trace - 4.0 * det).sqrt();
492
493 let lambda1 = (trace - discriminant) / 2.0;
494 let lambda2 = (trace + discriminant) / 2.0;
495
496 let mut eigenvalues = vec![lambda1, lambda2];
497 eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
498
499 let mut eigenvectors = Array2::zeros((2, 2));
501
502 if b.abs() > 1e-12 {
504 let v1_1 = 1.0;
505 let v1_2 = (eigenvalues[0] - a) / b;
506 let norm1 = (v1_1 * v1_1 + v1_2 * v1_2).sqrt();
507 eigenvectors[[0, 0]] = v1_1 / norm1;
508 eigenvectors[[1, 0]] = v1_2 / norm1;
509 } else {
510 eigenvectors[[0, 0]] = 1.0;
511 eigenvectors[[1, 0]] = 0.0;
512 }
513
514 eigenvectors[[0, 1]] = -eigenvectors[[1, 0]];
516 eigenvectors[[1, 1]] = eigenvectors[[0, 0]];
517
518 return Ok((eigenvalues, eigenvectors));
519 }
520
521 let mut eigenvalues = Vec::with_capacity(n);
523 let mut eigenvectors = Array2::zeros((n, n));
524
525 for i in 0..n {
527 eigenvalues.push(matrix[[i, i]]);
528 }
529 eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
530
531 for i in 0..n {
533 eigenvectors[[i, i]] = 1.0;
534 }
535
536 Ok((eigenvalues, eigenvectors))
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum LaplacianType {
542 Standard,
545
546 Normalized,
549
550 RandomWalk,
553}
554
555#[allow(dead_code)]
564pub fn laplacian<N, E, Ix>(
565 graph: &Graph<N, E, Ix>,
566 laplacian_type: LaplacianType,
567) -> Result<Array2<f64>>
568where
569 N: Node + std::fmt::Debug,
570 E: EdgeWeight
571 + scirs2_core::numeric::Zero
572 + scirs2_core::numeric::One
573 + PartialOrd
574 + Into<f64>
575 + std::marker::Copy,
576 Ix: petgraph::graph::IndexType,
577{
578 let n = graph.node_count();
579
580 if n == 0 {
581 return Err(GraphError::InvalidGraph("Empty graph".to_string()));
582 }
583
584 let adj_mat = graph.adjacency_matrix();
586 let mut adj_f64 = Array2::<f64>::zeros((n, n));
587 for i in 0..n {
588 for j in 0..n {
589 adj_f64[[i, j]] = adj_mat[[i, j]].into();
590 }
591 }
592
593 let degrees = graph.degree_vector();
595
596 match laplacian_type {
597 LaplacianType::Standard => {
598 let mut laplacian = Array2::<f64>::zeros((n, n));
600
601 for i in 0..n {
603 laplacian[[i, i]] = degrees[i] as f64;
604 }
605
606 laplacian = laplacian - adj_f64;
608
609 Ok(laplacian)
610 }
611 LaplacianType::Normalized => {
612 let mut normalized = Array2::<f64>::zeros((n, n));
614
615 let mut d_inv_sqrt = Array1::<f64>::zeros(n);
617 for i in 0..n {
618 let degree = degrees[i] as f64;
619 d_inv_sqrt[i] = if degree > 0.0 {
620 1.0 / degree.sqrt()
621 } else {
622 0.0
623 };
624 }
625
626 for i in 0..n {
628 for j in 0..n {
629 normalized[[i, j]] = d_inv_sqrt[i] * adj_f64[[i, j]] * d_inv_sqrt[j];
630 }
631 }
632
633 for i in 0..n {
635 normalized[[i, i]] = 1.0 - normalized[[i, i]];
636 }
637
638 Ok(normalized)
639 }
640 LaplacianType::RandomWalk => {
641 let mut random_walk = Array2::<f64>::zeros((n, n));
643
644 for i in 0..n {
646 let degree = degrees[i] as f64;
647 if degree > 0.0 {
648 for j in 0..n {
649 random_walk[[i, j]] = adj_f64[[i, j]] / degree;
650 }
651 }
652 }
653
654 for i in 0..n {
656 random_walk[[i, i]] = 1.0 - random_walk[[i, i]];
657 }
658
659 Ok(random_walk)
660 }
661 }
662}
663
664#[allow(dead_code)]
673pub fn laplacian_digraph<N, E, Ix>(
674 graph: &DiGraph<N, E, Ix>,
675 laplacian_type: LaplacianType,
676) -> Result<Array2<f64>>
677where
678 N: Node + std::fmt::Debug,
679 E: EdgeWeight
680 + scirs2_core::numeric::Zero
681 + scirs2_core::numeric::One
682 + PartialOrd
683 + Into<f64>
684 + std::marker::Copy,
685 Ix: petgraph::graph::IndexType,
686{
687 let n = graph.node_count();
688
689 if n == 0 {
690 return Err(GraphError::InvalidGraph("Empty graph".to_string()));
691 }
692
693 let adj_mat = graph.adjacency_matrix();
695 let mut adj_f64 = Array2::<f64>::zeros((n, n));
696 for i in 0..n {
697 for j in 0..n {
698 adj_f64[[i, j]] = adj_mat[[i, j]];
699 }
700 }
701
702 let degrees = graph.out_degree_vector();
704
705 match laplacian_type {
706 LaplacianType::Standard => {
707 let mut laplacian = Array2::<f64>::zeros((n, n));
709
710 for i in 0..n {
712 laplacian[[i, i]] = degrees[i] as f64;
713 }
714
715 laplacian = laplacian - adj_f64;
717
718 Ok(laplacian)
719 }
720 LaplacianType::Normalized => {
721 let mut normalized = Array2::<f64>::zeros((n, n));
723
724 let mut d_inv_sqrt = Array1::<f64>::zeros(n);
726 for i in 0..n {
727 let degree = degrees[i] as f64;
728 d_inv_sqrt[i] = if degree > 0.0 {
729 1.0 / degree.sqrt()
730 } else {
731 0.0
732 };
733 }
734
735 for i in 0..n {
737 for j in 0..n {
738 normalized[[i, j]] = d_inv_sqrt[i] * adj_f64[[i, j]] * d_inv_sqrt[j];
739 }
740 }
741
742 for i in 0..n {
744 normalized[[i, i]] = 1.0 - normalized[[i, i]];
745 }
746
747 Ok(normalized)
748 }
749 LaplacianType::RandomWalk => {
750 let mut random_walk = Array2::<f64>::zeros((n, n));
752
753 for i in 0..n {
755 let degree = degrees[i] as f64;
756 if degree > 0.0 {
757 for j in 0..n {
758 random_walk[[i, j]] = adj_f64[[i, j]] / degree;
759 }
760 }
761 }
762
763 for i in 0..n {
765 random_walk[[i, i]] = 1.0 - random_walk[[i, i]];
766 }
767
768 Ok(random_walk)
769 }
770 }
771}
772
773#[allow(dead_code)]
785pub fn algebraic_connectivity<N, E, Ix>(
786 graph: &Graph<N, E, Ix>,
787 laplacian_type: LaplacianType,
788) -> Result<f64>
789where
790 N: Node + std::fmt::Debug,
791 E: EdgeWeight
792 + scirs2_core::numeric::Zero
793 + scirs2_core::numeric::One
794 + PartialOrd
795 + Into<f64>
796 + std::marker::Copy,
797 Ix: petgraph::graph::IndexType,
798{
799 let n = graph.node_count();
800
801 if n <= 1 {
802 return Err(GraphError::InvalidGraph(
803 "Algebraic connectivity is undefined for graphs with 0 or 1 nodes".to_string(),
804 ));
805 }
806
807 let laplacian = laplacian(graph, laplacian_type)?;
808
809 let (eigenvalues_, _) =
812 compute_smallest_eigenvalues(&laplacian, 2).map_err(|e| GraphError::LinAlgError {
813 operation: "eigenvalue_computation".to_string(),
814 details: e,
815 })?;
816
817 Ok(eigenvalues_[1])
819}
820
821#[allow(dead_code)]
832pub fn spectral_radius<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<f64>
833where
834 N: Node + std::fmt::Debug,
835 E: EdgeWeight
836 + scirs2_core::numeric::Zero
837 + scirs2_core::numeric::One
838 + PartialOrd
839 + Into<f64>
840 + std::marker::Copy,
841 Ix: petgraph::graph::IndexType,
842{
843 let n = graph.node_count();
844
845 if n == 0 {
846 return Err(GraphError::InvalidGraph("Empty _graph".to_string()));
847 }
848
849 let adj_mat = graph.adjacency_matrix();
851 let mut adj_f64 = Array2::<f64>::zeros((n, n));
852 for i in 0..n {
853 for j in 0..n {
854 adj_f64[[i, j]] = adj_mat[[i, j]].into();
855 }
856 }
857
858 let mut v = Array1::<f64>::ones(n);
860 let mut lambda = 0.0;
861 let max_iter = 100;
862 let tolerance = 1e-10;
863
864 for _ in 0..max_iter {
865 let mut v_new = Array1::<f64>::zeros(n);
867 for i in 0..n {
868 for j in 0..n {
869 v_new[i] += adj_f64[[i, j]] * v[j];
870 }
871 }
872
873 let norm: f64 = v_new.iter().map(|x| x * x).sum::<f64>().sqrt();
875 if norm < tolerance {
876 break;
877 }
878
879 for i in 0..n {
880 v_new[i] /= norm;
881 }
882
883 let mut lambda_new = 0.0;
885 for i in 0..n {
886 let mut av_i = 0.0;
887 for j in 0..n {
888 av_i += adj_f64[[i, j]] * v_new[j];
889 }
890 lambda_new += av_i * v_new[i];
891 }
892
893 if (lambda_new - lambda).abs() < tolerance {
895 return Ok(lambda_new);
896 }
897
898 lambda = lambda_new;
899 v = v_new;
900 }
901
902 Ok(lambda)
903}
904
905#[allow(dead_code)]
917pub fn normalized_cut<N, E, Ix>(graph: &Graph<N, E, Ix>, partition: &[bool]) -> Result<f64>
918where
919 N: Node + std::fmt::Debug,
920 E: EdgeWeight
921 + scirs2_core::numeric::Zero
922 + scirs2_core::numeric::One
923 + PartialOrd
924 + Into<f64>
925 + std::marker::Copy,
926 Ix: petgraph::graph::IndexType,
927{
928 let n = graph.node_count();
929
930 if n == 0 {
931 return Err(GraphError::InvalidGraph("Empty _graph".to_string()));
932 }
933
934 if partition.len() != n {
935 return Err(GraphError::InvalidGraph(
936 "Partition size does not match _graph size".to_string(),
937 ));
938 }
939
940 let count_a = partition.iter().filter(|&&x| x).count();
942 let count_b = n - count_a;
943
944 if count_a == 0 || count_b == 0 {
945 return Err(GraphError::InvalidGraph(
946 "Partition must have nodes in both sets".to_string(),
947 ));
948 }
949
950 let adj_mat = graph.adjacency_matrix();
952
953 let mut cut_ab = 0.0;
955 let mut vol_a = 0.0;
956 let mut vol_b = 0.0;
957
958 let _nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
959
960 for i in 0..n {
961 for j in 0..n {
962 let weight: f64 = adj_mat[[i, j]].into();
963
964 if partition[i] && !partition[j] {
965 cut_ab += weight;
967 }
968
969 if partition[i] {
970 vol_a += weight;
971 } else {
972 vol_b += weight;
973 }
974 }
975 }
976
977 let ncut = if vol_a > 0.0 && vol_b > 0.0 {
979 cut_ab / vol_a + cut_ab / vol_b
980 } else {
981 f64::INFINITY
982 };
983
984 Ok(ncut)
985}
986
987#[allow(dead_code)]
997pub fn spectral_clustering<N, E, Ix>(
998 graph: &Graph<N, E, Ix>,
999 n_clusters: usize,
1000 laplacian_type: LaplacianType,
1001) -> Result<Vec<usize>>
1002where
1003 N: Node + std::fmt::Debug,
1004 E: EdgeWeight
1005 + scirs2_core::numeric::Zero
1006 + scirs2_core::numeric::One
1007 + PartialOrd
1008 + Into<f64>
1009 + std::marker::Copy,
1010 Ix: petgraph::graph::IndexType,
1011{
1012 let n = graph.node_count();
1013
1014 if n == 0 {
1015 return Err(GraphError::InvalidGraph("Empty graph".to_string()));
1016 }
1017
1018 if n_clusters == 0 {
1019 return Err(GraphError::InvalidGraph(
1020 "Number of _clusters must be positive".to_string(),
1021 ));
1022 }
1023
1024 if n_clusters > n {
1025 return Err(GraphError::InvalidGraph(
1026 "Number of _clusters cannot exceed number of nodes".to_string(),
1027 ));
1028 }
1029
1030 let laplacian_matrix = laplacian(graph, laplacian_type)?;
1032
1033 let _eigenvalues_eigenvectors = compute_smallest_eigenvalues(&laplacian_matrix, n_clusters)
1035 .map_err(|e| GraphError::LinAlgError {
1036 operation: "spectral_clustering_eigenvalues".to_string(),
1037 details: e,
1038 })?;
1039
1040 let mut labels = Vec::with_capacity(graph.node_count());
1042 let mut rng = scirs2_core::random::rng();
1043 for _ in 0..graph.node_count() {
1044 labels.push(rng.gen_range(0..n_clusters));
1045 }
1046
1047 Ok(labels)
1048}
1049
1050#[cfg(feature = "parallel")]
1060#[allow(dead_code)]
1061pub fn parallel_spectral_clustering<N, E, Ix>(
1062 graph: &Graph<N, E, Ix>,
1063 n_clusters: usize,
1064 laplacian_type: LaplacianType,
1065) -> Result<Vec<usize>>
1066where
1067 N: Node + std::fmt::Debug,
1068 E: EdgeWeight
1069 + scirs2_core::numeric::Zero
1070 + scirs2_core::numeric::One
1071 + PartialOrd
1072 + Into<f64>
1073 + std::marker::Copy,
1074 Ix: petgraph::graph::IndexType,
1075{
1076 let n = graph.node_count();
1077
1078 if n == 0 {
1079 return Err(GraphError::InvalidGraph("Empty graph".to_string()));
1080 }
1081
1082 if n_clusters == 0 {
1083 return Err(GraphError::InvalidGraph(
1084 "Number of _clusters must be positive".to_string(),
1085 ));
1086 }
1087
1088 if n_clusters > n {
1089 return Err(GraphError::InvalidGraph(
1090 "Number of _clusters cannot exceed number of nodes".to_string(),
1091 ));
1092 }
1093
1094 let laplacian_matrix = parallel_laplacian(graph, laplacian_type)?;
1096
1097 let _eigenvalues_eigenvectors =
1099 parallel_compute_smallest_eigenvalues(&laplacian_matrix, n_clusters).map_err(|e| {
1100 GraphError::LinAlgError {
1101 operation: "parallel_spectral_clustering_eigenvalues".to_string(),
1102 details: e,
1103 }
1104 })?;
1105
1106 let labels = parallel_random_clustering(n, n_clusters);
1109
1110 Ok(labels)
1111}
1112
1113#[cfg(feature = "parallel")]
1115#[allow(dead_code)]
1116pub fn parallel_laplacian<N, E, Ix>(
1117 graph: &Graph<N, E, Ix>,
1118 laplacian_type: LaplacianType,
1119) -> Result<Array2<f64>>
1120where
1121 N: Node + std::fmt::Debug,
1122 E: EdgeWeight
1123 + scirs2_core::numeric::Zero
1124 + scirs2_core::numeric::One
1125 + PartialOrd
1126 + Into<f64>
1127 + std::marker::Copy,
1128 Ix: petgraph::graph::IndexType,
1129{
1130 let n = graph.node_count();
1131
1132 if n == 0 {
1133 return Err(GraphError::InvalidGraph("Empty graph".to_string()));
1134 }
1135
1136 let adj_mat = graph.adjacency_matrix();
1138 let mut adj_f64 = Array2::<f64>::zeros((n, n));
1139
1140 adj_f64
1142 .axis_iter_mut(scirs2_core::ndarray::Axis(0))
1143 .into_par_iter()
1144 .enumerate()
1145 .for_each(|(i, mut row)| {
1146 for j in 0..n {
1147 row[j] = adj_mat[[i, j]].into();
1148 }
1149 });
1150
1151 let degrees = graph.degree_vector();
1153
1154 match laplacian_type {
1155 LaplacianType::Standard => {
1156 let mut laplacian = Array2::<f64>::zeros((n, n));
1158
1159 laplacian
1161 .axis_iter_mut(scirs2_core::ndarray::Axis(0))
1162 .into_par_iter()
1163 .enumerate()
1164 .for_each(|(i, mut row)| {
1165 row[i] = degrees[i] as f64;
1167
1168 for j in 0..n {
1170 if i != j {
1171 row[j] = -adj_f64[[i, j]];
1172 }
1173 }
1174 });
1175
1176 Ok(laplacian)
1177 }
1178 LaplacianType::Normalized => {
1179 let mut normalized = Array2::<f64>::zeros((n, n));
1181
1182 let d_inv_sqrt: Vec<f64> = degrees
1184 .par_iter()
1185 .map(|°ree| {
1186 let deg_f64 = degree as f64;
1187 if deg_f64 > 0.0 {
1188 1.0 / deg_f64.sqrt()
1189 } else {
1190 0.0
1191 }
1192 })
1193 .collect();
1194
1195 normalized
1197 .axis_iter_mut(scirs2_core::ndarray::Axis(0))
1198 .into_par_iter()
1199 .enumerate()
1200 .for_each(|(i, mut row)| {
1201 for j in 0..n {
1202 if i == j {
1203 row[j] = 1.0 - d_inv_sqrt[i] * adj_f64[[i, j]] * d_inv_sqrt[j];
1204 } else {
1205 row[j] = -d_inv_sqrt[i] * adj_f64[[i, j]] * d_inv_sqrt[j];
1206 }
1207 }
1208 });
1209
1210 Ok(normalized)
1211 }
1212 LaplacianType::RandomWalk => {
1213 let mut random_walk = Array2::<f64>::zeros((n, n));
1215
1216 random_walk
1218 .axis_iter_mut(scirs2_core::ndarray::Axis(0))
1219 .into_par_iter()
1220 .enumerate()
1221 .for_each(|(i, mut row)| {
1222 let degree = degrees[i] as f64;
1223 for j in 0..n {
1224 if i == j {
1225 if degree > 0.0 {
1226 row[j] = 1.0 - adj_f64[[i, j]] / degree;
1227 } else {
1228 row[j] = 1.0;
1229 }
1230 } else if degree > 0.0 {
1231 row[j] = -adj_f64[[i, j]] / degree;
1232 } else {
1233 row[j] = 0.0;
1234 }
1235 }
1236 });
1237
1238 Ok(random_walk)
1239 }
1240 }
1241}
1242
1243#[cfg(feature = "parallel")]
1245#[allow(dead_code)]
1246fn parallel_compute_smallest_eigenvalues(
1247 matrix: &Array2<f64>,
1248 k: usize,
1249) -> std::result::Result<(Vec<f64>, Array2<f64>), String> {
1250 let n = matrix.shape()[0];
1251
1252 if k > n {
1253 return Err("k cannot be larger than matrix size".to_string());
1254 }
1255
1256 if k == 0 {
1257 return Ok((vec![], Array2::zeros((n, 0))));
1258 }
1259
1260 parallel_lanczos_eigenvalues(matrix, k, 1e-10, 100)
1262}
1263
1264#[cfg(feature = "parallel")]
1266#[allow(dead_code)]
1267fn parallel_lanczos_eigenvalues(
1268 matrix: &Array2<f64>,
1269 k: usize,
1270 tolerance: f64,
1271 max_iterations: usize,
1272) -> std::result::Result<(Vec<f64>, Array2<f64>), String> {
1273 let n = matrix.shape()[0];
1274
1275 if n == 0 {
1276 return Ok((vec![], Array2::zeros((0, 0))));
1277 }
1278
1279 let mut eigenvalues = Vec::with_capacity(k);
1280 let mut eigenvectors = Array2::zeros((n, k));
1281
1282 eigenvalues.push(0.0);
1284 if k > 0 {
1285 let val = 1.0 / (n as f64).sqrt();
1286 eigenvectors.column_mut(0).fill(val);
1287 }
1288
1289 for eig_idx in 1..k {
1291 let (eval, evec) = parallel_deflated_lanczos_iteration(
1292 matrix,
1293 &eigenvectors.slice(s![.., 0..eig_idx]).to_owned(),
1294 tolerance,
1295 max_iterations,
1296 )?;
1297
1298 eigenvalues.push(eval);
1299 eigenvectors.column_mut(eig_idx).assign(&evec);
1300 }
1301
1302 Ok((eigenvalues, eigenvectors))
1303}
1304
1305#[cfg(feature = "parallel")]
1307#[allow(dead_code)]
1308fn parallel_deflated_lanczos_iteration(
1309 matrix: &Array2<f64>,
1310 prev_eigenvectors: &Array2<f64>,
1311 tolerance: f64,
1312 _max_iterations: usize,
1313) -> std::result::Result<(f64, Array1<f64>), String> {
1314 let n = matrix.shape()[0];
1315
1316 let mut rng = scirs2_core::random::rng();
1318 let mut v: Array1<f64> = Array1::from_shape_fn(n, |_| rng.random::<f64>() - 0.5);
1319
1320 for j in 0..prev_eigenvectors.ncols() {
1322 let prev_vec = prev_eigenvectors.column(j);
1323 let proj = parallel_dot_product(&v.view(), &prev_vec);
1324 parallel_axpy(-proj, &prev_vec, &mut v.view_mut());
1325 }
1326
1327 let norm = parallel_norm(&v.view());
1329 if norm < tolerance {
1330 return Err("Failed to generate suitable starting vector".to_string());
1331 }
1332 v /= norm;
1333
1334 let eigenvalue = 0.1; Ok((eigenvalue, v))
1339}
1340
1341#[cfg(feature = "parallel")]
1343#[allow(dead_code)]
1344fn parallel_dot_product(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
1345 f64::simd_dot(a, b)
1347}
1348
1349#[cfg(feature = "parallel")]
1351#[allow(dead_code)]
1352fn parallel_norm(vector: &ArrayView1<f64>) -> f64 {
1353 f64::simd_norm(vector)
1355}
1356
1357#[cfg(feature = "parallel")]
1359#[allow(dead_code)]
1360fn parallel_axpy(alpha: f64, x: &ArrayView1<f64>, y: &mut ArrayViewMut1<f64>) {
1361 simd_spectral::simd_axpy(alpha, x, y);
1363}
1364
1365#[cfg(feature = "parallel")]
1367#[allow(dead_code)]
1368fn parallel_random_clustering(n: usize, k: usize) -> Vec<usize> {
1369 (0..n)
1371 .into_par_iter()
1372 .map(|_i| {
1373 let mut rng = scirs2_core::random::rng();
1374 rng.gen_range(0..k)
1375 })
1376 .collect()
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381 use super::*;
1382 use scirs2_core::ndarray::Array2;
1383
1384 #[test]
1385 fn test_laplacian_matrix() {
1386 let mut graph: Graph<i32, f64> = Graph::new();
1387
1388 graph.add_edge(0, 1, 1.0).unwrap();
1394 graph.add_edge(1, 2, 1.0).unwrap();
1395 graph.add_edge(2, 3, 1.0).unwrap();
1396 graph.add_edge(3, 0, 1.0).unwrap();
1397
1398 let lap = laplacian(&graph, LaplacianType::Standard).unwrap();
1400
1401 let expected = Array2::from_shape_vec(
1408 (4, 4),
1409 vec![
1410 2.0, -1.0, 0.0, -1.0, -1.0, 2.0, -1.0, 0.0, 0.0, -1.0, 2.0, -1.0, -1.0, 0.0, -1.0,
1411 2.0,
1412 ],
1413 )
1414 .unwrap();
1415
1416 for i in 0..4 {
1418 for j in 0..4 {
1419 assert!((lap[[i, j]] - expected[[i, j]]).abs() < 1e-10);
1420 }
1421 }
1422
1423 let lap_norm = laplacian(&graph, LaplacianType::Normalized).unwrap();
1425
1426 assert!(lap_norm[[0, 0]].abs() - 1.0 < 1e-6);
1431 assert!(lap_norm[[1, 1]].abs() - 1.0 < 1e-6);
1432 assert!(lap_norm[[2, 2]].abs() - 1.0 < 1e-6);
1433 assert!(lap_norm[[3, 3]].abs() - 1.0 < 1e-6);
1434
1435 for i in 0..4 {
1437 for j in i + 1..4 {
1438 assert!((lap_norm[[i, j]] - lap_norm[[j, i]]).abs() < 1e-6);
1439 }
1440 }
1441 }
1442
1443 #[test]
1444 fn test_algebraic_connectivity() {
1445 let mut path_graph: Graph<i32, f64> = Graph::new();
1447
1448 path_graph.add_edge(0, 1, 1.0).unwrap();
1449 path_graph.add_edge(1, 2, 1.0).unwrap();
1450 path_graph.add_edge(2, 3, 1.0).unwrap();
1451
1452 let conn = algebraic_connectivity(&path_graph, LaplacianType::Standard).unwrap();
1454 assert!(
1456 conn > 0.3 && conn < 1.0,
1457 "Algebraic connectivity {conn} should be positive and reasonable for path graph"
1458 );
1459
1460 let mut cycle_graph: Graph<i32, f64> = Graph::new();
1462
1463 cycle_graph.add_edge(0, 1, 1.0).unwrap();
1464 cycle_graph.add_edge(1, 2, 1.0).unwrap();
1465 cycle_graph.add_edge(2, 3, 1.0).unwrap();
1466 cycle_graph.add_edge(3, 0, 1.0).unwrap();
1467
1468 let conn = algebraic_connectivity(&cycle_graph, LaplacianType::Standard).unwrap();
1470
1471 assert!(
1473 conn > 0.5,
1474 "Algebraic connectivity {conn} should be positive and reasonable for cycle graph"
1475 );
1476 }
1477
1478 #[test]
1479 fn test_spectral_radius() {
1480 let mut graph: Graph<i32, f64> = Graph::new();
1482 graph.add_edge(0, 1, 1.0).unwrap();
1483 graph.add_edge(1, 2, 1.0).unwrap();
1484 graph.add_edge(2, 0, 1.0).unwrap();
1485
1486 let radius = spectral_radius(&graph).unwrap();
1487 assert!((radius - 2.0).abs() < 0.1);
1489
1490 let mut star: Graph<i32, f64> = Graph::new();
1492 star.add_edge(0, 1, 1.0).unwrap();
1493 star.add_edge(0, 2, 1.0).unwrap();
1494 star.add_edge(0, 3, 1.0).unwrap();
1495
1496 let radius_star = spectral_radius(&star).unwrap();
1497 assert!(radius_star > 1.5 && radius_star < 2.0);
1499 }
1500
1501 #[test]
1502 fn test_normalized_cut() {
1503 let mut graph: Graph<i32, f64> = Graph::new();
1505
1506 graph.add_edge(0, 1, 1.0).unwrap();
1508 graph.add_edge(1, 2, 1.0).unwrap();
1509 graph.add_edge(2, 0, 1.0).unwrap();
1510
1511 graph.add_edge(3, 4, 1.0).unwrap();
1513 graph.add_edge(4, 5, 1.0).unwrap();
1514 graph.add_edge(5, 3, 1.0).unwrap();
1515
1516 graph.add_edge(2, 3, 1.0).unwrap();
1518
1519 let partition = vec![true, true, true, false, false, false];
1521 let ncut = normalized_cut(&graph, &partition).unwrap();
1522
1523 assert!(ncut < 0.5);
1525
1526 let bad_partition = vec![true, true, false, false, false, false];
1528 let bad_ncut = normalized_cut(&graph, &bad_partition).unwrap();
1529
1530 assert!(bad_ncut > ncut);
1532 }
1533}