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