1use scirs2_core::ndarray::Array2;
29use scirs2_core::random::{Rng, RngExt, SeedableRng, StdRng};
30
31use crate::error::{GraphError, Result};
32
33pub fn graph_laplacian(adj: &Array2<f64>) -> Array2<f64> {
44 let n = adj.nrows();
45 let mut lap = Array2::zeros((n, n));
46 for i in 0..n {
47 let deg: f64 = adj.row(i).sum();
48 lap[[i, i]] = deg;
49 for j in 0..n {
50 if i != j {
51 lap[[i, j]] = -adj[[i, j]];
52 }
53 }
54 }
55 lap
56}
57
58pub fn normalized_laplacian(adj: &Array2<f64>) -> Result<Array2<f64>> {
67 let n = adj.nrows();
68 if n == 0 {
69 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
70 }
71 if adj.ncols() != n {
72 return Err(GraphError::InvalidGraph(
73 "adjacency matrix must be square".into(),
74 ));
75 }
76
77 let d_inv_sqrt: Vec<f64> = (0..n)
79 .map(|i| {
80 let deg = adj.row(i).sum();
81 if deg > 0.0 {
82 1.0 / deg.sqrt()
83 } else {
84 0.0
85 }
86 })
87 .collect();
88
89 let l = graph_laplacian(adj);
90 let mut l_norm = Array2::zeros((n, n));
91 for i in 0..n {
92 for j in 0..n {
93 l_norm[[i, j]] = d_inv_sqrt[i] * l[[i, j]] * d_inv_sqrt[j];
94 }
95 }
96 Ok(l_norm)
97}
98
99fn matvec(m: &Array2<f64>, v: &[f64]) -> Vec<f64> {
105 let n = m.nrows();
106 let mut out = vec![0.0f64; n];
107 for i in 0..n {
108 for j in 0..n {
109 out[i] += m[[i, j]] * v[j];
110 }
111 }
112 out
113}
114
115fn vec_norm(v: &[f64]) -> f64 {
117 v.iter().map(|x| x * x).sum::<f64>().sqrt()
118}
119
120fn normalize_inplace(v: &mut [f64]) -> f64 {
122 let n = vec_norm(v);
123 if n > 1e-300 {
124 v.iter_mut().for_each(|x| *x /= n);
125 }
126 n
127}
128
129fn deflate(v: &mut [f64], basis: &[Vec<f64>]) {
131 for b in basis {
132 let dot: f64 = v.iter().zip(b.iter()).map(|(a, c)| a * c).sum();
133 for (vi, bi) in v.iter_mut().zip(b.iter()) {
134 *vi -= dot * bi;
135 }
136 }
137}
138
139fn power_iteration(m: &Array2<f64>, max_iter: usize, tol: f64, seed: u64) -> (f64, Vec<f64>) {
142 let n = m.nrows();
143 let mut rng = StdRng::seed_from_u64(seed);
144 let mut v: Vec<f64> = (0..n).map(|_| rng.random::<f64>() - 0.5).collect();
145 normalize_inplace(&mut v);
146
147 let mut lambda = 0.0f64;
148 for _ in 0..max_iter {
149 let mv = matvec(m, &v);
150 let new_lambda: f64 = mv.iter().zip(v.iter()).map(|(a, b)| a * b).sum();
151 let mut new_v = mv;
152 normalize_inplace(&mut new_v);
153 if (new_lambda - lambda).abs() < tol {
154 return (new_lambda, new_v);
155 }
156 lambda = new_lambda;
157 v = new_v;
158 }
159 (lambda, v)
160}
161
162fn inverse_iteration(
167 m: &Array2<f64>,
168 sigma: f64,
169 prev_evecs: &[Vec<f64>],
170 max_iter: usize,
171 tol: f64,
172 seed: u64,
173) -> std::result::Result<(f64, Vec<f64>), String> {
174 let n = m.nrows();
175 if n == 0 {
176 return Err("empty matrix".into());
177 }
178
179 let mut a = m.to_owned();
181 for i in 0..n {
182 a[[i, i]] -= sigma;
183 }
184
185 let (lu, piv) = lu_decompose(&a)?;
187
188 let mut rng = StdRng::seed_from_u64(seed);
189 let mut v: Vec<f64> = (0..n).map(|_| rng.random::<f64>() - 0.5).collect();
190 deflate(&mut v, prev_evecs);
191 normalize_inplace(&mut v);
192
193 let mut eigenvalue = sigma;
194
195 for _ in 0..max_iter {
196 let mut w = lu_solve(&lu, &piv, &v)?;
198 deflate(&mut w, prev_evecs);
199 let norm = normalize_inplace(&mut w);
200 if norm < 1e-300 {
201 break;
202 }
203 let mv = matvec(m, &w);
205 let new_eigenvalue: f64 = mv.iter().zip(w.iter()).map(|(a, b)| a * b).sum();
206 let diff: f64 = w
207 .iter()
208 .zip(v.iter())
209 .map(|(a, b)| (a - b).powi(2))
210 .sum::<f64>()
211 .sqrt();
212 v = w;
213 if (new_eigenvalue - eigenvalue).abs() < tol && diff < tol {
214 eigenvalue = new_eigenvalue;
215 break;
216 }
217 eigenvalue = new_eigenvalue;
218 }
219
220 Ok((eigenvalue, v))
221}
222
223fn lu_decompose(a: &Array2<f64>) -> std::result::Result<(Vec<Vec<f64>>, Vec<usize>), String> {
227 let n = a.nrows();
228 let mut lu: Vec<Vec<f64>> = (0..n).map(|i| a.row(i).to_vec()).collect();
229 let mut piv: Vec<usize> = (0..n).collect();
230
231 for k in 0..n {
232 let mut max_val = lu[k][k].abs();
234 let mut max_row = k;
235 for i in (k + 1)..n {
236 if lu[i][k].abs() > max_val {
237 max_val = lu[i][k].abs();
238 max_row = i;
239 }
240 }
241 if max_val < 1e-300 {
242 return Err(format!("singular matrix at column {k}"));
243 }
244 lu.swap(k, max_row);
245 piv.swap(k, max_row);
246
247 for i in (k + 1)..n {
248 lu[i][k] /= lu[k][k];
249 for j in (k + 1)..n {
250 let factor = lu[i][k] * lu[k][j];
251 lu[i][j] -= factor;
252 }
253 }
254 }
255 Ok((lu, piv))
256}
257
258fn lu_solve(lu: &[Vec<f64>], piv: &[usize], b: &[f64]) -> std::result::Result<Vec<f64>, String> {
260 let n = lu.len();
261 let mut x: Vec<f64> = vec![0.0; n];
263 for i in 0..n {
264 x[i] = b[piv[i]];
265 }
266 for i in 1..n {
268 for j in 0..i {
269 x[i] -= lu[i][j] * x[j];
270 }
271 }
272 for i in (0..n).rev() {
274 for j in (i + 1)..n {
275 x[i] -= lu[i][j] * x[j];
276 }
277 if lu[i][i].abs() < 1e-300 {
278 return Err(format!("zero pivot at {i}"));
279 }
280 x[i] /= lu[i][i];
281 }
282 Ok(x)
283}
284
285fn smallest_k_eigen(
290 m: &Array2<f64>,
291 k: usize,
292 seed: u64,
293) -> std::result::Result<(Vec<f64>, Vec<Vec<f64>>), String> {
294 let n = m.nrows();
295 if k == 0 {
296 return Ok((vec![], vec![]));
297 }
298 if k > n {
299 return Err(format!("k={k} > n={n}"));
300 }
301
302 let (lambda_max, _) = power_iteration(m, 200, 1e-8, seed);
304
305 let mut eigenvalues: Vec<f64> = Vec::with_capacity(k);
306 let mut eigenvectors: Vec<Vec<f64>> = Vec::with_capacity(k);
307
308 let mut shift = -1e-4;
310
311 for idx in 0..k {
312 let trial_shift = if idx == 0 {
315 -1e-4 } else {
317 eigenvalues[idx - 1] - 1e-3
319 };
320 shift = trial_shift.min(shift);
321
322 let result = inverse_iteration(m, shift, &eigenvectors, 150, 1e-8, seed + idx as u64);
324 let (eval, evec) = match result {
325 Ok(r) => r,
326 Err(_) => {
327 inverse_iteration(
329 m,
330 shift + 1e-6,
331 &eigenvectors,
332 150,
333 1e-8,
334 seed + idx as u64 + 1000,
335 )?
336 }
337 };
338
339 let clamped = if eval.abs() < 1e-6 { 0.0 } else { eval };
341 eigenvalues.push(clamped);
342 eigenvectors.push(evec);
343 shift = clamped + (lambda_max - clamped) * 0.01;
345 }
346
347 Ok((eigenvalues, eigenvectors))
348}
349
350pub fn fiedler_vector(adj: &Array2<f64>) -> Result<(f64, Vec<f64>)> {
364 let n = adj.nrows();
365 if n < 2 {
366 return Err(GraphError::InvalidGraph(
367 "need at least 2 nodes for Fiedler vector".into(),
368 ));
369 }
370
371 let lap = graph_laplacian(adj);
372 let (evals, evecs) = smallest_k_eigen(&lap, 2, 12345).map_err(|e| GraphError::LinAlgError {
373 operation: "fiedler_vector".into(),
374 details: e,
375 })?;
376
377 if evals.len() < 2 {
378 return Err(GraphError::LinAlgError {
379 operation: "fiedler_vector".into(),
380 details: "could not compute second eigenvalue".into(),
381 });
382 }
383
384 Ok((evals[1], evecs[1].clone()))
385}
386
387#[derive(Debug, Clone)]
393pub struct SpectralClusterResult {
394 pub assignments: Vec<usize>,
396 pub eigenvalues: Vec<f64>,
398 pub embedding: Array2<f64>,
400}
401
402pub fn spectral_clustering(
413 adj: &Array2<f64>,
414 k: usize,
415 seed: u64,
416) -> Result<SpectralClusterResult> {
417 let n = adj.nrows();
418 if n == 0 {
419 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
420 }
421 if k == 0 || k > n {
422 return Err(GraphError::InvalidParameter {
423 param: "k".into(),
424 value: k.to_string(),
425 expected: "1..=n".into(),
426 context: "spectral_clustering".into(),
427 });
428 }
429
430 let l_norm = normalized_laplacian(adj)?;
431 let (evals, evecs) =
432 smallest_k_eigen(&l_norm, k, seed).map_err(|e| GraphError::LinAlgError {
433 operation: "spectral_clustering".into(),
434 details: e,
435 })?;
436
437 let mut embedding = Array2::zeros((n, k));
439 for col in 0..k {
440 if col < evecs.len() {
441 for row in 0..n {
442 embedding[[row, col]] = evecs[col][row];
443 }
444 }
445 }
446
447 let assignments = kmeans_lloyd(&embedding, k, 300, seed)?;
449
450 Ok(SpectralClusterResult {
451 assignments,
452 eigenvalues: evals,
453 embedding,
454 })
455}
456
457fn kmeans_lloyd(data: &Array2<f64>, k: usize, max_iter: usize, seed: u64) -> Result<Vec<usize>> {
460 let n = data.nrows();
461 let d = data.ncols();
462 if n == 0 || k == 0 {
463 return Ok(vec![]);
464 }
465
466 let mut rng = StdRng::seed_from_u64(seed);
468
469 let mut centroids: Vec<Vec<f64>> = Vec::with_capacity(k);
471 let first_idx = rng.random_range(0..n);
472 centroids.push(data.row(first_idx).to_vec());
473
474 for _ in 1..k {
476 let dists: Vec<f64> = (0..n)
477 .map(|i| {
478 let row = data.row(i);
479 centroids
480 .iter()
481 .map(|c| {
482 row.iter()
483 .zip(c.iter())
484 .map(|(a, b)| (a - b).powi(2))
485 .sum::<f64>()
486 })
487 .fold(f64::INFINITY, f64::min)
488 })
489 .collect();
490
491 let total: f64 = dists.iter().sum();
492 let mut r = rng.random::<f64>() * total;
493 let mut chosen = n - 1;
494 for (i, &d_val) in dists.iter().enumerate() {
495 r -= d_val;
496 if r <= 0.0 {
497 chosen = i;
498 break;
499 }
500 }
501 centroids.push(data.row(chosen).to_vec());
502 }
503
504 let mut assignments = vec![0usize; n];
505
506 for _iter in 0..max_iter {
507 let mut changed = false;
509 for i in 0..n {
510 let row = data.row(i);
511 let mut best_dist = f64::INFINITY;
512 let mut best_c = 0;
513 for (ci, centroid) in centroids.iter().enumerate() {
514 let dist: f64 = row
515 .iter()
516 .zip(centroid.iter())
517 .map(|(a, b)| (a - b).powi(2))
518 .sum();
519 if dist < best_dist {
520 best_dist = dist;
521 best_c = ci;
522 }
523 }
524 if assignments[i] != best_c {
525 changed = true;
526 assignments[i] = best_c;
527 }
528 }
529
530 if !changed {
531 break;
532 }
533
534 let mut sums = vec![vec![0.0f64; d]; k];
536 let mut counts = vec![0usize; k];
537 for i in 0..n {
538 let c = assignments[i];
539 counts[c] += 1;
540 for j in 0..d {
541 sums[c][j] += data[[i, j]];
542 }
543 }
544 for c in 0..k {
545 if counts[c] > 0 {
546 for j in 0..d {
547 centroids[c][j] = sums[c][j] / counts[c] as f64;
548 }
549 }
550 }
551 }
552
553 Ok(assignments)
554}
555
556pub fn graph_fourier_transform(adj: &Array2<f64>, signal: &[f64]) -> Result<Vec<f64>> {
570 let n = adj.nrows();
571 if n == 0 {
572 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
573 }
574 if signal.len() != n {
575 return Err(GraphError::InvalidParameter {
576 param: "signal".into(),
577 value: signal.len().to_string(),
578 expected: format!("{n}"),
579 context: "graph_fourier_transform".into(),
580 });
581 }
582
583 let lap = graph_laplacian(adj);
584 let (_, evecs) = smallest_k_eigen(&lap, n, 42).map_err(|e| GraphError::LinAlgError {
585 operation: "graph_fourier_transform".into(),
586 details: e,
587 })?;
588
589 let coeffs: Vec<f64> = evecs
591 .iter()
592 .map(|uk| uk.iter().zip(signal.iter()).map(|(a, b)| a * b).sum())
593 .collect();
594
595 Ok(coeffs)
596}
597
598fn pinv_symmetric(m: &Array2<f64>, tol: f64) -> std::result::Result<Array2<f64>, String> {
606 let n = m.nrows();
607 if n == 0 {
608 return Ok(Array2::zeros((0, 0)));
609 }
610 let (evals, evecs) = smallest_k_eigen(m, n, 999)?;
611
612 let mut pinv = Array2::zeros((n, n));
613 for (k, &lambda) in evals.iter().enumerate() {
614 if lambda.abs() <= tol {
615 continue;
616 }
617 let uk = &evecs[k];
618 for i in 0..n {
619 for j in 0..n {
620 pinv[[i, j]] += uk[i] * uk[j] / lambda;
621 }
622 }
623 }
624 Ok(pinv)
625}
626
627pub fn effective_resistance(adj: &Array2<f64>, i: usize, j: usize) -> Result<f64> {
638 let n = adj.nrows();
639 if n == 0 {
640 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
641 }
642 if i >= n || j >= n {
643 return Err(GraphError::InvalidParameter {
644 param: "i or j".into(),
645 value: format!("({i},{j})"),
646 expected: format!("< {n}"),
647 context: "effective_resistance".into(),
648 });
649 }
650 if i == j {
651 return Ok(0.0);
652 }
653
654 let lap = graph_laplacian(adj);
655 let lp = pinv_symmetric(&lap, 1e-9).map_err(|e| GraphError::LinAlgError {
656 operation: "effective_resistance".into(),
657 details: e,
658 })?;
659
660 let r = lp[[i, i]] + lp[[j, j]] - 2.0 * lp[[i, j]];
661 Ok(r.max(0.0))
662}
663
664pub fn resistance_matrix(adj: &Array2<f64>) -> Result<Array2<f64>> {
669 let n = adj.nrows();
670 if n == 0 {
671 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
672 }
673 if adj.ncols() != n {
674 return Err(GraphError::InvalidGraph(
675 "adjacency matrix must be square".into(),
676 ));
677 }
678
679 let lap = graph_laplacian(adj);
680 let lp = pinv_symmetric(&lap, 1e-9).map_err(|e| GraphError::LinAlgError {
681 operation: "resistance_matrix".into(),
682 details: e,
683 })?;
684
685 let mut r_mat = Array2::zeros((n, n));
686 for i in 0..n {
687 for j in 0..n {
688 if i != j {
689 let r = lp[[i, i]] + lp[[j, j]] - 2.0 * lp[[i, j]];
690 r_mat[[i, j]] = r.max(0.0);
691 }
692 }
693 }
694 Ok(r_mat)
695}
696
697#[cfg(test)]
702mod tests {
703 use super::*;
704 use scirs2_core::ndarray::Array2;
705
706 fn path_adj(n: usize) -> Array2<f64> {
707 let mut adj = Array2::zeros((n, n));
708 for i in 0..(n - 1) {
709 adj[[i, i + 1]] = 1.0;
710 adj[[i + 1, i]] = 1.0;
711 }
712 adj
713 }
714
715 fn complete_adj(n: usize) -> Array2<f64> {
716 let mut adj = Array2::ones((n, n));
717 for i in 0..n {
718 adj[[i, i]] = 0.0;
719 }
720 adj
721 }
722
723 fn complete_bipartite(m: usize, n: usize) -> Array2<f64> {
725 let total = m + n;
726 let mut adj = Array2::zeros((total, total));
727 for i in 0..m {
728 for j in m..total {
729 adj[[i, j]] = 1.0;
730 adj[[j, i]] = 1.0;
731 }
732 }
733 adj
734 }
735
736 #[test]
739 fn test_laplacian_path3() {
740 let adj = path_adj(3);
742 let l = graph_laplacian(&adj);
743 for i in 0..3 {
745 let row_sum: f64 = l.row(i).sum();
746 assert!(row_sum.abs() < 1e-12, "row {i} sum = {row_sum}");
747 }
748 assert!((l[[0, 0]] - 1.0).abs() < 1e-12);
750 assert!((l[[1, 1]] - 2.0).abs() < 1e-12);
751 assert!((l[[2, 2]] - 1.0).abs() < 1e-12);
752 }
753
754 #[test]
755 fn test_laplacian_complete4() {
756 let adj = complete_adj(4);
757 let l = graph_laplacian(&adj);
758 for i in 0..4 {
759 let row_sum: f64 = l.row(i).sum();
760 assert!(row_sum.abs() < 1e-12, "row {i} sum = {row_sum}");
761 assert!((l[[i, i]] - 3.0).abs() < 1e-12);
762 }
763 }
764
765 #[test]
768 fn test_normalized_laplacian_diagonal_ones() {
769 let adj = complete_adj(4); let l_norm = normalized_laplacian(&adj).expect("norm lap");
772 for i in 0..4 {
773 assert!(
774 (l_norm[[i, i]] - 1.0).abs() < 1e-10,
775 "diagonal[{i}] = {}",
776 l_norm[[i, i]]
777 );
778 }
779 }
780
781 #[test]
782 fn test_normalized_laplacian_symmetric() {
783 let adj = path_adj(5);
784 let l_norm = normalized_laplacian(&adj).expect("norm lap");
785 for i in 0..5 {
786 for j in 0..5 {
787 assert!(
788 (l_norm[[i, j]] - l_norm[[j, i]]).abs() < 1e-12,
789 "not symmetric at ({i},{j})"
790 );
791 }
792 }
793 }
794
795 #[test]
798 fn test_fiedler_value_positive_connected() {
799 let adj = path_adj(6);
801 let (lambda2, fv) = fiedler_vector(&adj).expect("fiedler");
802 assert!(
803 lambda2 > 1e-6,
804 "Fiedler value should be positive: {lambda2}"
805 );
806 assert_eq!(fv.len(), 6);
807 let norm: f64 = fv.iter().map(|x| x * x).sum::<f64>().sqrt();
809 assert!(
810 (norm - 1.0).abs() < 1e-6,
811 "Fiedler vector should be unit norm: {norm}"
812 );
813 }
814
815 #[test]
816 fn test_fiedler_bisection() {
817 let adj = path_adj(4);
819 let (_lambda2, fv) = fiedler_vector(&adj).expect("fiedler");
820 let left_mean = (fv[0] + fv[1]) / 2.0;
823 let right_mean = (fv[2] + fv[3]) / 2.0;
824 assert!(
825 left_mean * right_mean < 0.0,
826 "Fiedler vector should bisect: left={left_mean}, right={right_mean}"
827 );
828 }
829
830 #[test]
831 fn test_fiedler_error_single_node() {
832 let adj = Array2::<f64>::zeros((1, 1));
833 assert!(fiedler_vector(&adj).is_err());
834 }
835
836 #[test]
839 fn test_spectral_clustering_shapes() {
840 let adj = path_adj(6);
841 let result = spectral_clustering(&adj, 2, 42).expect("spectral clustering");
842 assert_eq!(result.assignments.len(), 6);
843 assert_eq!(result.eigenvalues.len(), 2);
844 assert_eq!(result.embedding.nrows(), 6);
845 assert_eq!(result.embedding.ncols(), 2);
846 }
847
848 #[test]
849 fn test_spectral_clustering_two_components() {
850 let mut adj = Array2::zeros((6, 6));
852 adj[[0, 1]] = 1.0;
853 adj[[1, 0]] = 1.0;
854 adj[[1, 2]] = 1.0;
855 adj[[2, 1]] = 1.0;
856 adj[[3, 4]] = 1.0;
857 adj[[4, 3]] = 1.0;
858 adj[[4, 5]] = 1.0;
859 adj[[5, 4]] = 1.0;
860
861 let result = spectral_clustering(&adj, 2, 7).expect("spectral clustering");
862 let c0 = result.assignments[0];
864 let c3 = result.assignments[3];
865 assert_ne!(
866 c0, c3,
867 "disconnected components should be in different clusters"
868 );
869 assert_eq!(result.assignments[0], result.assignments[1]);
871 assert_eq!(result.assignments[1], result.assignments[2]);
872 assert_eq!(result.assignments[3], result.assignments[4]);
873 assert_eq!(result.assignments[4], result.assignments[5]);
874 }
875
876 #[test]
877 fn test_spectral_clustering_invalid_k() {
878 let adj = path_adj(4);
879 assert!(spectral_clustering(&adj, 0, 0).is_err());
880 assert!(spectral_clustering(&adj, 5, 0).is_err());
881 }
882
883 #[test]
886 fn test_gft_length() {
887 let adj = path_adj(4);
888 let signal = vec![1.0, 0.0, 0.0, 0.0];
889 let coeffs = graph_fourier_transform(&adj, &signal).expect("gft");
890 assert_eq!(coeffs.len(), 4);
891 }
892
893 #[test]
894 fn test_gft_constant_signal() {
895 let adj = complete_adj(4);
897 let signal = vec![1.0; 4];
898 let coeffs = graph_fourier_transform(&adj, &signal).expect("gft");
899 assert_eq!(coeffs.len(), 4);
900 let max_coeff = coeffs.iter().map(|x| x.abs()).fold(0.0f64, f64::max);
902 assert!(max_coeff > 0.5, "DC component should dominate: {coeffs:?}");
903 }
904
905 #[test]
908 fn test_effective_resistance_self() {
909 let adj = path_adj(4);
910 let r = effective_resistance(&adj, 1, 1).expect("eff res");
911 assert_eq!(r, 0.0);
912 }
913
914 #[test]
915 fn test_effective_resistance_path3() {
916 let adj = path_adj(3);
919 let r01 = effective_resistance(&adj, 0, 1).expect("r01");
920 let r12 = effective_resistance(&adj, 1, 2).expect("r12");
921 let r02 = effective_resistance(&adj, 0, 2).expect("r02");
922 assert!((r01 - 1.0).abs() < 1e-4, "R(0,1) = {r01}");
923 assert!((r12 - 1.0).abs() < 1e-4, "R(1,2) = {r12}");
924 assert!((r02 - 2.0).abs() < 1e-4, "R(0,2) = {r02}");
925 }
926
927 #[test]
928 fn test_effective_resistance_complete_graph() {
929 let n = 4;
931 let adj = complete_adj(n);
932 let r = effective_resistance(&adj, 0, 1).expect("eff res");
933 let expected = 2.0 / n as f64;
934 assert!(
935 (r - expected).abs() < 1e-4,
936 "K{n} effective resistance = {r}, expected {expected}"
937 );
938 }
939
940 #[test]
943 fn test_resistance_matrix_shape() {
944 let adj = path_adj(4);
945 let r_mat = resistance_matrix(&adj).expect("res mat");
946 assert_eq!(r_mat.nrows(), 4);
947 assert_eq!(r_mat.ncols(), 4);
948 }
949
950 #[test]
951 fn test_resistance_matrix_symmetric() {
952 let adj = path_adj(5);
953 let r_mat = resistance_matrix(&adj).expect("res mat");
954 for i in 0..5 {
955 for j in 0..5 {
956 assert!(
957 (r_mat[[i, j]] - r_mat[[j, i]]).abs() < 1e-8,
958 "not symmetric at ({i},{j})"
959 );
960 }
961 }
962 }
963
964 #[test]
965 fn test_resistance_matrix_zero_diagonal() {
966 let adj = complete_adj(4);
967 let r_mat = resistance_matrix(&adj).expect("res mat");
968 for i in 0..4 {
969 assert!(
970 r_mat[[i, i]].abs() < 1e-10,
971 "diagonal[{i}] = {}",
972 r_mat[[i, i]]
973 );
974 }
975 }
976
977 #[test]
978 fn test_resistance_matrix_path3_values() {
979 let adj = path_adj(3);
980 let r_mat = resistance_matrix(&adj).expect("res mat");
981 assert!(
983 (r_mat[[0, 1]] - 1.0).abs() < 1e-4,
984 "R(0,1) = {}",
985 r_mat[[0, 1]]
986 );
987 assert!(
988 (r_mat[[1, 2]] - 1.0).abs() < 1e-4,
989 "R(1,2) = {}",
990 r_mat[[1, 2]]
991 );
992 assert!(
993 (r_mat[[0, 2]] - 2.0).abs() < 1e-4,
994 "R(0,2) = {}",
995 r_mat[[0, 2]]
996 );
997 }
998
999 #[test]
1000 fn test_bipartite_laplacian_max_eigenvalue() {
1001 let adj = complete_bipartite(2, 2);
1003 let l_norm = normalized_laplacian(&adj).expect("norm lap");
1004 let (lambda_max, _) = power_iteration(&l_norm, 500, 1e-10, 77);
1006 assert!(
1007 (lambda_max - 2.0).abs() < 0.05,
1008 "K_{{2,2}} max eigenvalue of L_norm = {lambda_max}, expected 2.0"
1009 );
1010 }
1011}