1use super::betweenness::betweenness;
25use super::closeness::closeness;
26use super::degree::DegreeMode;
27use super::eigenvector::{EigenvectorMode, eigenvector_centrality_full};
28use super::strength::{StrengthMode, strength_with_mode};
29use crate::core::{Graph, IgraphResult};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LoopMode {
34 NoLoops,
36 LoopsOnce,
38 LoopsTwice,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CentralizationMode {
45 In,
46 Out,
47 All,
48}
49
50#[allow(clippy::cast_precision_loss)]
68pub fn centralization(scores: &[f64], theoretical_max: f64, normalized: bool) -> f64 {
69 if scores.is_empty() {
70 return f64::NAN;
71 }
72
73 let max_score = scores.iter().copied().fold(f64::NEG_INFINITY, f64::max);
74 let n = scores.len() as f64;
75 let sum: f64 = scores.iter().sum();
76 let cent = n * max_score - sum;
77
78 if normalized {
79 cent / theoretical_max
80 } else {
81 cent
82 }
83}
84
85#[allow(clippy::cast_precision_loss)]
99pub fn centralization_degree_tmax(
100 n: u32,
101 directed: bool,
102 mode: CentralizationMode,
103 loops: LoopMode,
104) -> f64 {
105 if n == 0 {
106 return f64::NAN;
107 }
108
109 let nf = f64::from(n);
110
111 if directed {
112 match mode {
113 CentralizationMode::In | CentralizationMode::Out => {
114 if matches!(loops, LoopMode::NoLoops) {
115 (nf - 1.0) * (nf - 1.0)
116 } else {
117 (nf - 1.0) * nf
118 }
119 }
120 CentralizationMode::All => {
121 if matches!(loops, LoopMode::NoLoops) {
122 2.0 * (nf - 1.0) * (nf - 2.0)
123 } else {
124 2.0 * (nf - 1.0) * (nf - 1.0)
125 }
126 }
127 }
128 } else {
129 match loops {
130 LoopMode::NoLoops => (nf - 1.0) * (nf - 2.0),
131 LoopMode::LoopsOnce => (nf - 1.0) * (nf - 1.0),
132 LoopMode::LoopsTwice => (nf - 1.0) * nf,
133 }
134 }
135}
136
137pub fn centralization_betweenness_tmax(n: u32, directed: bool) -> f64 {
151 if n == 0 {
152 return f64::NAN;
153 }
154
155 let nf = f64::from(n);
156
157 if directed {
158 (nf - 1.0) * (nf - 1.0) * (nf - 2.0)
159 } else {
160 (nf - 1.0) * (nf - 1.0) * (nf - 2.0) / 2.0
161 }
162}
163
164pub fn centralization_closeness_tmax(n: u32, mode: CentralizationMode) -> f64 {
181 if n == 0 {
182 return f64::NAN;
183 }
184
185 let nf = f64::from(n);
186
187 match mode {
188 CentralizationMode::In | CentralizationMode::Out => (nf - 1.0) * (1.0 - 1.0 / nf),
189 CentralizationMode::All => (nf - 1.0) * (nf - 2.0) / (2.0 * nf - 3.0),
190 }
191}
192
193pub fn centralization_eigenvector_tmax(n: u32, mode: CentralizationMode) -> f64 {
210 if n == 0 {
211 return f64::NAN;
212 }
213 if n == 1 {
214 return 0.0;
215 }
216
217 let nf = f64::from(n);
218
219 match mode {
220 CentralizationMode::In | CentralizationMode::Out => nf - 1.0,
221 CentralizationMode::All => nf - 2.0,
222 }
223}
224
225#[derive(Debug, Clone, PartialEq)]
227pub struct CentralizationResult {
228 pub scores: Vec<f64>,
230 pub centralization: f64,
232 pub theoretical_max: f64,
234}
235
236fn degree_to_strength_mode(mode: DegreeMode) -> StrengthMode {
237 match mode {
238 DegreeMode::Out => StrengthMode::Out,
239 DegreeMode::In => StrengthMode::In,
240 DegreeMode::All => StrengthMode::All,
241 }
242}
243
244fn degree_to_cent_mode(mode: DegreeMode) -> CentralizationMode {
245 match mode {
246 DegreeMode::Out => CentralizationMode::Out,
247 DegreeMode::In => CentralizationMode::In,
248 DegreeMode::All => CentralizationMode::All,
249 }
250}
251
252pub fn centralization_degree_wrapper(
273 graph: &Graph,
274 mode: DegreeMode,
275 loops: bool,
276 normalized: bool,
277) -> IgraphResult<CentralizationResult> {
278 let n = graph.vcount();
279 let ecount = graph.ecount();
280
281 let scores = if ecount == 0 {
282 vec![0.0_f64; n as usize]
283 } else {
284 let unit_w = vec![1.0_f64; ecount];
285 strength_with_mode(graph, &unit_w, degree_to_strength_mode(mode), loops)?
286 };
287
288 let loop_mode = if loops {
289 LoopMode::LoopsTwice
290 } else {
291 LoopMode::NoLoops
292 };
293 let tmax =
294 centralization_degree_tmax(n, graph.is_directed(), degree_to_cent_mode(mode), loop_mode);
295 let cent = centralization(&scores, tmax, normalized);
296
297 Ok(CentralizationResult {
298 scores,
299 centralization: cent,
300 theoretical_max: tmax,
301 })
302}
303
304pub fn centralization_betweenness_wrapper(
323 graph: &Graph,
324 normalized: bool,
325) -> IgraphResult<CentralizationResult> {
326 let scores = betweenness(graph)?;
327 let directed = graph.is_directed();
328 let tmax = centralization_betweenness_tmax(graph.vcount(), directed);
329 let cent = centralization(&scores, tmax, normalized);
330
331 Ok(CentralizationResult {
332 scores,
333 centralization: cent,
334 theoretical_max: tmax,
335 })
336}
337
338pub fn centralization_closeness_wrapper(
359 graph: &Graph,
360 normalized: bool,
361) -> IgraphResult<CentralizationResult> {
362 let raw = closeness(graph)?;
363 let scores: Vec<f64> = raw.into_iter().map(|v| v.unwrap_or(f64::NAN)).collect();
364
365 let cent_mode = if graph.is_directed() {
366 CentralizationMode::Out
367 } else {
368 CentralizationMode::All
369 };
370 let tmax = centralization_closeness_tmax(graph.vcount(), cent_mode);
371 let cent = centralization(&scores, tmax, normalized);
372
373 Ok(CentralizationResult {
374 scores,
375 centralization: cent,
376 theoretical_max: tmax,
377 })
378}
379
380pub fn centralization_eigenvector_wrapper(
401 graph: &Graph,
402 normalized: bool,
403) -> IgraphResult<CentralizationResult> {
404 let eig_mode = if graph.is_directed() {
405 EigenvectorMode::Out
406 } else {
407 EigenvectorMode::All
408 };
409 let eig = eigenvector_centrality_full(graph, eig_mode, None)?;
410 let scores = eig.vector;
411
412 let cent_mode = if graph.is_directed() {
413 CentralizationMode::Out
414 } else {
415 CentralizationMode::All
416 };
417 let tmax = centralization_eigenvector_tmax(graph.vcount(), cent_mode);
418 let cent = centralization(&scores, tmax, normalized);
419
420 Ok(CentralizationResult {
421 scores,
422 centralization: cent,
423 theoretical_max: tmax,
424 })
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 fn approx_eq(a: f64, b: f64) -> bool {
432 (a - b).abs() < 1e-9
433 }
434
435 #[test]
436 fn centralization_empty() {
437 let c = centralization(&[], 1.0, false);
438 assert!(c.is_nan());
439 }
440
441 #[test]
442 fn centralization_single() {
443 let c = centralization(&[5.0], 1.0, false);
444 assert!(approx_eq(c, 0.0));
445 }
446
447 #[test]
448 fn centralization_star_degree() {
449 let scores = [4.0, 1.0, 1.0, 1.0, 1.0];
450 let c = centralization(&scores, 12.0, false);
451 assert!(approx_eq(c, 12.0));
452
453 let c_norm = centralization(&scores, 12.0, true);
454 assert!(approx_eq(c_norm, 1.0));
455 }
456
457 #[test]
458 fn centralization_uniform() {
459 let scores = [3.0, 3.0, 3.0, 3.0];
460 let c = centralization(&scores, 6.0, false);
461 assert!(approx_eq(c, 0.0));
462 }
463
464 #[test]
465 fn centralization_normalized() {
466 let scores = [4.0, 2.0, 1.0];
467 let unnorm = centralization(&scores, 1.0, false);
468 let norm = centralization(&scores, 6.0, true);
469 assert!(approx_eq(unnorm, 5.0));
470 assert!(approx_eq(norm, 5.0 / 6.0));
471 }
472
473 #[test]
474 fn degree_tmax_zero() {
475 assert!(
476 centralization_degree_tmax(0, false, CentralizationMode::All, LoopMode::NoLoops)
477 .is_nan()
478 );
479 }
480
481 #[test]
482 fn degree_tmax_undirected_no_loops() {
483 assert!(approx_eq(
484 centralization_degree_tmax(5, false, CentralizationMode::All, LoopMode::NoLoops),
485 12.0
486 ));
487 assert!(approx_eq(
488 centralization_degree_tmax(10, false, CentralizationMode::All, LoopMode::NoLoops),
489 72.0
490 ));
491 }
492
493 #[test]
494 fn degree_tmax_undirected_loops_once() {
495 assert!(approx_eq(
496 centralization_degree_tmax(5, false, CentralizationMode::All, LoopMode::LoopsOnce),
497 16.0
498 ));
499 }
500
501 #[test]
502 fn degree_tmax_undirected_loops_twice() {
503 assert!(approx_eq(
504 centralization_degree_tmax(5, false, CentralizationMode::All, LoopMode::LoopsTwice),
505 20.0
506 ));
507 }
508
509 #[test]
510 fn degree_tmax_directed_in_no_loops() {
511 assert!(approx_eq(
512 centralization_degree_tmax(5, true, CentralizationMode::In, LoopMode::NoLoops),
513 16.0
514 ));
515 }
516
517 #[test]
518 fn degree_tmax_directed_all_no_loops() {
519 assert!(approx_eq(
520 centralization_degree_tmax(5, true, CentralizationMode::All, LoopMode::NoLoops),
521 24.0
522 ));
523 }
524
525 #[test]
526 fn degree_tmax_directed_in_with_loops() {
527 assert!(approx_eq(
528 centralization_degree_tmax(5, true, CentralizationMode::In, LoopMode::LoopsTwice),
529 20.0
530 ));
531 }
532
533 #[test]
534 fn degree_tmax_directed_all_with_loops() {
535 assert!(approx_eq(
536 centralization_degree_tmax(5, true, CentralizationMode::All, LoopMode::LoopsTwice),
537 32.0
538 ));
539 }
540
541 #[test]
542 fn betweenness_tmax_zero() {
543 assert!(centralization_betweenness_tmax(0, false).is_nan());
544 }
545
546 #[test]
547 fn betweenness_tmax_undirected() {
548 assert!(approx_eq(centralization_betweenness_tmax(5, false), 24.0));
549 assert!(approx_eq(centralization_betweenness_tmax(3, false), 2.0));
550 }
551
552 #[test]
553 fn betweenness_tmax_directed() {
554 assert!(approx_eq(centralization_betweenness_tmax(5, true), 48.0));
555 assert!(approx_eq(centralization_betweenness_tmax(3, true), 4.0));
556 }
557
558 #[test]
559 fn closeness_tmax_zero() {
560 assert!(centralization_closeness_tmax(0, CentralizationMode::All).is_nan());
561 }
562
563 #[test]
564 fn closeness_tmax_undirected() {
565 let tmax = centralization_closeness_tmax(5, CentralizationMode::All);
566 assert!(approx_eq(tmax, 12.0 / 7.0));
567 }
568
569 #[test]
570 fn closeness_tmax_directed() {
571 let tmax = centralization_closeness_tmax(5, CentralizationMode::In);
572 assert!(approx_eq(tmax, 4.0 * (1.0 - 1.0 / 5.0)));
573 }
574
575 #[test]
576 fn eigenvector_tmax_zero() {
577 assert!(centralization_eigenvector_tmax(0, CentralizationMode::All).is_nan());
578 }
579
580 #[test]
581 fn eigenvector_tmax_one() {
582 assert!(approx_eq(
583 centralization_eigenvector_tmax(1, CentralizationMode::All),
584 0.0
585 ));
586 }
587
588 #[test]
589 fn eigenvector_tmax_undirected() {
590 assert!(approx_eq(
591 centralization_eigenvector_tmax(5, CentralizationMode::All),
592 3.0
593 ));
594 }
595
596 #[test]
597 fn eigenvector_tmax_directed() {
598 assert!(approx_eq(
599 centralization_eigenvector_tmax(5, CentralizationMode::In),
600 4.0
601 ));
602 }
603
604 #[test]
605 fn star5_degree_centralization() {
606 let scores = [4.0, 1.0, 1.0, 1.0, 1.0];
607 let tmax = centralization_degree_tmax(5, false, CentralizationMode::All, LoopMode::NoLoops);
608 let c = centralization(&scores, tmax, true);
609 assert!(approx_eq(c, 1.0), "star degree centralization = {c}");
610 }
611
612 #[test]
613 fn ring_degree_centralization() {
614 let scores = [2.0, 2.0, 2.0, 2.0, 2.0];
615 let tmax = centralization_degree_tmax(5, false, CentralizationMode::All, LoopMode::NoLoops);
616 let c = centralization(&scores, tmax, true);
617 assert!(approx_eq(c, 0.0), "ring degree centralization = {c}");
618 }
619
620 #[test]
621 fn star_betweenness_centralization() {
622 let n = 5u32;
623 let scores = [6.0, 0.0, 0.0, 0.0, 0.0];
624 let tmax = centralization_betweenness_tmax(n, false);
625 let c = centralization(&scores, tmax, true);
626 assert!(approx_eq(c, 1.0), "star betweenness centralization = {c}");
627 }
628
629 fn make_star(n: u32) -> Graph {
632 let mut g = Graph::with_vertices(n);
633 for v in 1..n {
634 g.add_edge(0, v).unwrap();
635 }
636 g
637 }
638
639 fn make_ring(n: u32) -> Graph {
640 let mut g = Graph::with_vertices(n);
641 for v in 0..n {
642 g.add_edge(v, (v + 1) % n).unwrap();
643 }
644 g
645 }
646
647 fn make_path(n: u32) -> Graph {
648 let mut g = Graph::with_vertices(n);
649 for v in 0..n - 1 {
650 g.add_edge(v, v + 1).unwrap();
651 }
652 g
653 }
654
655 #[test]
658 fn wrapper_degree_star5() {
659 let g = make_star(5);
660 let r = centralization_degree_wrapper(&g, DegreeMode::All, false, true).unwrap();
661 assert!(approx_eq(r.centralization, 1.0), "got {}", r.centralization);
662 assert!(approx_eq(r.scores[0], 4.0));
663 for i in 1..5 {
664 assert!(approx_eq(r.scores[i], 1.0));
665 }
666 }
667
668 #[test]
669 fn wrapper_degree_ring5() {
670 let g = make_ring(5);
671 let r = centralization_degree_wrapper(&g, DegreeMode::All, false, true).unwrap();
672 assert!(approx_eq(r.centralization, 0.0), "got {}", r.centralization);
673 }
674
675 #[test]
676 fn wrapper_degree_empty() {
677 let g = Graph::with_vertices(0);
678 let r = centralization_degree_wrapper(&g, DegreeMode::All, false, true).unwrap();
679 assert!(r.scores.is_empty());
680 assert!(r.centralization.is_nan());
681 }
682
683 #[test]
684 fn wrapper_degree_single_vertex() {
685 let g = Graph::with_vertices(1);
686 let r = centralization_degree_wrapper(&g, DegreeMode::All, false, false).unwrap();
687 assert_eq!(r.scores.len(), 1);
688 assert!(approx_eq(r.scores[0], 0.0));
689 assert!(approx_eq(r.centralization, 0.0));
690 }
691
692 #[test]
693 fn wrapper_degree_unnormalized() {
694 let g = make_star(5);
695 let r = centralization_degree_wrapper(&g, DegreeMode::All, false, false).unwrap();
696 assert!(approx_eq(r.centralization, 12.0));
697 assert!(approx_eq(r.theoretical_max, 12.0));
698 }
699
700 #[test]
701 fn wrapper_degree_with_self_loops() {
702 let mut g = Graph::with_vertices(3);
703 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
705 g.add_edge(1, 2).unwrap();
706 let r_loops = centralization_degree_wrapper(&g, DegreeMode::All, true, false).unwrap();
707 let r_no_loops = centralization_degree_wrapper(&g, DegreeMode::All, false, false).unwrap();
708 assert!(r_loops.scores[0] > r_no_loops.scores[0]);
710 }
711
712 #[test]
715 fn wrapper_betweenness_star5() {
716 let g = make_star(5);
717 let r = centralization_betweenness_wrapper(&g, true).unwrap();
718 assert!(approx_eq(r.centralization, 1.0), "got {}", r.centralization);
719 }
720
721 #[test]
722 fn wrapper_betweenness_path5() {
723 let g = make_path(5);
724 let r = centralization_betweenness_wrapper(&g, false).unwrap();
725 assert!(r.centralization > 0.0);
726 assert!(r.scores[2] > r.scores[0]);
728 assert!(r.scores[2] > r.scores[4]);
729 }
730
731 #[test]
732 fn wrapper_betweenness_ring() {
733 let g = make_ring(5);
734 let r = centralization_betweenness_wrapper(&g, true).unwrap();
735 assert!(approx_eq(r.centralization, 0.0), "got {}", r.centralization);
736 }
737
738 #[test]
739 fn wrapper_betweenness_empty() {
740 let g = Graph::with_vertices(0);
741 let r = centralization_betweenness_wrapper(&g, true).unwrap();
742 assert!(r.scores.is_empty());
743 }
744
745 #[test]
748 fn wrapper_closeness_star5() {
749 let g = make_star(5);
750 let r = centralization_closeness_wrapper(&g, true).unwrap();
751 assert!(approx_eq(r.centralization, 1.0), "got {}", r.centralization);
752 assert!(approx_eq(r.scores[0], 1.0));
754 }
755
756 #[test]
757 fn wrapper_closeness_ring() {
758 let g = make_ring(5);
759 let r = centralization_closeness_wrapper(&g, true).unwrap();
760 assert!(approx_eq(r.centralization, 0.0), "got {}", r.centralization);
761 }
762
763 #[test]
764 fn wrapper_closeness_disconnected_is_nan() {
765 let g = Graph::with_vertices(2);
767 let r = centralization_closeness_wrapper(&g, true).unwrap();
768 assert!(r.scores[0].is_nan());
769 assert!(r.scores[1].is_nan());
770 assert!(r.centralization.is_nan());
771 }
772
773 #[test]
776 fn wrapper_eigenvector_star5() {
777 let g = make_star(5);
778 let r = centralization_eigenvector_wrapper(&g, true).unwrap();
779 assert!(r.centralization > 0.0);
780 assert!(approx_eq(r.scores[0], 1.0));
782 for i in 1..5 {
784 assert!(approx_eq(r.scores[i], r.scores[1]));
785 }
786 }
787
788 #[test]
789 fn wrapper_eigenvector_ring() {
790 let g = make_ring(5);
791 let r = centralization_eigenvector_wrapper(&g, true).unwrap();
792 assert!(approx_eq(r.centralization, 0.0), "got {}", r.centralization);
794 }
795
796 #[test]
797 fn wrapper_eigenvector_empty() {
798 let g = Graph::with_vertices(0);
799 let r = centralization_eigenvector_wrapper(&g, true).unwrap();
800 assert!(r.scores.is_empty());
801 }
802}
803
804#[cfg(all(test, feature = "proptest-harness"))]
805mod proptests {
806 use super::*;
807 use proptest::prelude::*;
808
809 proptest! {
810 #[test]
811 fn centralization_non_negative(
812 scores in proptest::collection::vec(0.0f64..100.0, 1..20)
813 ) {
814 let c = centralization(&scores, 1.0, false);
815 prop_assert!(c >= 0.0, "centralization should be >= 0, got {c}");
816 }
817
818 #[test]
819 fn centralization_uniform_is_zero(val in 0.0f64..100.0, n in 1usize..20) {
820 let scores = vec![val; n];
821 let c = centralization(&scores, 1.0, false);
822 prop_assert!(
823 c.abs() < 1e-9,
824 "uniform scores should give centralization 0, got {c}"
825 );
826 }
827
828 #[test]
829 fn tmax_non_negative(n in 3u32..100) {
830 let deg = centralization_degree_tmax(n, false, CentralizationMode::All, LoopMode::NoLoops);
831 let bet = centralization_betweenness_tmax(n, false);
832 let clo = centralization_closeness_tmax(n, CentralizationMode::All);
833 let eig = centralization_eigenvector_tmax(n, CentralizationMode::All);
834 prop_assert!(deg > 0.0, "degree tmax should be > 0 for n={n}");
835 prop_assert!(bet > 0.0, "betweenness tmax should be > 0 for n={n}");
836 prop_assert!(clo > 0.0, "closeness tmax should be > 0 for n={n}");
837 prop_assert!(eig > 0.0, "eigenvector tmax should be > 0 for n={n}");
838 }
839 }
840}