1use crate::core::graph::EdgeId;
60use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
61
62pub fn rich_club_sequence(
109 graph: &Graph,
110 weights: Option<&[f64]>,
111 vertex_order: &[VertexId],
112 normalized: bool,
113 loops: bool,
114 directed: bool,
115) -> IgraphResult<Vec<f64>> {
116 let vcount = graph.vcount();
117 let ecount = graph.ecount();
118 let vcount_usize = vcount as usize;
119
120 if vertex_order.len() != vcount_usize {
121 return Err(IgraphError::InvalidArgument(format!(
122 "rich_club_sequence: vertex_order length ({}) does not match vcount ({})",
123 vertex_order.len(),
124 vcount
125 )));
126 }
127 if let Some(w) = weights {
128 if w.len() != ecount {
129 return Err(IgraphError::InvalidArgument(format!(
130 "rich_club_sequence: weights length ({}) does not match ecount ({})",
131 w.len(),
132 ecount
133 )));
134 }
135 }
136
137 let directed_eff = directed && graph.is_directed();
139
140 let mut order_of: Vec<u32> = vec![u32::MAX; vcount_usize];
144 for (pos, &v) in vertex_order.iter().enumerate() {
145 if v >= vcount {
146 return Err(IgraphError::InvalidArgument(format!(
147 "rich_club_sequence: vertex_order entry {v} is out of range [0, {vcount})"
148 )));
149 }
150 let slot = &mut order_of[v as usize];
151 if *slot != u32::MAX {
152 return Err(IgraphError::InvalidArgument(format!(
153 "rich_club_sequence: vertex_order is not a permutation; vertex {v} appears more than once"
154 )));
155 }
156 *slot = u32::try_from(pos).map_err(|_| {
159 IgraphError::InvalidArgument(
160 "rich_club_sequence: index exceeds u32::MAX (internal invariant)".to_string(),
161 )
162 })?;
163 }
164
165 let mut res = vec![0.0f64; vcount_usize];
166
167 let ecount_u32 = u32::try_from(ecount).map_err(|_| {
170 IgraphError::InvalidArgument(
171 "rich_club_sequence: ecount exceeds u32::MAX (internal invariant)".to_string(),
172 )
173 })?;
174 for eid in 0..ecount_u32 {
175 let (v1, v2) = graph.edge(eid as EdgeId)?;
176 let o1 = order_of[v1 as usize];
177 let o2 = order_of[v2 as usize];
178 let bucket = o1.min(o2) as usize;
179 let contribution = match weights {
180 Some(w) => w[eid as usize],
181 None => 1.0,
182 };
183 res[bucket] += contribution;
184 }
185
186 let mut total = 0.0f64;
190 for slot in res.iter_mut().rev() {
191 total += *slot;
192 *slot = total;
193 }
194
195 if normalized {
196 for (i, slot) in res.iter_mut().enumerate() {
200 let i_u32 = u32::try_from(i).map_err(|_| {
201 IgraphError::InvalidArgument(
202 "rich_club_sequence: index exceeds u32::MAX (internal invariant)".to_string(),
203 )
204 })?;
205 let remaining = vcount - i_u32;
206 *slot /= total_possible_edges(remaining, directed_eff, loops);
207 }
208 }
209
210 Ok(res)
211}
212
213fn total_possible_edges(n: u32, directed: bool, loops: bool) -> f64 {
224 let nv = f64::from(n);
227 if loops {
228 if directed {
229 nv * nv
230 } else {
231 nv * (nv + 1.0) / 2.0
232 }
233 } else if directed {
234 nv * (nv - 1.0)
235 } else {
236 nv * (nv - 1.0) / 2.0
237 }
238}
239
240#[cfg(test)]
241#[allow(
242 clippy::float_cmp,
246 clippy::cast_precision_loss,
248)]
249mod tests {
250 use super::*;
251
252 fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
256 assert_eq!(
257 actual.len(),
258 expected.len(),
259 "lengths differ: actual={actual:?} expected={expected:?}"
260 );
261 for (i, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
262 if e.is_nan() {
263 assert!(a.is_nan(), "index {i}: expected NaN, got {a}");
264 } else {
265 assert!(
266 (a - e).abs() <= tol,
267 "index {i}: |{a} - {e}| = {} > {tol}",
268 (a - e).abs()
269 );
270 }
271 }
272 }
273
274 fn upstream_undirected_no_loop() -> Graph {
277 let mut g = Graph::with_vertices(7);
278 let edges = [
280 (0, 3),
281 (1, 3),
282 (2, 3),
283 (4, 3),
284 (5, 3),
285 (5, 6),
286 (1, 2),
287 (2, 5),
288 ];
289 for (u, v) in edges {
290 g.add_edge(u, v).expect("add_edge");
291 }
292 g
293 }
294
295 fn upstream_directed_no_loop() -> Graph {
298 let mut g = Graph::new(7, true).expect("Graph::new directed");
299 let edges = [
300 (0, 2),
301 (1, 2),
302 (2, 3),
303 (1, 3),
304 (3, 5),
305 (3, 4),
306 (5, 6),
307 (6, 5),
308 ];
309 for (u, v) in edges {
310 g.add_edge(u, v).expect("add_edge");
311 }
312 g
313 }
314
315 fn upstream_undirected_loop() -> Graph {
317 let mut g = Graph::with_vertices(7);
318 let edges = [
319 (0, 3),
320 (1, 3),
321 (2, 3),
322 (4, 4),
323 (5, 3),
324 (5, 6),
325 (1, 2),
326 (2, 5),
327 (6, 4),
328 ];
329 for (u, v) in edges {
330 g.add_edge(u, v).expect("add_edge");
331 }
332 g
333 }
334
335 fn upstream_directed_loop() -> Graph {
337 let mut g = Graph::new(7, true).expect("Graph::new directed");
338 let edges = [
339 (0, 2),
340 (1, 2),
341 (2, 3),
342 (1, 3),
343 (3, 5),
344 (3, 4),
345 (5, 6),
346 (6, 5),
347 (4, 4),
348 ];
349 for (u, v) in edges {
350 g.add_edge(u, v).expect("add_edge");
351 }
352 g
353 }
354
355 #[test]
356 fn test1_null_graph_matches_upstream() {
357 let g = Graph::with_vertices(0);
358 let r = rich_club_sequence(&g, None, &[], true, false, false).expect("ok");
359 assert!(r.is_empty());
360 }
361
362 #[test]
363 fn test2_singleton_matches_upstream() {
364 let g = Graph::with_vertices(1);
365 let r = rich_club_sequence(&g, None, &[0], true, false, false).expect("ok");
366 assert_eq!(r.len(), 1);
369 assert!(r[0].is_nan());
370 }
371
372 #[test]
373 fn test3a_undirected_no_loop_in_order_matches_upstream() {
374 let g = upstream_undirected_no_loop();
375 let r =
376 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, false, false).expect("ok");
377 let expected = [
380 8.0 / 21.0, 7.0 / 15.0, 0.5,
383 0.5,
384 1.0 / 3.0, 1.0,
386 f64::NAN,
387 ];
388 assert_close(&r, &expected, 1e-9);
389 }
390
391 #[test]
392 fn test3b_undirected_no_loop_reverse_matches_upstream() {
393 let g = upstream_undirected_no_loop();
394 let r =
395 rich_club_sequence(&g, None, &[6, 5, 4, 3, 2, 1, 0], true, false, false).expect("ok");
396 let expected = [
398 8.0 / 21.0,
399 7.0 / 15.0,
400 0.5,
401 2.0 / 3.0,
402 1.0 / 3.0,
403 0.0,
404 f64::NAN,
405 ];
406 assert_close(&r, &expected, 1e-9);
407 }
408
409 #[test]
410 fn test4a_directed_no_loop_in_order_matches_upstream() {
411 let g = upstream_directed_no_loop();
412 let r =
413 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, false, true).expect("ok");
414 let expected = [
416 8.0 / 42.0, 7.0 / 30.0, 0.25,
419 1.0 / 3.0,
420 1.0 / 3.0,
421 1.0,
422 f64::NAN,
423 ];
424 assert_close(&r, &expected, 1e-9);
425 }
426
427 #[test]
428 fn test4b_directed_no_loop_reverse_matches_upstream() {
429 let g = upstream_directed_no_loop();
430 let r =
431 rich_club_sequence(&g, None, &[6, 5, 4, 3, 2, 1, 0], true, false, true).expect("ok");
432 let expected = [
434 8.0 / 42.0,
435 6.0 / 30.0, 3.0 / 12.0, 1.0 / 3.0,
438 1.0 / 3.0,
439 0.0,
440 f64::NAN,
441 ];
442 assert_close(&r, &expected, 1e-9);
443 }
444
445 #[test]
446 fn test5a_undirected_loop_in_order_matches_upstream() {
447 let g = upstream_undirected_loop();
448 let r =
449 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, true, false).expect("ok");
450 let expected = [
453 9.0 / 28.0,
454 8.0 / 21.0,
455 6.0 / 15.0,
456 4.0 / 10.0,
457 3.0 / 6.0,
458 1.0 / 3.0,
459 0.0 / 1.0,
460 ];
461 assert_close(&r, &expected, 1e-9);
462 }
463
464 #[test]
465 fn test5b_undirected_loop_reverse_matches_upstream() {
466 let g = upstream_undirected_loop();
467 let r =
468 rich_club_sequence(&g, None, &[6, 5, 4, 3, 2, 1, 0], true, true, false).expect("ok");
469 let expected = [
471 9.0 / 28.0,
472 7.0 / 21.0,
473 5.0 / 15.0,
474 4.0 / 10.0,
475 1.0 / 6.0,
476 0.0,
477 0.0,
478 ];
479 assert_close(&r, &expected, 1e-9);
480 }
481
482 #[test]
483 fn test6a_directed_loop_in_order_matches_upstream() {
484 let g = upstream_directed_loop();
485 let r = rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, true, true).expect("ok");
486 let expected = [
489 9.0 / 49.0,
490 8.0 / 36.0,
491 6.0 / 25.0,
492 5.0 / 16.0,
493 3.0 / 9.0,
494 2.0 / 4.0,
495 0.0,
496 ];
497 assert_close(&r, &expected, 1e-9);
498 }
499
500 #[test]
501 fn test6b_directed_loop_reverse_matches_upstream() {
502 let g = upstream_directed_loop();
503 let r = rich_club_sequence(&g, None, &[6, 5, 4, 3, 2, 1, 0], true, true, true).expect("ok");
504 let expected = [
506 9.0 / 49.0,
507 7.0 / 36.0,
508 6.0 / 25.0,
509 4.0 / 16.0,
510 2.0 / 9.0,
511 0.0,
512 0.0,
513 ];
514 assert_close(&r, &expected, 1e-9);
515 }
516
517 #[test]
518 fn test7a_weighted_doubles_test3a() {
519 let g = upstream_undirected_no_loop();
520 let weights = vec![2.0; g.ecount()];
521 let r = rich_club_sequence(
522 &g,
523 Some(&weights),
524 &[0, 1, 2, 3, 4, 5, 6],
525 true,
526 false,
527 false,
528 )
529 .expect("ok");
530 let expected = [16.0 / 21.0, 14.0 / 15.0, 1.0, 1.0, 2.0 / 3.0, 2.0, f64::NAN];
532 assert_close(&r, &expected, 1e-9);
533 }
534
535 #[test]
536 fn test7b_weighted_non_integer_matches_test3a() {
537 let g = upstream_undirected_no_loop();
538 let weights = [1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.5, 1.5];
542 let r = rich_club_sequence(
543 &g,
544 Some(&weights),
545 &[0, 1, 2, 3, 4, 5, 6],
546 true,
547 false,
548 false,
549 )
550 .expect("ok");
551 let expected = [8.0 / 21.0, 7.0 / 15.0, 0.5, 0.5, 1.0 / 3.0, 1.0, f64::NAN];
552 assert_close(&r, &expected, 1e-9);
553 }
554
555 #[test]
556 fn raw_unnormalized_returns_edge_counts() {
557 let g = upstream_undirected_no_loop();
558 let r =
559 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], false, false, false).expect("ok");
560 assert_eq!(r, vec![8.0, 7.0, 5.0, 3.0, 1.0, 1.0, 0.0]);
567 }
568
569 #[test]
570 fn raw_unnormalized_undirected_no_loop_reverse() {
571 let g = upstream_undirected_no_loop();
572 let r =
573 rich_club_sequence(&g, None, &[6, 5, 4, 3, 2, 1, 0], false, false, false).expect("ok");
574 assert_eq!(r[0], 8.0);
577 assert_eq!(r[6], 0.0);
578 for w in r.windows(2) {
580 assert!(w[0] >= w[1], "non-monotone: {r:?}");
581 }
582 }
583
584 #[test]
585 fn error_vertex_order_wrong_length() {
586 let g = upstream_undirected_no_loop();
587 let r = rich_club_sequence(&g, None, &[0], true, false, false);
588 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
589 }
590
591 #[test]
592 fn error_weights_wrong_length() {
593 let g = upstream_undirected_no_loop();
594 let w = vec![1.0];
595 let r = rich_club_sequence(&g, Some(&w), &[0, 1, 2, 3, 4, 5, 6], true, false, false);
596 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
597 }
598
599 #[test]
600 fn error_vertex_order_out_of_range() {
601 let g = upstream_undirected_no_loop();
602 let r = rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 99], true, false, false);
603 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
604 }
605
606 #[test]
607 fn error_vertex_order_duplicate() {
608 let g = upstream_undirected_no_loop();
609 let r = rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 0], true, false, false);
611 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
612 }
613
614 #[test]
615 fn directed_flag_silently_false_on_undirected_graph() {
616 let g = upstream_undirected_no_loop();
617 let r_true =
620 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, false, true).expect("ok");
621 let r_false =
622 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, false, false).expect("ok");
623 assert_close(&r_true, &r_false, 0.0);
624 }
625
626 #[test]
627 fn first_entry_equals_total_when_unnormalized() {
628 let g = upstream_undirected_no_loop();
630 let r =
631 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], false, false, false).expect("ok");
632 assert_eq!(r[0], g.ecount() as f64);
633
634 let g = upstream_directed_no_loop();
635 let r =
636 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], false, false, true).expect("ok");
637 assert_eq!(r[0], g.ecount() as f64);
638 }
639
640 #[test]
641 fn loops_normalize_avoids_nan_when_single_vertex_remains() {
642 let g = upstream_undirected_no_loop();
645 let r =
646 rich_club_sequence(&g, None, &[0, 1, 2, 3, 4, 5, 6], true, true, false).expect("ok");
647 assert_eq!(r[6], 0.0);
648 assert!(r.iter().all(|x| x.is_finite()));
649 }
650
651 #[test]
652 fn total_possible_edges_table() {
653 assert_eq!(super::total_possible_edges(4, false, false), 6.0); assert_eq!(super::total_possible_edges(4, true, false), 12.0); assert_eq!(super::total_possible_edges(4, false, true), 10.0); assert_eq!(super::total_possible_edges(4, true, true), 16.0); for &(d, l) in &[(false, false), (true, false), (false, true), (true, true)] {
660 assert_eq!(super::total_possible_edges(0, d, l), 0.0);
661 }
662 }
663}
664
665#[cfg(all(test, feature = "proptest-harness"))]
666mod proptests {
667 use super::*;
668 use proptest::prelude::*;
669
670 fn random_graph_strategy(directed: bool) -> impl Strategy<Value = Graph> {
673 (1usize..=20).prop_flat_map(move |n| {
674 let edge_count = 0usize..=(2 * n);
675 edge_count
676 .prop_flat_map(move |m| {
677 let n_u32 = n as u32;
678 (Just(n_u32), prop::collection::vec((0..n_u32, 0..n_u32), m))
679 })
680 .prop_map(move |(n_u32, edges)| {
681 let mut g = Graph::new(n_u32, directed).expect("Graph::new");
682 for (u, v) in edges {
683 g.add_edge(u, v).expect("add_edge");
684 }
685 g
686 })
687 })
688 }
689
690 fn permutation_strategy(n: u32) -> impl Strategy<Value = Vec<u32>> {
693 Just((0..n).collect::<Vec<_>>()).prop_shuffle()
694 }
695
696 proptest! {
697 #[test]
698 fn unnormalized_first_entry_equals_ecount(g in random_graph_strategy(false)) {
699 let n = g.vcount();
700 if n == 0 {
701 let r = rich_club_sequence(&g, None, &[], false, false, false).expect("ok");
702 prop_assert!(r.is_empty());
703 return Ok(());
704 }
705 let order: Vec<u32> = (0..n).collect();
706 let r = rich_club_sequence(&g, None, &order, false, false, false).expect("ok");
707 prop_assert_eq!(r[0], g.ecount() as f64);
708 }
709
710 #[test]
711 fn unnormalized_last_entry_equals_self_loops_on_last_vertex(
712 g in random_graph_strategy(false),
713 ) {
714 let n = g.vcount();
719 if n == 0 {
720 return Ok(());
721 }
722 let order: Vec<u32> = (0..n).collect();
723 let r = rich_club_sequence(&g, None, &order, false, false, false).expect("ok");
724 let last_vertex = order[(n - 1) as usize];
725 let mut self_loops_on_last = 0u32;
726 for eid in 0..g.ecount() {
727 let (u, v) = g.edge(eid as u32).expect("edge");
728 if u == last_vertex && v == last_vertex {
729 self_loops_on_last += 1;
730 }
731 }
732 prop_assert_eq!(r[(n - 1) as usize], f64::from(self_loops_on_last));
733 }
734
735 #[test]
736 fn unnormalized_monotone_non_increasing(g in random_graph_strategy(true)) {
737 let n = g.vcount();
738 if n == 0 {
739 return Ok(());
740 }
741 let order: Vec<u32> = (0..n).collect();
742 let r = rich_club_sequence(&g, None, &order, false, false, true).expect("ok");
743 for w in r.windows(2) {
744 prop_assert!(w[0] >= w[1], "non-monotone at window {w:?}");
745 }
746 }
747
748 #[test]
749 fn permutation_invariant_first_entry(
750 g in random_graph_strategy(false),
751 ) {
752 let n = g.vcount();
753 if n == 0 {
754 return Ok(());
755 }
756 let perm_strat = permutation_strategy(n);
760 proptest!(|(order in perm_strat)| {
761 let r = rich_club_sequence(&g, None, &order, false, false, false).expect("ok");
762 prop_assert_eq!(r[0], g.ecount() as f64);
763 });
764 }
765
766 #[test]
767 fn weighted_scale_invariance(g in random_graph_strategy(false), c in 0.1f64..10.0) {
768 let n = g.vcount();
769 if n == 0 {
770 return Ok(());
771 }
772 let order: Vec<u32> = (0..n).collect();
773 let r_unit = rich_club_sequence(&g, None, &order, false, false, false).expect("ok");
774 let w: Vec<f64> = vec![c; g.ecount()];
775 let r_scaled =
776 rich_club_sequence(&g, Some(&w), &order, false, false, false).expect("ok");
777 for (a, b) in r_unit.iter().zip(r_scaled.iter()) {
778 prop_assert!((a * c - b).abs() <= 1e-9 * (1.0 + b.abs()));
779 }
780 }
781
782 #[test]
783 fn normalize_in_unit_interval_for_loopless_undirected(
784 g in random_graph_strategy(false),
785 ) {
786 let n = g.vcount();
787 if n == 0 {
788 return Ok(());
789 }
790 let mut bad = false;
799 let mut seen: std::collections::HashSet<(u32, u32)> =
800 std::collections::HashSet::new();
801 for eid in 0..g.ecount() {
802 let (u, v) = g.edge(eid as u32).expect("edge");
803 if u == v {
804 bad = true;
805 break;
806 }
807 let key = if u <= v { (u, v) } else { (v, u) };
808 if !seen.insert(key) {
809 bad = true;
810 break;
811 }
812 }
813 if bad {
814 return Ok(());
815 }
816 let order: Vec<u32> = (0..n).collect();
817 let r = rich_club_sequence(&g, None, &order, true, false, false).expect("ok");
818 for (i, &x) in r.iter().enumerate() {
819 if i + 1 == n as usize {
820 prop_assert!(x.is_nan());
822 } else {
823 prop_assert!((0.0..=1.0).contains(&x), "out of [0,1]: r[{i}] = {x}");
824 }
825 }
826 }
827
828 #[test]
829 fn error_on_wrong_vertex_order_length(g in random_graph_strategy(false)) {
830 let n = g.vcount();
831 let bad = if n == 0 { vec![0u32] } else { vec![] };
833 let r = rich_club_sequence(&g, None, &bad, false, false, false);
834 prop_assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
835 }
836 }
837}