#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
use crate::core::rng::SplitMix64;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
fn validate_inputs(n: u32, m: u32) -> IgraphResult<()> {
let n_u64 = u64::from(n);
let m_u64 = u64::from(m);
let steps = n_u64.saturating_sub(1);
let new_edges = steps
.checked_mul(m_u64)
.ok_or(IgraphError::Internal("barabasi_game edge count overflow"))?;
let _bag = n_u64
.checked_add(new_edges)
.and_then(|s| s.checked_add(new_edges))
.ok_or(IgraphError::Internal("barabasi_game bag size overflow"))?;
Ok(())
}
pub fn barabasi_game_bag(
n: u32,
m: u32,
outpref: bool,
directed: bool,
seed: u64,
) -> IgraphResult<Graph> {
validate_inputs(n, m)?;
let outpref = outpref || !directed;
if n == 0 {
return Graph::new(0, directed);
}
if n == 1 || m == 0 {
return Graph::new(n, directed);
}
let m_usize = m as usize;
let steps = (n as usize).saturating_sub(1);
let total_edges = steps
.checked_mul(m_usize)
.ok_or(IgraphError::Internal("barabasi_game edge total overflow"))?;
let bag_capacity = (n as usize)
.checked_add(total_edges)
.and_then(|s| s.checked_add(if outpref { total_edges } else { 0 }))
.ok_or(IgraphError::Internal("barabasi_game bag capacity overflow"))?;
let mut bag: Vec<VertexId> = Vec::with_capacity(bag_capacity);
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
bag.push(0);
let mut rng = SplitMix64::new(seed);
for i in 1..n {
let bag_len = bag.len();
debug_assert!(bag_len > 0);
let edges_start = edges.len();
for _ in 0..m {
let pick = rng.gen_index(bag_len);
let to = bag[pick];
edges.push((i, to));
}
bag.push(i);
for k in 0..m_usize {
let neighbour = edges[edges_start + k].1;
bag.push(neighbour);
if outpref {
bag.push(i);
}
}
}
let mut g = Graph::new(n, directed)?;
g.add_edges(edges)?;
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
(0..n_edges)
.map(|eid| g.edge(eid).expect("edge id in bounds"))
.collect()
}
#[test]
fn n_zero_returns_empty_graph() {
let g = barabasi_game_bag(0, 3, false, true, 1).unwrap();
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn n_one_returns_singleton() {
let g = barabasi_game_bag(1, 5, false, true, 1).unwrap();
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn m_zero_returns_edgeless() {
let g = barabasi_game_bag(10, 0, false, true, 1).unwrap();
assert_eq!(g.vcount(), 10);
assert_eq!(g.ecount(), 0);
}
#[test]
fn exact_edge_count_directed_constant_m() {
for &(n, m) in &[(10u32, 1u32), (10, 3), (50, 2), (200, 5)] {
let g = barabasi_game_bag(n, m, false, true, 0x00C0_FFEE).unwrap();
assert_eq!(g.vcount(), n);
assert_eq!(g.ecount(), (n as usize - 1) * m as usize);
}
}
#[test]
fn undirected_forces_outpref() {
let g = barabasi_game_bag(50, 3, false, false, 0x0BAD_C0DE).unwrap();
assert_eq!(g.vcount(), 50);
assert_eq!(g.ecount(), 49 * 3);
assert!(!g.is_directed());
}
#[test]
fn deterministic_with_seed() {
let a = barabasi_game_bag(100, 4, true, true, 0xDEAD).unwrap();
let b = barabasi_game_bag(100, 4, true, true, 0xDEAD).unwrap();
assert_eq!(a.vcount(), b.vcount());
assert_eq!(a.ecount(), b.ecount());
let ea: Vec<_> = collect_edges(&a);
let eb: Vec<_> = collect_edges(&b);
assert_eq!(ea, eb);
}
#[test]
fn different_seeds_yield_different_graphs() {
let a = barabasi_game_bag(100, 4, false, true, 1).unwrap();
let b = barabasi_game_bag(100, 4, false, true, 2).unwrap();
let ea: Vec<_> = collect_edges(&a);
let eb: Vec<_> = collect_edges(&b);
assert_ne!(ea, eb, "different seeds must produce different graphs");
}
#[test]
fn first_vertex_has_no_outgoing_in_directed_graph() {
let g = barabasi_game_bag(50, 3, false, true, 0x42).unwrap();
for (src, _dst) in collect_edges(&g) {
assert_ne!(src, 0, "seed vertex must never be the source");
}
}
#[test]
fn edges_only_point_to_earlier_vertices() {
let g = barabasi_game_bag(80, 2, false, true, 0xBEEF).unwrap();
for (src, dst) in collect_edges(&g) {
assert!(
dst < src,
"edge ({src} -> {dst}) violates BA temporal ordering"
);
}
}
#[test]
fn high_degree_concentration_around_early_vertices() {
let g = barabasi_game_bag(500, 3, false, true, 0xABCD).unwrap();
let mut deg = vec![0u32; g.vcount() as usize];
for (src, dst) in collect_edges(&g) {
deg[src as usize] += 1;
deg[dst as usize] += 1;
}
let mut indexed: Vec<(usize, u32)> = deg.iter().copied().enumerate().collect();
indexed.sort_by_key(|b| std::cmp::Reverse(b.1));
let top5: Vec<usize> = indexed.iter().take(5).map(|(v, _)| *v).collect();
assert!(
top5.contains(&0),
"expected vertex 0 in top-5, got {top5:?}"
);
}
#[test]
fn outpref_true_increases_self_degree_role() {
let g = barabasi_game_bag(40, 2, true, true, 7).unwrap();
assert_eq!(g.ecount(), 39 * 2);
}
#[test]
fn validate_huge_overflow_is_rejected() {
let huge = u32::MAX;
let res = barabasi_game_bag(huge, huge, true, true, 0);
let _ = res;
}
}