1#![allow(
84 clippy::cast_possible_truncation,
85 clippy::cast_sign_loss,
86 clippy::too_many_arguments,
87 clippy::needless_range_loop
88)]
89
90use crate::core::rng::SplitMix64;
91use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
92
93struct PsumTree {
99 n: usize,
100 bit: Vec<f64>,
101 values: Vec<f64>,
102 total: f64,
103}
104
105impl PsumTree {
106 fn new(n: usize) -> Self {
107 Self {
108 n,
109 bit: vec![0.0; n + 1],
110 values: vec![0.0; n],
111 total: 0.0,
112 }
113 }
114
115 fn set(&mut self, i: usize, v: f64) {
116 let delta = v - self.values[i];
117 self.values[i] = v;
118 self.total += delta;
119 let mut k = i + 1;
120 while k <= self.n {
121 self.bit[k] += delta;
122 k += k & k.wrapping_neg();
123 }
124 }
125
126 fn total(&self) -> f64 {
127 self.total
128 }
129
130 fn search_bounded(&self, target: f64, bound: usize) -> usize {
139 if bound == 0 {
140 return 0;
141 }
142 let mut idx: usize = 0;
143 let mut remaining = target;
144 let mut step = 1usize;
145 while step.saturating_mul(2) <= bound {
146 step *= 2;
147 }
148 while step > 0 {
149 let next = idx + step;
150 if next <= bound && self.bit[next] <= remaining {
151 idx = next;
152 remaining -= self.bit[next];
153 }
154 step >>= 1;
155 }
156 idx.min(bound - 1)
157 }
158}
159
160fn attraction(degree: u32, power: f64, a: f64) -> f64 {
163 let term = if power == 0.0 {
164 1.0
165 } else if degree == 0 {
166 if power > 0.0 { 0.0 } else { f64::INFINITY }
171 } else {
172 f64::from(degree).powf(power)
173 };
174 term + a
175}
176
177fn validate(
178 nodes: u32,
179 power: f64,
180 m: u32,
181 outseq: Option<&[u32]>,
182 outpref: bool,
183 a: f64,
184) -> IgraphResult<()> {
185 if !power.is_finite() {
186 return Err(IgraphError::InvalidArgument(format!(
187 "power must be finite (got {power})"
188 )));
189 }
190 if !a.is_finite() {
191 return Err(IgraphError::InvalidArgument(format!(
192 "A must be finite (got {a})"
193 )));
194 }
195 if outpref {
196 if a < 0.0 {
197 return Err(IgraphError::InvalidArgument(format!(
198 "A must be non-negative when outpref is true (got {a})"
199 )));
200 }
201 } else if a <= 0.0 {
202 return Err(IgraphError::InvalidArgument(format!(
203 "A must be positive when outpref is false (got {a})"
204 )));
205 }
206 if let Some(seq) = outseq {
207 if seq.len() != nodes as usize {
208 return Err(IgraphError::InvalidArgument(format!(
209 "outseq length ({}) must equal nodes ({nodes})",
210 seq.len()
211 )));
212 }
213 }
214 let _ = m;
215 Ok(())
216}
217
218fn edge_capacity(nodes: u32, m: u32, outseq: Option<&[u32]>) -> usize {
219 let n = nodes as usize;
220 match outseq {
221 Some(seq) => seq.iter().skip(1).map(|&x| x as usize).sum(),
222 None => n.saturating_sub(1).saturating_mul(m as usize),
223 }
224}
225
226pub fn barabasi_game_psumtree(
252 nodes: u32,
253 power: f64,
254 m: u32,
255 outseq: Option<&[u32]>,
256 outpref: bool,
257 a: f64,
258 directed: bool,
259 seed: u64,
260) -> IgraphResult<Graph> {
261 let outpref = outpref || !directed;
262 validate(nodes, power, m, outseq, outpref, a)?;
263 barabasi_psumtree_inner(nodes, power, m, outseq, outpref, a, directed, seed, false)
264}
265
266pub fn barabasi_game_psumtree_multiple(
290 nodes: u32,
291 power: f64,
292 m: u32,
293 outseq: Option<&[u32]>,
294 outpref: bool,
295 a: f64,
296 directed: bool,
297 seed: u64,
298) -> IgraphResult<Graph> {
299 let outpref = outpref || !directed;
300 validate(nodes, power, m, outseq, outpref, a)?;
301 barabasi_psumtree_inner(nodes, power, m, outseq, outpref, a, directed, seed, true)
302}
303
304#[allow(clippy::too_many_arguments)]
305fn barabasi_psumtree_inner(
306 nodes: u32,
307 power: f64,
308 m: u32,
309 outseq: Option<&[u32]>,
310 outpref: bool,
311 a: f64,
312 directed: bool,
313 seed: u64,
314 allow_multiple: bool,
315) -> IgraphResult<Graph> {
316 let mut graph = Graph::new(nodes, directed)?;
317 if nodes < 2 {
318 return Ok(graph);
319 }
320 if outseq.is_none() && m == 0 {
321 return Ok(graph);
322 }
323 if let Some(seq) = outseq {
324 let after_zero: u64 = seq.iter().skip(1).map(|&x| u64::from(x)).sum();
325 if after_zero == 0 {
326 return Ok(graph);
327 }
328 }
329
330 let n = nodes as usize;
331 let mut rng = SplitMix64::new(seed);
332 let mut psum = PsumTree::new(n);
333 let mut degree: Vec<u32> = vec![0; n];
334 let capacity = edge_capacity(nodes, m, outseq);
335 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(capacity);
336
337 psum.set(0, 1.0);
341
342 let mut picks: Vec<usize> = Vec::with_capacity(m as usize);
345
346 for i in 1..n {
347 let no_of_neighbors = if let Some(seq) = outseq {
348 seq[i] as usize
349 } else {
350 m as usize
351 };
352
353 picks.clear();
354
355 if allow_multiple && no_of_neighbors >= i {
356 for to in 0..i {
360 degree[to] += 1;
361 edges.push((
362 u32::try_from(i)
363 .map_err(|_| IgraphError::Internal("vertex index overflows u32"))?,
364 u32::try_from(to)
365 .map_err(|_| IgraphError::Internal("vertex index overflows u32"))?,
366 ));
367 psum.set(to, attraction(degree[to], power, a));
368 }
369 } else if allow_multiple {
370 let sum_snapshot = psum.total();
373 for _ in 0..no_of_neighbors {
374 let to = if sum_snapshot > 0.0 {
375 let target = rng.gen_unit() * sum_snapshot;
376 psum.search_bounded(target, i)
380 } else {
381 let span = i as u64;
382 (rng.next_u64() % span) as usize
383 };
384 degree[to] += 1;
385 edges.push((
386 u32::try_from(i)
387 .map_err(|_| IgraphError::Internal("vertex index overflows u32"))?,
388 u32::try_from(to)
389 .map_err(|_| IgraphError::Internal("vertex index overflows u32"))?,
390 ));
391 picks.push(to);
392 }
393 for &nn in &picks {
396 psum.set(nn, attraction(degree[nn], power, a));
397 }
398 } else {
399 for _ in 0..no_of_neighbors {
408 let sum = psum.total();
409 let to = if sum > 0.0 {
410 let target = rng.gen_unit() * sum;
411 psum.search_bounded(target, i)
417 } else {
418 let span = i as u64;
423 (rng.next_u64() % span) as usize
424 };
425 degree[to] += 1;
426 edges.push((
427 u32::try_from(i)
428 .map_err(|_| IgraphError::Internal("vertex index overflows u32"))?,
429 u32::try_from(to)
430 .map_err(|_| IgraphError::Internal("vertex index overflows u32"))?,
431 ));
432 psum.set(to, 0.0);
433 picks.push(to);
434 }
435 for &nn in &picks {
437 psum.set(nn, attraction(degree[nn], power, a));
438 }
439 }
440
441 if outpref {
443 let inc = if allow_multiple && no_of_neighbors >= i {
449 i as u32
450 } else {
451 no_of_neighbors as u32
452 };
453 degree[i] += inc;
454 psum.set(i, attraction(degree[i], power, a));
455 } else {
456 psum.set(i, attraction(0, power, a));
457 }
458 }
459
460 graph.add_edges(edges)?;
461 Ok(graph)
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use std::collections::HashMap;
468
469 fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
470 let n = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
471 (0..n)
472 .map(|eid| g.edge(eid).expect("edge id in bounds"))
473 .collect()
474 }
475
476 #[test]
477 fn psumtree_n_zero_returns_empty() {
478 let g = barabasi_game_psumtree(0, 1.0, 3, None, false, 1.0, true, 1).unwrap();
479 assert_eq!(g.vcount(), 0);
480 assert_eq!(g.ecount(), 0);
481 }
482
483 #[test]
484 fn psumtree_multiple_n_zero_returns_empty() {
485 let g = barabasi_game_psumtree_multiple(0, 1.0, 3, None, false, 1.0, true, 1).unwrap();
486 assert_eq!(g.vcount(), 0);
487 assert_eq!(g.ecount(), 0);
488 }
489
490 #[test]
491 fn psumtree_n_one_singleton() {
492 let g = barabasi_game_psumtree(1, 1.0, 5, None, false, 1.0, true, 1).unwrap();
493 assert_eq!(g.vcount(), 1);
494 assert_eq!(g.ecount(), 0);
495 }
496
497 #[test]
498 fn psumtree_m_zero_edgeless() {
499 let g = barabasi_game_psumtree(10, 1.0, 0, None, false, 1.0, true, 1).unwrap();
500 assert_eq!(g.vcount(), 10);
501 assert_eq!(g.ecount(), 0);
502 }
503
504 #[test]
505 fn psumtree_exact_edge_count() {
506 for &(n, m) in &[(10u32, 1u32), (50, 2), (100, 5)] {
507 let g = barabasi_game_psumtree(n, 1.0, m, None, false, 1.0, true, 0xABCD).unwrap();
508 assert_eq!(g.vcount(), n);
509 assert_eq!(g.ecount(), ((n - 1) * m) as usize);
510 }
511 }
512
513 #[test]
514 fn psumtree_multiple_exact_edge_count() {
515 for &(n, m) in &[(10u32, 1u32), (50, 2), (100, 5)] {
519 let g =
520 barabasi_game_psumtree_multiple(n, 1.0, m, None, false, 1.0, true, 0xABCD).unwrap();
521 assert_eq!(g.vcount(), n);
522 let expected = (n - 1) * m - m * (m - 1) / 2;
523 assert_eq!(g.ecount(), expected as usize);
524 }
525 }
526
527 #[test]
528 fn psumtree_simple_no_within_step_duplicates_after_warmup() {
529 let m: u32 = 4;
534 let g = barabasi_game_psumtree(100, 1.0, m, None, false, 1.0, true, 0xBEEF).unwrap();
535 let edges = collect_edges(&g);
536 let mut by_src: HashMap<VertexId, Vec<VertexId>> = HashMap::new();
537 for &(s, d) in &edges {
538 by_src.entry(s).or_default().push(d);
539 }
540 for (&src, dsts) in &by_src {
541 if src > m {
542 let mut sorted = dsts.clone();
543 sorted.sort_unstable();
544 sorted.dedup();
545 assert_eq!(sorted.len(), dsts.len(), "duplicate at src {src}: {dsts:?}");
546 }
547 }
548 }
549
550 #[test]
551 fn psumtree_simple_no_self_loops() {
552 let g = barabasi_game_psumtree(200, 1.0, 3, None, true, 1.0, true, 0xC001).unwrap();
553 for (s, d) in collect_edges(&g) {
554 assert_ne!(s, d, "self-loop ({s} -> {d}) emitted");
555 }
556 }
557
558 #[test]
559 fn psumtree_multiple_no_self_loops() {
560 let g =
561 barabasi_game_psumtree_multiple(200, 1.0, 3, None, true, 1.0, true, 0xC001).unwrap();
562 for (s, d) in collect_edges(&g) {
563 assert_ne!(s, d, "self-loop ({s} -> {d}) emitted");
564 }
565 }
566
567 #[test]
568 fn psumtree_edges_point_to_earlier_vertices() {
569 let g = barabasi_game_psumtree(50, 1.0, 2, None, false, 1.0, true, 0xBEEF).unwrap();
570 for (s, d) in collect_edges(&g) {
571 assert!(d < s, "edge ({s} -> {d}) violates BA temporal ordering");
572 }
573 }
574
575 #[test]
576 fn psumtree_multiple_edges_point_to_earlier_vertices() {
577 let g =
578 barabasi_game_psumtree_multiple(50, 1.0, 3, None, false, 1.0, true, 0xBEEF).unwrap();
579 for (s, d) in collect_edges(&g) {
580 assert!(d < s, "edge ({s} -> {d}) violates BA temporal ordering");
581 }
582 }
583
584 #[test]
585 fn psumtree_deterministic_with_seed() {
586 let a = barabasi_game_psumtree(80, 1.5, 3, None, false, 1.0, true, 0xDEAD).unwrap();
587 let b = barabasi_game_psumtree(80, 1.5, 3, None, false, 1.0, true, 0xDEAD).unwrap();
588 assert_eq!(collect_edges(&a), collect_edges(&b));
589 }
590
591 #[test]
592 fn psumtree_multiple_deterministic_with_seed() {
593 let a =
594 barabasi_game_psumtree_multiple(80, 1.5, 3, None, false, 1.0, true, 0xDEAD).unwrap();
595 let b =
596 barabasi_game_psumtree_multiple(80, 1.5, 3, None, false, 1.0, true, 0xDEAD).unwrap();
597 assert_eq!(collect_edges(&a), collect_edges(&b));
598 }
599
600 #[test]
601 fn psumtree_different_seeds_differ() {
602 let a = barabasi_game_psumtree(100, 1.0, 2, None, false, 1.0, true, 1).unwrap();
603 let b = barabasi_game_psumtree(100, 1.0, 2, None, false, 1.0, true, 2).unwrap();
604 assert_ne!(collect_edges(&a), collect_edges(&b));
605 }
606
607 #[test]
608 fn psumtree_undirected_forces_outpref() {
609 let g = barabasi_game_psumtree(50, 1.0, 2, None, false, 1.0, false, 0x42).unwrap();
610 assert_eq!(g.vcount(), 50);
611 assert!(!g.is_directed());
612 assert_eq!(g.ecount(), 49 * 2);
615 }
616
617 #[test]
618 fn psumtree_undirected_a_zero_outpref_ok() {
619 let g = barabasi_game_psumtree(30, 1.0, 1, None, false, 0.0, false, 0xC0).unwrap();
623 assert_eq!(g.ecount(), 29);
624 }
625
626 #[test]
627 fn psumtree_rejects_a_nonpositive_when_outpref_false() {
628 let err = barabasi_game_psumtree(10, 1.0, 1, None, false, 0.0, true, 1).unwrap_err();
629 match err {
630 IgraphError::InvalidArgument(s) => {
631 assert!(s.contains("A must be positive when outpref is false"));
632 }
633 other => panic!("expected InvalidArgument, got {other:?}"),
634 }
635 }
636
637 #[test]
638 fn psumtree_rejects_a_negative_when_outpref_true() {
639 let err = barabasi_game_psumtree(10, 1.0, 1, None, true, -0.5, true, 1).unwrap_err();
640 match err {
641 IgraphError::InvalidArgument(s) => {
642 assert!(s.contains("A must be non-negative when outpref is true"));
643 }
644 other => panic!("expected InvalidArgument, got {other:?}"),
645 }
646 }
647
648 #[test]
649 fn psumtree_rejects_power_nan() {
650 let err = barabasi_game_psumtree(10, f64::NAN, 1, None, false, 1.0, true, 1).unwrap_err();
651 match err {
652 IgraphError::InvalidArgument(s) => assert!(s.contains("power must be finite")),
653 other => panic!("expected InvalidArgument, got {other:?}"),
654 }
655 }
656
657 #[test]
658 fn psumtree_rejects_a_inf() {
659 let err =
660 barabasi_game_psumtree(10, 1.0, 1, None, true, f64::INFINITY, true, 1).unwrap_err();
661 match err {
662 IgraphError::InvalidArgument(s) => assert!(s.contains("A must be finite")),
663 other => panic!("expected InvalidArgument, got {other:?}"),
664 }
665 }
666
667 #[test]
668 fn psumtree_rejects_outseq_length_mismatch() {
669 let seq = vec![0u32, 1, 2];
670 let err = barabasi_game_psumtree(10, 1.0, 1, Some(&seq), false, 1.0, true, 1).unwrap_err();
671 match err {
672 IgraphError::InvalidArgument(s) => assert!(s.contains("outseq length")),
673 other => panic!("expected InvalidArgument, got {other:?}"),
674 }
675 }
676
677 #[test]
678 fn psumtree_outseq_overrides_m() {
679 let seq = vec![0u32, 1, 1, 2, 3];
680 let g = barabasi_game_psumtree(5, 1.0, 99, Some(&seq), false, 1.0, true, 0).unwrap();
681 assert_eq!(g.vcount(), 5);
682 assert_eq!(g.ecount(), 7);
684 }
685
686 #[test]
687 fn psumtree_multiple_outseq_overrides_m() {
688 let seq = vec![0u32, 1, 1, 2, 3];
689 let g =
690 barabasi_game_psumtree_multiple(5, 1.0, 99, Some(&seq), false, 1.0, true, 0).unwrap();
691 assert_eq!(g.vcount(), 5);
692 assert_eq!(g.ecount(), 7);
693 }
694
695 #[test]
696 fn psumtree_first_vertex_never_source() {
697 let g = barabasi_game_psumtree(50, 1.0, 2, None, false, 1.0, true, 0x42).unwrap();
698 for (s, _) in collect_edges(&g) {
699 assert_ne!(s, 0);
700 }
701 }
702
703 #[test]
704 fn psumtree_concentration_around_early_vertices() {
705 let g = barabasi_game_psumtree(500, 1.0, 3, None, false, 1.0, true, 0xABCD).unwrap();
706 let mut deg = vec![0u32; g.vcount() as usize];
707 for (s, d) in collect_edges(&g) {
708 deg[s as usize] += 1;
709 deg[d as usize] += 1;
710 }
711 let mut indexed: Vec<(usize, u32)> = deg.iter().copied().enumerate().collect();
712 indexed.sort_by_key(|&(_, d)| std::cmp::Reverse(d));
713 let top5: Vec<usize> = indexed.iter().take(5).map(|(v, _)| *v).collect();
714 assert!(
715 top5.contains(&0),
716 "expected vertex 0 in top-5, got {top5:?}"
717 );
718 }
719
720 #[test]
721 fn psumtree_high_power_super_concentration() {
722 let g = barabasi_game_psumtree(300, 2.0, 1, None, false, 1.0, true, 0xF00D).unwrap();
725 let mut deg = vec![0u32; g.vcount() as usize];
726 for (s, d) in collect_edges(&g) {
727 deg[s as usize] += 1;
728 deg[d as usize] += 1;
729 }
730 let max_deg = *deg.iter().max().unwrap();
731 let total: u32 = deg.iter().sum();
732 let max_v = deg.iter().position(|&d| d == max_deg).unwrap();
733 assert!(
736 max_deg >= total / 10,
737 "max_deg {max_deg} too small vs total {total}"
738 );
739 assert!(max_v < 30, "top vertex {max_v} should be among earliest");
740 }
741}
742
743#[cfg(all(test, feature = "proptest-harness"))]
744mod proptests {
745 use super::*;
746 use proptest::prelude::*;
747
748 proptest! {
749 #[test]
750 fn psumtree_no_self_loops(
751 n in 2u32..120,
752 m in 1u32..6,
753 power in 0.0f64..3.0,
754 seed: u64,
755 outpref: bool,
756 ) {
757 let g = barabasi_game_psumtree(n, power, m, None, outpref, 1.0, true, seed)
761 .expect("valid params");
762 let m_e = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
763 for eid in 0..m_e {
764 let (s, d) = g.edge(eid).expect("edge in bounds");
765 prop_assert_ne!(s, d);
766 }
767 }
768
769 #[test]
770 fn psumtree_multiple_no_self_loops(
771 n in 2u32..120,
772 m in 1u32..6,
773 power in 0.0f64..3.0,
774 seed: u64,
775 outpref: bool,
776 ) {
777 let g = barabasi_game_psumtree_multiple(n, power, m, None, outpref, 1.0, true, seed)
778 .expect("valid params");
779 let m_e = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
780 for eid in 0..m_e {
781 let (s, d) = g.edge(eid).expect("edge in bounds");
782 prop_assert_ne!(s, d);
783 }
784 }
785
786 #[test]
787 fn psumtree_exact_edge_count_prop(
788 n in 2u32..200,
789 m in 1u32..5,
790 seed: u64,
791 ) {
792 let g = barabasi_game_psumtree(n, 1.0, m, None, false, 1.0, true, seed)
793 .expect("valid params");
794 prop_assert_eq!(g.ecount() as u64, u64::from((n - 1) * m));
795 }
796
797 #[test]
798 fn psumtree_multiple_exact_edge_count_prop(
799 n in 2u32..200,
800 m in 1u32..5,
801 seed: u64,
802 ) {
803 let g = barabasi_game_psumtree_multiple(n, 1.0, m, None, false, 1.0, true, seed)
806 .expect("valid params");
807 let expected: u64 = if n > m {
808 u64::from((n - 1) * m) - u64::from(m * (m - 1) / 2)
809 } else {
810 u64::from((n - 1) * n / 2)
813 };
814 prop_assert_eq!(g.ecount() as u64, expected);
815 }
816
817 #[test]
818 fn psumtree_simple_no_within_step_dups_after_warmup(
819 n in 10u32..80,
820 m in 2u32..5,
821 seed: u64,
822 ) {
823 let g = barabasi_game_psumtree(n, 1.0, m, None, false, 1.0, true, seed)
825 .expect("valid params");
826 let m_e = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
827 let mut by_src: std::collections::HashMap<VertexId, Vec<VertexId>> = std::collections::HashMap::new();
828 for eid in 0..m_e {
829 let (s, d) = g.edge(eid).expect("edge in bounds");
830 by_src.entry(s).or_default().push(d);
831 }
832 for (&src, dsts) in &by_src {
833 if src > m {
834 let mut sorted = dsts.clone();
835 sorted.sort_unstable();
836 let total = sorted.len();
837 sorted.dedup();
838 prop_assert_eq!(sorted.len(), total,
839 "duplicate dsts at src {}: {:?}", src, dsts);
840 }
841 }
842 }
843 }
844}