1use crate::core::{Graph, IgraphError, IgraphResult};
48
49const DEFAULT_EPS: f64 = 1e-14;
52const DEFAULT_MAX_ITER: usize = 5000;
53
54const SHIFTED_EPS: f64 = 1e-12;
57const SHIFTED_MAX_ITER: usize = 5000;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum EigenvectorMode {
64 Out,
69 In,
73 All,
76}
77
78#[derive(Debug, Clone, PartialEq)]
82pub struct EigenvectorScores {
83 pub vector: Vec<f64>,
87 pub eigenvalue: f64,
91}
92
93pub fn eigenvector_centrality(graph: &Graph) -> IgraphResult<Vec<f64>> {
119 if graph.is_directed() {
120 return Err(IgraphError::Unsupported(
121 "directed eigenvector_centrality — use eigenvector_centrality_directed or _full",
122 ));
123 }
124 undirected_unweighted(graph).map(|s| s.vector)
125}
126
127pub fn eigenvector_centrality_weighted(
158 graph: &Graph,
159 weights: &[f64],
160) -> IgraphResult<EigenvectorScores> {
161 if graph.is_directed() {
162 return Err(IgraphError::Unsupported(
163 "directed eigenvector_centrality_weighted — use eigenvector_centrality_directed_weighted",
164 ));
165 }
166 validate_weights(graph, weights)?;
167 undirected_weighted(graph, Some(weights))
168}
169
170pub fn eigenvector_centrality_directed(
192 graph: &Graph,
193 mode: EigenvectorMode,
194) -> IgraphResult<EigenvectorScores> {
195 eigenvector_centrality_full(graph, mode, None)
196}
197
198pub fn eigenvector_centrality_directed_weighted(
206 graph: &Graph,
207 mode: EigenvectorMode,
208 weights: &[f64],
209) -> IgraphResult<EigenvectorScores> {
210 eigenvector_centrality_full(graph, mode, Some(weights))
211}
212
213pub fn eigenvector_centrality_full(
222 graph: &Graph,
223 mode: EigenvectorMode,
224 weights: Option<&[f64]>,
225) -> IgraphResult<EigenvectorScores> {
226 if let Some(w) = weights {
227 validate_weights(graph, w)?;
228 }
229 let effective_mode = if graph.is_directed() {
231 mode
232 } else {
233 EigenvectorMode::All
234 };
235 match effective_mode {
236 EigenvectorMode::All => match weights {
237 None => undirected_unweighted(graph),
238 Some(w) => undirected_weighted(graph, Some(w)),
239 },
240 EigenvectorMode::Out | EigenvectorMode::In => directed_path(graph, effective_mode, weights),
241 }
242}
243
244fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
247 let m = graph.ecount();
248 if weights.len() != m {
249 return Err(IgraphError::InvalidArgument(format!(
250 "weights vector length ({}) not equal to number of edges ({})",
251 weights.len(),
252 m
253 )));
254 }
255 Ok(())
256}
257
258fn undirected_unweighted(graph: &Graph) -> IgraphResult<EigenvectorScores> {
261 let n = graph.vcount();
262 let n_us = n as usize;
263 if n == 0 {
264 return Ok(EigenvectorScores {
265 vector: Vec::new(),
266 eigenvalue: 0.0,
267 });
268 }
269 if graph.ecount() == 0 {
270 return Ok(EigenvectorScores {
271 vector: vec![1.0; n_us],
272 eigenvalue: 0.0,
273 });
274 }
275 if n == 1 {
276 return Ok(EigenvectorScores {
277 vector: vec![1.0],
278 eigenvalue: 0.0,
279 });
280 }
281
282 let mut adj: Vec<Vec<u32>> = Vec::with_capacity(n_us);
283 for v in 0..n {
284 adj.push(graph.neighbors(v)?);
285 }
286
287 let mut x = vec![1.0_f64; n_us];
288 let mut x_new = vec![0.0_f64; n_us];
289
290 for _ in 0..DEFAULT_MAX_ITER {
291 for v in 0..n_us {
293 let mut sum = x[v];
294 for &u in &adj[v] {
295 sum += x[u as usize];
296 }
297 x_new[v] = sum;
298 }
299 let max = x_new.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
300 if max > 0.0 {
301 for slot in &mut x_new {
302 *slot /= max;
303 }
304 }
305 let mut diff = 0.0_f64;
306 for v in 0..n_us {
307 diff += (x_new[v] - x[v]).abs();
308 }
309 std::mem::swap(&mut x, &mut x_new);
310 if diff < DEFAULT_EPS {
311 break;
312 }
313 }
314
315 let mut ax = vec![0.0_f64; n_us];
317 for v in 0..n_us {
318 let mut sum = 0.0_f64;
319 for &u in &adj[v] {
320 sum += x[u as usize];
321 }
322 ax[v] = sum;
323 }
324 let num: f64 = x.iter().zip(ax.iter()).map(|(a, b)| a * b).sum();
325 let den: f64 = x.iter().map(|a| a * a).sum();
326 let eigenvalue = if den > 0.0 { num / den } else { 0.0 };
327
328 for slot in &mut x {
331 if *slot < 0.0 {
332 *slot = 0.0;
333 }
334 }
335
336 Ok(EigenvectorScores {
337 vector: x,
338 eigenvalue,
339 })
340}
341
342#[allow(
345 clippy::too_many_lines,
346 clippy::many_single_char_names,
347 clippy::cast_possible_truncation
348)]
349fn undirected_weighted(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<EigenvectorScores> {
350 let n = graph.vcount();
351 let n_us = n as usize;
352 let m = graph.ecount();
353 if n == 0 {
354 return Ok(EigenvectorScores {
355 vector: Vec::new(),
356 eigenvalue: 0.0,
357 });
358 }
359 if m == 0 {
360 return Ok(EigenvectorScores {
361 vector: vec![1.0; n_us],
362 eigenvalue: 0.0,
363 });
364 }
365 if let Some(w) = weights {
366 if w.iter().all(|&x| x == 0.0) {
367 return Ok(EigenvectorScores {
368 vector: vec![1.0; n_us],
369 eigenvalue: 0.0,
370 });
371 }
372 }
373 if n == 1 {
374 return Ok(EigenvectorScores {
375 vector: vec![1.0],
376 eigenvalue: 0.0,
377 });
378 }
379
380 let negative_weights = weights.is_some_and(|w| w.iter().any(|&x| x < 0.0));
381
382 let mut inc: Vec<Vec<u32>> = Vec::with_capacity(n_us);
385 for v in 0..n {
386 inc.push(graph.incident(v)?);
387 }
388
389 let mut x = vec![0.0_f64; n_us];
391 for v in 0..n {
392 let v_us = v as usize;
393 let mut strength = 0.0_f64;
394 for &e in &inc[v_us] {
395 let w = weights.map_or(1.0, |ws| ws[e as usize]);
396 strength += w;
397 }
398 if strength != 0.0 {
399 x[v_us] = strength.abs();
400 } else if !negative_weights {
401 x[v_us] = 0.0;
402 } else {
403 x[v_us] = if inc[v_us].is_empty() { 0.0 } else { 1.0 };
406 }
407 }
408 if x.iter().all(|&y| y == 0.0) {
410 x.fill(1.0);
411 }
412 let max0 = x.iter().fold(0.0_f64, |acc, &y| acc.max(y.abs()));
414 if max0 > 0.0 {
415 for slot in &mut x {
416 *slot /= max0;
417 }
418 }
419
420 let shift = 1.0_f64;
425 let mut x_new = vec![0.0_f64; n_us];
426
427 for _ in 0..SHIFTED_MAX_ITER {
428 for v in 0..n_us {
430 let mut sum = shift * x[v];
431 for &e in &inc[v] {
432 let other = graph.edge_other(e, v as u32)?;
433 let w = weights.map_or(1.0, |ws| ws[e as usize]);
434 sum += w * x[other as usize];
435 }
436 x_new[v] = sum;
437 }
438 let pivot = if negative_weights {
443 let mut best = 0.0_f64;
444 for &v in &x_new {
445 if v.abs() > best.abs() {
446 best = v;
447 }
448 }
449 best
450 } else {
451 x_new.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()))
452 };
453 if pivot != 0.0 {
454 for slot in &mut x_new {
455 *slot /= pivot;
456 }
457 }
458 let mut diff = 0.0_f64;
459 for v in 0..n_us {
460 diff += (x_new[v] - x[v]).abs();
461 }
462 std::mem::swap(&mut x, &mut x_new);
463 if diff < SHIFTED_EPS {
464 break;
465 }
466 }
467
468 let mut wx = vec![0.0_f64; n_us];
470 for v in 0..n_us {
471 let mut sum = 0.0_f64;
472 for &e in &inc[v] {
473 let other = graph.edge_other(e, v as u32)?;
474 let w = weights.map_or(1.0, |ws| ws[e as usize]);
475 sum += w * x[other as usize];
476 }
477 wx[v] = sum;
478 }
479 let num: f64 = x.iter().zip(wx.iter()).map(|(a, b)| a * b).sum();
480 let den: f64 = x.iter().map(|a| a * a).sum();
481 let eigenvalue = if den > 0.0 { num / den } else { 0.0 };
482
483 let max = x.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
486 if max > 0.0 {
487 for slot in &mut x {
488 *slot /= max;
489 }
490 }
491 if !negative_weights {
492 for slot in &mut x {
493 if *slot < 0.0 {
494 *slot = 0.0;
495 }
496 }
497 }
498
499 Ok(EigenvectorScores {
500 vector: x,
501 eigenvalue,
502 })
503}
504
505#[allow(
508 clippy::too_many_lines,
509 clippy::many_single_char_names,
510 clippy::cast_possible_truncation,
511 clippy::needless_range_loop
512)]
513fn directed_path(
514 graph: &Graph,
515 mode: EigenvectorMode,
516 weights: Option<&[f64]>,
517) -> IgraphResult<EigenvectorScores> {
518 let n = graph.vcount();
519 let n_us = n as usize;
520 let m = graph.ecount();
521 if n == 0 {
522 return Ok(EigenvectorScores {
523 vector: Vec::new(),
524 eigenvalue: 0.0,
525 });
526 }
527 if m == 0 {
528 return Ok(EigenvectorScores {
529 vector: vec![1.0; n_us],
530 eigenvalue: 0.0,
531 });
532 }
533 if let Some(w) = weights {
534 if w.iter().all(|&x| x == 0.0) {
535 return Ok(EigenvectorScores {
536 vector: vec![1.0; n_us],
537 eigenvalue: 0.0,
538 });
539 }
540 }
541
542 let negative_weights = weights.is_some_and(|w| w.iter().any(|&x| x < 0.0));
543
544 if !negative_weights && crate::algorithms::properties::is_dag::is_dag(graph) {
548 return dag_sentinel(graph, mode, weights);
549 }
550
551 let mut walk: Vec<Vec<(u32, u32)>> = vec![Vec::new(); n_us];
555 let m_u32 = u32::try_from(m)
556 .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32::MAX".into()))?;
557 match mode {
558 EigenvectorMode::Out => {
559 for e in 0..m_u32 {
560 let (u, v) = graph.edge(e)?;
561 walk[v as usize].push((e, u));
562 }
563 }
564 EigenvectorMode::In => {
565 for e in 0..m_u32 {
566 let (u, v) = graph.edge(e)?;
567 walk[u as usize].push((e, v));
568 }
569 }
570 EigenvectorMode::All => unreachable!("All is dispatched to undirected_*"),
571 }
572
573 let mut x = vec![0.0_f64; n_us];
576 for v in 0..n_us {
577 let mut strength = 0.0_f64;
578 for &(e, _) in &walk[v] {
579 let w = weights.map_or(1.0, |ws| ws[e as usize]);
580 strength += w;
581 }
582 if strength != 0.0 {
583 x[v] = strength.abs();
584 } else if !negative_weights {
585 x[v] = 0.0;
586 } else {
587 x[v] = if walk[v].is_empty() { 0.0 } else { 1.0 };
588 }
589 }
590 if x.iter().all(|&y| y == 0.0) {
591 x.fill(1.0);
592 }
593 let max0 = x.iter().fold(0.0_f64, |acc, &y| acc.max(y.abs()));
594 if max0 > 0.0 {
595 for slot in &mut x {
596 *slot /= max0;
597 }
598 }
599
600 let mut row_norm: f64 = 0.0;
606 for v in 0..n_us {
607 let mut s = 0.0_f64;
608 for &(e, _) in &walk[v] {
609 let w = weights.map_or(1.0, |ws| ws[e as usize]);
610 s += w.abs();
611 }
612 if s > row_norm {
613 row_norm = s;
614 }
615 }
616 let shift = row_norm.max(1.0) + 1.0;
617
618 let mut x_new = vec![0.0_f64; n_us];
619 for _ in 0..SHIFTED_MAX_ITER {
620 for v in 0..n_us {
621 let mut sum = shift * x[v];
622 for &(e, nei) in &walk[v] {
623 let w = weights.map_or(1.0, |ws| ws[e as usize]);
624 sum += w * x[nei as usize];
625 }
626 x_new[v] = sum;
627 }
628 let pivot = if negative_weights {
629 let mut best = 0.0_f64;
630 for &v in &x_new {
631 if v.abs() > best.abs() {
632 best = v;
633 }
634 }
635 best
636 } else {
637 x_new.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()))
638 };
639 if pivot != 0.0 {
640 for slot in &mut x_new {
641 *slot /= pivot;
642 }
643 }
644 let mut diff = 0.0_f64;
645 for v in 0..n_us {
646 diff += (x_new[v] - x[v]).abs();
647 }
648 std::mem::swap(&mut x, &mut x_new);
649 if diff < SHIFTED_EPS {
650 break;
651 }
652 }
653
654 let mut mx = vec![0.0_f64; n_us];
656 for v in 0..n_us {
657 let mut sum = 0.0_f64;
658 for &(e, nei) in &walk[v] {
659 let w = weights.map_or(1.0, |ws| ws[e as usize]);
660 sum += w * x[nei as usize];
661 }
662 mx[v] = sum;
663 }
664 let num: f64 = x.iter().zip(mx.iter()).map(|(a, b)| a * b).sum();
665 let den: f64 = x.iter().map(|a| a * a).sum();
666 let mut eigenvalue = if den > 0.0 { num / den } else { 0.0 };
667
668 if !negative_weights && eigenvalue <= SHIFTED_EPS {
672 eigenvalue = 0.0;
673 x.fill(0.0);
674 } else {
675 let max = x.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
676 if max > 0.0 {
677 for slot in &mut x {
678 *slot /= max;
679 }
680 }
681 if !negative_weights {
682 for slot in &mut x {
683 if *slot < 0.0 {
684 *slot = 0.0;
685 }
686 }
687 }
688 }
689
690 Ok(EigenvectorScores {
691 vector: x,
692 eigenvalue,
693 })
694}
695
696#[allow(clippy::many_single_char_names)]
698fn dag_sentinel(
699 graph: &Graph,
700 mode: EigenvectorMode,
701 weights: Option<&[f64]>,
702) -> IgraphResult<EigenvectorScores> {
703 let n = graph.vcount();
704 let n_us = n as usize;
705 let m = graph.ecount();
706 let mut strength = vec![0.0_f64; n_us];
711 let m_u32 = u32::try_from(m)
712 .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32::MAX".into()))?;
713 for e in 0..m_u32 {
714 let (u, v) = graph.edge(e)?;
715 let w = weights.map_or(1.0, |ws| ws[e as usize]);
716 match mode {
717 EigenvectorMode::Out => strength[u as usize] += w,
718 EigenvectorMode::In => strength[v as usize] += w,
719 EigenvectorMode::All => unreachable!(),
720 }
721 }
722 let vector: Vec<f64> = strength
723 .iter()
724 .map(|&s| if s == 0.0 { 1.0 } else { 0.0 })
725 .collect();
726 Ok(EigenvectorScores {
727 vector,
728 eigenvalue: 0.0,
729 })
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 fn close(actual: &[f64], expected: &[f64], tol: f64) {
737 assert_eq!(actual.len(), expected.len(), "length mismatch");
738 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
739 assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
740 }
741 }
742
743 #[test]
744 fn empty_graph_yields_empty() {
745 let g = Graph::with_vertices(0);
746 assert!(eigenvector_centrality(&g).unwrap().is_empty());
747 }
748
749 #[test]
750 fn singleton_yields_one() {
751 let g = Graph::with_vertices(1);
752 assert_eq!(eigenvector_centrality(&g).unwrap(), vec![1.0]);
753 }
754
755 #[test]
756 fn isolated_vertices_yield_uniform_one() {
757 let g = Graph::with_vertices(3);
758 let ec = eigenvector_centrality(&g).unwrap();
759 close(&ec, &[1.0, 1.0, 1.0], 1e-9);
760 }
761
762 #[test]
763 fn triangle_uniform_one() {
764 let mut g = Graph::with_vertices(3);
765 g.add_edge(0, 1).unwrap();
766 g.add_edge(1, 2).unwrap();
767 g.add_edge(2, 0).unwrap();
768 let ec = eigenvector_centrality(&g).unwrap();
769 close(&ec, &[1.0, 1.0, 1.0], 1e-9);
770 }
771
772 #[test]
773 fn star_4_centre_one_leaves_inverse_sqrt_three() {
774 let mut g = Graph::with_vertices(4);
775 for v in 1..4 {
776 g.add_edge(0, v).unwrap();
777 }
778 let ec = eigenvector_centrality(&g).unwrap();
779 let inv_sqrt3 = 1.0 / 3f64.sqrt();
780 close(&ec, &[1.0, inv_sqrt3, inv_sqrt3, inv_sqrt3], 1e-9);
781 }
782
783 #[test]
784 fn k4_uniform_one() {
785 let mut g = Graph::with_vertices(4);
786 for u in 0..4u32 {
787 for v in (u + 1)..4 {
788 g.add_edge(u, v).unwrap();
789 }
790 }
791 let ec = eigenvector_centrality(&g).unwrap();
792 close(&ec, &[1.0, 1.0, 1.0, 1.0], 1e-9);
793 }
794
795 #[test]
796 fn directed_graph_returns_unsupported() {
797 let mut g = Graph::new(3, true).unwrap();
798 g.add_edge(0, 1).unwrap();
799 assert!(eigenvector_centrality(&g).is_err());
800 }
801
802 #[test]
805 fn weighted_star_unit_matches_unweighted() {
806 let mut g = Graph::with_vertices(5);
807 for v in 1..5 {
808 g.add_edge(0, v).unwrap();
809 }
810 let s = eigenvector_centrality_weighted(&g, &vec![1.0; g.ecount()]).unwrap();
811 close(&s.vector, &[1.0, 0.5, 0.5, 0.5, 0.5], 1e-9);
812 assert!((s.eigenvalue - 2.0).abs() < 1e-6);
813 }
814
815 #[test]
816 fn weighted_k5_unit_eigenvalue_four() {
817 let mut g = Graph::with_vertices(5);
818 for u in 0..5u32 {
819 for v in (u + 1)..5 {
820 g.add_edge(u, v).unwrap();
821 }
822 }
823 let s = eigenvector_centrality_weighted(&g, &vec![1.0; g.ecount()]).unwrap();
824 close(&s.vector, &[1.0, 1.0, 1.0, 1.0, 1.0], 1e-9);
825 assert!((s.eigenvalue - 4.0).abs() < 1e-6);
826 }
827
828 #[test]
829 fn weighted_empty_returns_ones() {
830 let g = Graph::with_vertices(4);
831 let s = eigenvector_centrality_weighted(&g, &[]).unwrap();
832 close(&s.vector, &[1.0; 4], 1e-15);
833 assert!(s.eigenvalue.abs() < f64::EPSILON);
834 }
835
836 #[test]
837 fn weighted_all_zero_returns_ones() {
838 let mut g = Graph::with_vertices(3);
839 g.add_edge(0, 1).unwrap();
840 g.add_edge(1, 2).unwrap();
841 let s = eigenvector_centrality_weighted(&g, &[0.0, 0.0]).unwrap();
842 close(&s.vector, &[1.0; 3], 1e-15);
843 assert!(s.eigenvalue.abs() < f64::EPSILON);
844 }
845
846 #[test]
847 fn weighted_length_mismatch_errors() {
848 let mut g = Graph::with_vertices(3);
849 g.add_edge(0, 1).unwrap();
850 g.add_edge(1, 2).unwrap();
851 assert!(eigenvector_centrality_weighted(&g, &[1.0]).is_err());
852 }
853
854 #[test]
857 fn directed_full_eigenvalue_n_minus_one() {
858 let mut g = Graph::new(5, true).unwrap();
860 for u in 0..5u32 {
861 for v in 0..5u32 {
862 if u != v {
863 g.add_edge(u, v).unwrap();
864 }
865 }
866 }
867 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
868 close(&s.vector, &[1.0, 1.0, 1.0, 1.0, 1.0], 1e-9);
869 assert!((s.eigenvalue - 4.0).abs() < 1e-6);
870 }
871
872 #[test]
873 fn directed_dag_out_star_returns_leaves_one() {
874 let mut g = Graph::new(5, true).unwrap();
876 for v in 1..5u32 {
877 g.add_edge(0, v).unwrap();
878 }
879 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
880 close(&s.vector, &[0.0, 1.0, 1.0, 1.0, 1.0], 1e-12);
881 assert!(s.eigenvalue.abs() < f64::EPSILON);
882 }
883
884 #[test]
885 fn directed_dag_in_star_in_mode_marks_leaves() {
886 let mut g = Graph::new(5, true).unwrap();
892 for v in 1..5u32 {
893 g.add_edge(v, 0).unwrap();
894 }
895 let s = eigenvector_centrality_directed(&g, EigenvectorMode::In).unwrap();
896 close(&s.vector, &[0.0, 1.0, 1.0, 1.0, 1.0], 1e-12);
897 assert!(s.eigenvalue.abs() < f64::EPSILON);
898 }
899
900 #[test]
901 fn directed_dag_in_star_out_mode_marks_centre() {
902 let mut g = Graph::new(5, true).unwrap();
906 for v in 1..5u32 {
907 g.add_edge(v, 0).unwrap();
908 }
909 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
910 close(&s.vector, &[1.0, 0.0, 0.0, 0.0, 0.0], 1e-12);
911 assert!(s.eigenvalue.abs() < f64::EPSILON);
912 }
913
914 #[test]
915 fn directed_small_cycle_chord_eigenvalue_matches_arpack() {
916 let mut g = Graph::new(4, true).unwrap();
919 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3), (3, 0), (1, 3)])
920 .unwrap();
921 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
922 assert!(
923 (s.eigenvalue - 1.220_744).abs() < 1e-3,
924 "expected ≈ 1.22074, got {}",
925 s.eigenvalue
926 );
927 let max_idx = s
929 .vector
930 .iter()
931 .enumerate()
932 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
933 .unwrap()
934 .0;
935 assert_eq!(max_idx, 3);
936 }
937
938 #[test]
939 fn directed_empty_directed_returns_ones() {
940 let g = Graph::new(5, true).unwrap();
941 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
942 close(&s.vector, &[1.0; 5], 1e-12);
943 assert!(s.eigenvalue.abs() < f64::EPSILON);
944 }
945
946 #[test]
949 fn full_undirected_with_all_mode_works() {
950 let mut g = Graph::with_vertices(3);
951 g.add_edge(0, 1).unwrap();
952 g.add_edge(1, 2).unwrap();
953 g.add_edge(2, 0).unwrap();
954 let s = eigenvector_centrality_full(&g, EigenvectorMode::All, None).unwrap();
955 close(&s.vector, &[1.0, 1.0, 1.0], 1e-9);
956 }
957
958 #[test]
959 fn full_undirected_forces_all_mode() {
960 let mut g = Graph::with_vertices(3);
962 g.add_edge(0, 1).unwrap();
963 g.add_edge(1, 2).unwrap();
964 g.add_edge(2, 0).unwrap();
965 let a = eigenvector_centrality_full(&g, EigenvectorMode::Out, None).unwrap();
966 let b = eigenvector_centrality_full(&g, EigenvectorMode::All, None).unwrap();
967 close(&a.vector, &b.vector, 1e-12);
968 assert!((a.eigenvalue - b.eigenvalue).abs() < 1e-12);
969 }
970
971 #[test]
974 fn directed_cycle_50_converges_to_unit_eigenvalue() {
975 let n: u32 = 50;
981 let mut g = Graph::new(n, true).unwrap();
982 for i in 0..n {
983 g.add_edge(i, (i + 1) % n).unwrap();
984 }
985 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
986 assert!(
987 (s.eigenvalue - 1.0).abs() < 1e-9,
988 "expected λ ≈ 1.0, got {}",
989 s.eigenvalue
990 );
991 let mx = s.vector.iter().copied().fold(0.0_f64, f64::max);
993 let mn = s.vector.iter().copied().fold(f64::INFINITY, f64::min);
994 assert!(mx - mn < 1e-9, "vec spread {} too large", mx - mn);
995 }
996
997 #[test]
998 fn directed_k10_eigenvalue_n_minus_one() {
999 let n: u32 = 10;
1002 let mut g = Graph::new(n, true).unwrap();
1003 for u in 0..n {
1004 for v in 0..n {
1005 if u != v {
1006 g.add_edge(u, v).unwrap();
1007 }
1008 }
1009 }
1010 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
1011 assert!(
1012 (s.eigenvalue - 9.0).abs() < 1e-9,
1013 "expected λ = 9, got {}",
1014 s.eigenvalue
1015 );
1016 close(&s.vector, &[1.0; 10], 1e-9);
1017 }
1018
1019 #[test]
1020 fn directed_cycle_chord_eigenvector_components_match_arpack() {
1021 let mut g = Graph::new(4, true).unwrap();
1025 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3), (3, 0), (1, 3)])
1026 .unwrap();
1027 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
1028 assert!((s.eigenvalue - 1.220_744).abs() < 1e-4);
1029 close(
1030 &s.vector,
1031 &[0.819_173, 0.671_044, 0.549_700, 1.000_000],
1032 1e-3,
1033 );
1034 }
1035}