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(
219 graph: &Graph,
220 mode: EigenvectorMode,
221 weights: &[f64],
222) -> IgraphResult<EigenvectorScores> {
223 eigenvector_centrality_full(graph, mode, Some(weights))
224}
225
226pub fn eigenvector_centrality_full(
248 graph: &Graph,
249 mode: EigenvectorMode,
250 weights: Option<&[f64]>,
251) -> IgraphResult<EigenvectorScores> {
252 if let Some(w) = weights {
253 validate_weights(graph, w)?;
254 }
255 let effective_mode = if graph.is_directed() {
257 mode
258 } else {
259 EigenvectorMode::All
260 };
261 match effective_mode {
262 EigenvectorMode::All => match weights {
263 None => undirected_unweighted(graph),
264 Some(w) => undirected_weighted(graph, Some(w)),
265 },
266 EigenvectorMode::Out | EigenvectorMode::In => directed_path(graph, effective_mode, weights),
267 }
268}
269
270fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
273 let m = graph.ecount();
274 if weights.len() != m {
275 return Err(IgraphError::InvalidArgument(format!(
276 "weights vector length ({}) not equal to number of edges ({})",
277 weights.len(),
278 m
279 )));
280 }
281 Ok(())
282}
283
284fn undirected_unweighted(graph: &Graph) -> IgraphResult<EigenvectorScores> {
287 let n = graph.vcount();
288 let n_us = n as usize;
289 if n == 0 {
290 return Ok(EigenvectorScores {
291 vector: Vec::new(),
292 eigenvalue: 0.0,
293 });
294 }
295 if graph.ecount() == 0 {
296 return Ok(EigenvectorScores {
297 vector: vec![1.0; n_us],
298 eigenvalue: 0.0,
299 });
300 }
301 if n == 1 {
302 return Ok(EigenvectorScores {
303 vector: vec![1.0],
304 eigenvalue: 0.0,
305 });
306 }
307
308 let mut adj: Vec<Vec<u32>> = Vec::with_capacity(n_us);
309 for v in 0..n {
310 adj.push(graph.neighbors(v)?);
311 }
312
313 let mut x = vec![1.0_f64; n_us];
314 let mut x_new = vec![0.0_f64; n_us];
315
316 for _ in 0..DEFAULT_MAX_ITER {
317 for v in 0..n_us {
319 let mut sum = x[v];
320 for &u in &adj[v] {
321 sum += x[u as usize];
322 }
323 x_new[v] = sum;
324 }
325 let max = x_new.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
326 if max > 0.0 {
327 for slot in &mut x_new {
328 *slot /= max;
329 }
330 }
331 let mut diff = 0.0_f64;
332 for v in 0..n_us {
333 diff += (x_new[v] - x[v]).abs();
334 }
335 std::mem::swap(&mut x, &mut x_new);
336 if diff < DEFAULT_EPS {
337 break;
338 }
339 }
340
341 let mut ax = vec![0.0_f64; n_us];
343 for v in 0..n_us {
344 let mut sum = 0.0_f64;
345 for &u in &adj[v] {
346 sum += x[u as usize];
347 }
348 ax[v] = sum;
349 }
350 let num: f64 = x.iter().zip(ax.iter()).map(|(a, b)| a * b).sum();
351 let den: f64 = x.iter().map(|a| a * a).sum();
352 let eigenvalue = if den > 0.0 { num / den } else { 0.0 };
353
354 for slot in &mut x {
357 if *slot < 0.0 {
358 *slot = 0.0;
359 }
360 }
361
362 Ok(EigenvectorScores {
363 vector: x,
364 eigenvalue,
365 })
366}
367
368#[allow(
371 clippy::too_many_lines,
372 clippy::many_single_char_names,
373 clippy::cast_possible_truncation
374)]
375fn undirected_weighted(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<EigenvectorScores> {
376 let n = graph.vcount();
377 let n_us = n as usize;
378 let m = graph.ecount();
379 if n == 0 {
380 return Ok(EigenvectorScores {
381 vector: Vec::new(),
382 eigenvalue: 0.0,
383 });
384 }
385 if m == 0 {
386 return Ok(EigenvectorScores {
387 vector: vec![1.0; n_us],
388 eigenvalue: 0.0,
389 });
390 }
391 if let Some(w) = weights {
392 if w.iter().all(|&x| x == 0.0) {
393 return Ok(EigenvectorScores {
394 vector: vec![1.0; n_us],
395 eigenvalue: 0.0,
396 });
397 }
398 }
399 if n == 1 {
400 return Ok(EigenvectorScores {
401 vector: vec![1.0],
402 eigenvalue: 0.0,
403 });
404 }
405
406 let negative_weights = weights.is_some_and(|w| w.iter().any(|&x| x < 0.0));
407
408 let mut inc: Vec<Vec<u32>> = Vec::with_capacity(n_us);
411 for v in 0..n {
412 inc.push(graph.incident(v)?);
413 }
414
415 let mut x = vec![0.0_f64; n_us];
417 for v in 0..n {
418 let v_us = v as usize;
419 let mut strength = 0.0_f64;
420 for &e in &inc[v_us] {
421 let w = weights.map_or(1.0, |ws| ws[e as usize]);
422 strength += w;
423 }
424 if strength != 0.0 {
425 x[v_us] = strength.abs();
426 } else if !negative_weights {
427 x[v_us] = 0.0;
428 } else {
429 x[v_us] = if inc[v_us].is_empty() { 0.0 } else { 1.0 };
432 }
433 }
434 if x.iter().all(|&y| y == 0.0) {
436 x.fill(1.0);
437 }
438 let max0 = x.iter().fold(0.0_f64, |acc, &y| acc.max(y.abs()));
440 if max0 > 0.0 {
441 for slot in &mut x {
442 *slot /= max0;
443 }
444 }
445
446 let shift = 1.0_f64;
451 let mut x_new = vec![0.0_f64; n_us];
452
453 for _ in 0..SHIFTED_MAX_ITER {
454 for v in 0..n_us {
456 let mut sum = shift * x[v];
457 for &e in &inc[v] {
458 let other = graph.edge_other(e, v as u32)?;
459 let w = weights.map_or(1.0, |ws| ws[e as usize]);
460 sum += w * x[other as usize];
461 }
462 x_new[v] = sum;
463 }
464 let pivot = if negative_weights {
469 let mut best = 0.0_f64;
470 for &v in &x_new {
471 if v.abs() > best.abs() {
472 best = v;
473 }
474 }
475 best
476 } else {
477 x_new.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()))
478 };
479 if pivot != 0.0 {
480 for slot in &mut x_new {
481 *slot /= pivot;
482 }
483 }
484 let mut diff = 0.0_f64;
485 for v in 0..n_us {
486 diff += (x_new[v] - x[v]).abs();
487 }
488 std::mem::swap(&mut x, &mut x_new);
489 if diff < SHIFTED_EPS {
490 break;
491 }
492 }
493
494 let mut wx = vec![0.0_f64; n_us];
496 for v in 0..n_us {
497 let mut sum = 0.0_f64;
498 for &e in &inc[v] {
499 let other = graph.edge_other(e, v as u32)?;
500 let w = weights.map_or(1.0, |ws| ws[e as usize]);
501 sum += w * x[other as usize];
502 }
503 wx[v] = sum;
504 }
505 let num: f64 = x.iter().zip(wx.iter()).map(|(a, b)| a * b).sum();
506 let den: f64 = x.iter().map(|a| a * a).sum();
507 let eigenvalue = if den > 0.0 { num / den } else { 0.0 };
508
509 let max = x.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
512 if max > 0.0 {
513 for slot in &mut x {
514 *slot /= max;
515 }
516 }
517 if !negative_weights {
518 for slot in &mut x {
519 if *slot < 0.0 {
520 *slot = 0.0;
521 }
522 }
523 }
524
525 Ok(EigenvectorScores {
526 vector: x,
527 eigenvalue,
528 })
529}
530
531#[allow(
534 clippy::too_many_lines,
535 clippy::many_single_char_names,
536 clippy::cast_possible_truncation,
537 clippy::needless_range_loop
538)]
539fn directed_path(
540 graph: &Graph,
541 mode: EigenvectorMode,
542 weights: Option<&[f64]>,
543) -> IgraphResult<EigenvectorScores> {
544 let n = graph.vcount();
545 let n_us = n as usize;
546 let m = graph.ecount();
547 if n == 0 {
548 return Ok(EigenvectorScores {
549 vector: Vec::new(),
550 eigenvalue: 0.0,
551 });
552 }
553 if m == 0 {
554 return Ok(EigenvectorScores {
555 vector: vec![1.0; n_us],
556 eigenvalue: 0.0,
557 });
558 }
559 if let Some(w) = weights {
560 if w.iter().all(|&x| x == 0.0) {
561 return Ok(EigenvectorScores {
562 vector: vec![1.0; n_us],
563 eigenvalue: 0.0,
564 });
565 }
566 }
567
568 let negative_weights = weights.is_some_and(|w| w.iter().any(|&x| x < 0.0));
569
570 if !negative_weights && crate::algorithms::properties::is_dag::is_dag(graph) {
574 return dag_sentinel(graph, mode, weights);
575 }
576
577 let mut walk: Vec<Vec<(u32, u32)>> = vec![Vec::new(); n_us];
581 let m_u32 = u32::try_from(m)
582 .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32::MAX".into()))?;
583 match mode {
584 EigenvectorMode::Out => {
585 for e in 0..m_u32 {
586 let (u, v) = graph.edge(e)?;
587 walk[v as usize].push((e, u));
588 }
589 }
590 EigenvectorMode::In => {
591 for e in 0..m_u32 {
592 let (u, v) = graph.edge(e)?;
593 walk[u as usize].push((e, v));
594 }
595 }
596 EigenvectorMode::All => unreachable!("All is dispatched to undirected_*"),
597 }
598
599 let mut x = vec![0.0_f64; n_us];
602 for v in 0..n_us {
603 let mut strength = 0.0_f64;
604 for &(e, _) in &walk[v] {
605 let w = weights.map_or(1.0, |ws| ws[e as usize]);
606 strength += w;
607 }
608 if strength != 0.0 {
609 x[v] = strength.abs();
610 } else if !negative_weights {
611 x[v] = 0.0;
612 } else {
613 x[v] = if walk[v].is_empty() { 0.0 } else { 1.0 };
614 }
615 }
616 if x.iter().all(|&y| y == 0.0) {
617 x.fill(1.0);
618 }
619 let max0 = x.iter().fold(0.0_f64, |acc, &y| acc.max(y.abs()));
620 if max0 > 0.0 {
621 for slot in &mut x {
622 *slot /= max0;
623 }
624 }
625
626 let mut row_norm: f64 = 0.0;
632 for v in 0..n_us {
633 let mut s = 0.0_f64;
634 for &(e, _) in &walk[v] {
635 let w = weights.map_or(1.0, |ws| ws[e as usize]);
636 s += w.abs();
637 }
638 if s > row_norm {
639 row_norm = s;
640 }
641 }
642 let shift = row_norm.max(1.0) + 1.0;
643
644 let mut x_new = vec![0.0_f64; n_us];
645 for _ in 0..SHIFTED_MAX_ITER {
646 for v in 0..n_us {
647 let mut sum = shift * x[v];
648 for &(e, nei) in &walk[v] {
649 let w = weights.map_or(1.0, |ws| ws[e as usize]);
650 sum += w * x[nei as usize];
651 }
652 x_new[v] = sum;
653 }
654 let pivot = if negative_weights {
655 let mut best = 0.0_f64;
656 for &v in &x_new {
657 if v.abs() > best.abs() {
658 best = v;
659 }
660 }
661 best
662 } else {
663 x_new.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()))
664 };
665 if pivot != 0.0 {
666 for slot in &mut x_new {
667 *slot /= pivot;
668 }
669 }
670 let mut diff = 0.0_f64;
671 for v in 0..n_us {
672 diff += (x_new[v] - x[v]).abs();
673 }
674 std::mem::swap(&mut x, &mut x_new);
675 if diff < SHIFTED_EPS {
676 break;
677 }
678 }
679
680 let mut mx = vec![0.0_f64; n_us];
682 for v in 0..n_us {
683 let mut sum = 0.0_f64;
684 for &(e, nei) in &walk[v] {
685 let w = weights.map_or(1.0, |ws| ws[e as usize]);
686 sum += w * x[nei as usize];
687 }
688 mx[v] = sum;
689 }
690 let num: f64 = x.iter().zip(mx.iter()).map(|(a, b)| a * b).sum();
691 let den: f64 = x.iter().map(|a| a * a).sum();
692 let mut eigenvalue = if den > 0.0 { num / den } else { 0.0 };
693
694 if !negative_weights && eigenvalue <= SHIFTED_EPS {
698 eigenvalue = 0.0;
699 x.fill(0.0);
700 } else {
701 let max = x.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
702 if max > 0.0 {
703 for slot in &mut x {
704 *slot /= max;
705 }
706 }
707 if !negative_weights {
708 for slot in &mut x {
709 if *slot < 0.0 {
710 *slot = 0.0;
711 }
712 }
713 }
714 }
715
716 Ok(EigenvectorScores {
717 vector: x,
718 eigenvalue,
719 })
720}
721
722#[allow(clippy::many_single_char_names)]
724fn dag_sentinel(
725 graph: &Graph,
726 mode: EigenvectorMode,
727 weights: Option<&[f64]>,
728) -> IgraphResult<EigenvectorScores> {
729 let n = graph.vcount();
730 let n_us = n as usize;
731 let m = graph.ecount();
732 let mut strength = vec![0.0_f64; n_us];
737 let m_u32 = u32::try_from(m)
738 .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32::MAX".into()))?;
739 for e in 0..m_u32 {
740 let (u, v) = graph.edge(e)?;
741 let w = weights.map_or(1.0, |ws| ws[e as usize]);
742 match mode {
743 EigenvectorMode::Out => strength[u as usize] += w,
744 EigenvectorMode::In => strength[v as usize] += w,
745 EigenvectorMode::All => unreachable!(),
746 }
747 }
748 let vector: Vec<f64> = strength
749 .iter()
750 .map(|&s| if s == 0.0 { 1.0 } else { 0.0 })
751 .collect();
752 Ok(EigenvectorScores {
753 vector,
754 eigenvalue: 0.0,
755 })
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 fn close(actual: &[f64], expected: &[f64], tol: f64) {
763 assert_eq!(actual.len(), expected.len(), "length mismatch");
764 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
765 assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
766 }
767 }
768
769 #[test]
770 fn empty_graph_yields_empty() {
771 let g = Graph::with_vertices(0);
772 assert!(eigenvector_centrality(&g).unwrap().is_empty());
773 }
774
775 #[test]
776 fn singleton_yields_one() {
777 let g = Graph::with_vertices(1);
778 assert_eq!(eigenvector_centrality(&g).unwrap(), vec![1.0]);
779 }
780
781 #[test]
782 fn isolated_vertices_yield_uniform_one() {
783 let g = Graph::with_vertices(3);
784 let ec = eigenvector_centrality(&g).unwrap();
785 close(&ec, &[1.0, 1.0, 1.0], 1e-9);
786 }
787
788 #[test]
789 fn triangle_uniform_one() {
790 let mut g = Graph::with_vertices(3);
791 g.add_edge(0, 1).unwrap();
792 g.add_edge(1, 2).unwrap();
793 g.add_edge(2, 0).unwrap();
794 let ec = eigenvector_centrality(&g).unwrap();
795 close(&ec, &[1.0, 1.0, 1.0], 1e-9);
796 }
797
798 #[test]
799 fn star_4_centre_one_leaves_inverse_sqrt_three() {
800 let mut g = Graph::with_vertices(4);
801 for v in 1..4 {
802 g.add_edge(0, v).unwrap();
803 }
804 let ec = eigenvector_centrality(&g).unwrap();
805 let inv_sqrt3 = 1.0 / 3f64.sqrt();
806 close(&ec, &[1.0, inv_sqrt3, inv_sqrt3, inv_sqrt3], 1e-9);
807 }
808
809 #[test]
810 fn k4_uniform_one() {
811 let mut g = Graph::with_vertices(4);
812 for u in 0..4u32 {
813 for v in (u + 1)..4 {
814 g.add_edge(u, v).unwrap();
815 }
816 }
817 let ec = eigenvector_centrality(&g).unwrap();
818 close(&ec, &[1.0, 1.0, 1.0, 1.0], 1e-9);
819 }
820
821 #[test]
822 fn directed_graph_returns_unsupported() {
823 let mut g = Graph::new(3, true).unwrap();
824 g.add_edge(0, 1).unwrap();
825 assert!(eigenvector_centrality(&g).is_err());
826 }
827
828 #[test]
831 fn weighted_star_unit_matches_unweighted() {
832 let mut g = Graph::with_vertices(5);
833 for v in 1..5 {
834 g.add_edge(0, v).unwrap();
835 }
836 let s = eigenvector_centrality_weighted(&g, &vec![1.0; g.ecount()]).unwrap();
837 close(&s.vector, &[1.0, 0.5, 0.5, 0.5, 0.5], 1e-9);
838 assert!((s.eigenvalue - 2.0).abs() < 1e-6);
839 }
840
841 #[test]
842 fn weighted_k5_unit_eigenvalue_four() {
843 let mut g = Graph::with_vertices(5);
844 for u in 0..5u32 {
845 for v in (u + 1)..5 {
846 g.add_edge(u, v).unwrap();
847 }
848 }
849 let s = eigenvector_centrality_weighted(&g, &vec![1.0; g.ecount()]).unwrap();
850 close(&s.vector, &[1.0, 1.0, 1.0, 1.0, 1.0], 1e-9);
851 assert!((s.eigenvalue - 4.0).abs() < 1e-6);
852 }
853
854 #[test]
855 fn weighted_empty_returns_ones() {
856 let g = Graph::with_vertices(4);
857 let s = eigenvector_centrality_weighted(&g, &[]).unwrap();
858 close(&s.vector, &[1.0; 4], 1e-15);
859 assert!(s.eigenvalue.abs() < f64::EPSILON);
860 }
861
862 #[test]
863 fn weighted_all_zero_returns_ones() {
864 let mut g = Graph::with_vertices(3);
865 g.add_edge(0, 1).unwrap();
866 g.add_edge(1, 2).unwrap();
867 let s = eigenvector_centrality_weighted(&g, &[0.0, 0.0]).unwrap();
868 close(&s.vector, &[1.0; 3], 1e-15);
869 assert!(s.eigenvalue.abs() < f64::EPSILON);
870 }
871
872 #[test]
873 fn weighted_length_mismatch_errors() {
874 let mut g = Graph::with_vertices(3);
875 g.add_edge(0, 1).unwrap();
876 g.add_edge(1, 2).unwrap();
877 assert!(eigenvector_centrality_weighted(&g, &[1.0]).is_err());
878 }
879
880 #[test]
883 fn directed_full_eigenvalue_n_minus_one() {
884 let mut g = Graph::new(5, true).unwrap();
886 for u in 0..5u32 {
887 for v in 0..5u32 {
888 if u != v {
889 g.add_edge(u, v).unwrap();
890 }
891 }
892 }
893 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
894 close(&s.vector, &[1.0, 1.0, 1.0, 1.0, 1.0], 1e-9);
895 assert!((s.eigenvalue - 4.0).abs() < 1e-6);
896 }
897
898 #[test]
899 fn directed_dag_out_star_returns_leaves_one() {
900 let mut g = Graph::new(5, true).unwrap();
902 for v in 1..5u32 {
903 g.add_edge(0, v).unwrap();
904 }
905 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
906 close(&s.vector, &[0.0, 1.0, 1.0, 1.0, 1.0], 1e-12);
907 assert!(s.eigenvalue.abs() < f64::EPSILON);
908 }
909
910 #[test]
911 fn directed_dag_in_star_in_mode_marks_leaves() {
912 let mut g = Graph::new(5, true).unwrap();
918 for v in 1..5u32 {
919 g.add_edge(v, 0).unwrap();
920 }
921 let s = eigenvector_centrality_directed(&g, EigenvectorMode::In).unwrap();
922 close(&s.vector, &[0.0, 1.0, 1.0, 1.0, 1.0], 1e-12);
923 assert!(s.eigenvalue.abs() < f64::EPSILON);
924 }
925
926 #[test]
927 fn directed_dag_in_star_out_mode_marks_centre() {
928 let mut g = Graph::new(5, true).unwrap();
932 for v in 1..5u32 {
933 g.add_edge(v, 0).unwrap();
934 }
935 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
936 close(&s.vector, &[1.0, 0.0, 0.0, 0.0, 0.0], 1e-12);
937 assert!(s.eigenvalue.abs() < f64::EPSILON);
938 }
939
940 #[test]
941 fn directed_small_cycle_chord_eigenvalue_matches_arpack() {
942 let mut g = Graph::new(4, true).unwrap();
945 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3), (3, 0), (1, 3)])
946 .unwrap();
947 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
948 assert!(
949 (s.eigenvalue - 1.220_744).abs() < 1e-3,
950 "expected ≈ 1.22074, got {}",
951 s.eigenvalue
952 );
953 let max_idx = s
955 .vector
956 .iter()
957 .enumerate()
958 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
959 .unwrap()
960 .0;
961 assert_eq!(max_idx, 3);
962 }
963
964 #[test]
965 fn directed_empty_directed_returns_ones() {
966 let g = Graph::new(5, true).unwrap();
967 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
968 close(&s.vector, &[1.0; 5], 1e-12);
969 assert!(s.eigenvalue.abs() < f64::EPSILON);
970 }
971
972 #[test]
975 fn full_undirected_with_all_mode_works() {
976 let mut g = Graph::with_vertices(3);
977 g.add_edge(0, 1).unwrap();
978 g.add_edge(1, 2).unwrap();
979 g.add_edge(2, 0).unwrap();
980 let s = eigenvector_centrality_full(&g, EigenvectorMode::All, None).unwrap();
981 close(&s.vector, &[1.0, 1.0, 1.0], 1e-9);
982 }
983
984 #[test]
985 fn full_undirected_forces_all_mode() {
986 let mut g = Graph::with_vertices(3);
988 g.add_edge(0, 1).unwrap();
989 g.add_edge(1, 2).unwrap();
990 g.add_edge(2, 0).unwrap();
991 let a = eigenvector_centrality_full(&g, EigenvectorMode::Out, None).unwrap();
992 let b = eigenvector_centrality_full(&g, EigenvectorMode::All, None).unwrap();
993 close(&a.vector, &b.vector, 1e-12);
994 assert!((a.eigenvalue - b.eigenvalue).abs() < 1e-12);
995 }
996
997 #[test]
1000 fn directed_cycle_50_converges_to_unit_eigenvalue() {
1001 let n: u32 = 50;
1007 let mut g = Graph::new(n, true).unwrap();
1008 for i in 0..n {
1009 g.add_edge(i, (i + 1) % n).unwrap();
1010 }
1011 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
1012 assert!(
1013 (s.eigenvalue - 1.0).abs() < 1e-9,
1014 "expected λ ≈ 1.0, got {}",
1015 s.eigenvalue
1016 );
1017 let mx = s.vector.iter().copied().fold(0.0_f64, f64::max);
1019 let mn = s.vector.iter().copied().fold(f64::INFINITY, f64::min);
1020 assert!(mx - mn < 1e-9, "vec spread {} too large", mx - mn);
1021 }
1022
1023 #[test]
1024 fn directed_k10_eigenvalue_n_minus_one() {
1025 let n: u32 = 10;
1028 let mut g = Graph::new(n, true).unwrap();
1029 for u in 0..n {
1030 for v in 0..n {
1031 if u != v {
1032 g.add_edge(u, v).unwrap();
1033 }
1034 }
1035 }
1036 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
1037 assert!(
1038 (s.eigenvalue - 9.0).abs() < 1e-9,
1039 "expected λ = 9, got {}",
1040 s.eigenvalue
1041 );
1042 close(&s.vector, &[1.0; 10], 1e-9);
1043 }
1044
1045 #[test]
1046 fn directed_cycle_chord_eigenvector_components_match_arpack() {
1047 let mut g = Graph::new(4, true).unwrap();
1051 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3), (3, 0), (1, 3)])
1052 .unwrap();
1053 let s = eigenvector_centrality_directed(&g, EigenvectorMode::Out).unwrap();
1054 assert!((s.eigenvalue - 1.220_744).abs() < 1e-4);
1055 close(
1056 &s.vector,
1057 &[0.819_173, 0.671_044, 0.549_700, 1.000_000],
1058 1e-3,
1059 );
1060 }
1061}