#![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 edges = steps
.checked_mul(m_u64)
.ok_or(IgraphError::Internal("growing_random edge count overflow"))?;
if edges > u64::from(u32::MAX) {
return Err(IgraphError::Internal(
"growing_random edge count exceeds u32::MAX",
));
}
Ok(())
}
pub fn growing_random_game(
n: u32,
m: u32,
directed: bool,
citation: bool,
seed: u64,
) -> IgraphResult<Graph> {
validate_inputs(n, m)?;
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) - 1;
let total_edges = steps
.checked_mul(m_usize)
.ok_or(IgraphError::Internal("growing_random edge total overflow"))?;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
let mut rng = SplitMix64::new(seed);
for i in 1..n {
for _ in 0..m {
let (from, to) = if citation {
let to = rng.gen_index(i as usize) as VertexId;
(i, to)
} else {
let from = rng.gen_index((i as usize) + 1) as VertexId;
let to = (rng.gen_index(i as usize) as VertexId) + 1;
(from, to)
};
edges.push((from, to));
}
}
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 = growing_random_game(0, 3, true, true, 1).unwrap();
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn n_one_returns_singleton() {
let g = growing_random_game(1, 5, true, true, 1).unwrap();
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn m_zero_returns_edgeless() {
let g = growing_random_game(20, 0, true, false, 1).unwrap();
assert_eq!(g.vcount(), 20);
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 = growing_random_game(n, m, true, true, 0x00C0_FFEE).unwrap();
assert_eq!(g.vcount(), n);
assert_eq!(g.ecount(), (n as usize - 1) * m as usize);
}
}
#[test]
fn exact_edge_count_undirected_free_mode() {
let g = growing_random_game(100, 4, false, false, 0xDEAD_BEEF).unwrap();
assert_eq!(g.vcount(), 100);
assert_eq!(g.ecount(), 99 * 4);
assert!(!g.is_directed());
}
#[test]
fn deterministic_with_seed() {
let a = growing_random_game(80, 3, true, true, 0xABCD).unwrap();
let b = growing_random_game(80, 3, true, true, 0xABCD).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 = growing_random_game(60, 2, true, false, 1).unwrap();
let b = growing_random_game(60, 2, true, false, 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 citation_mode_directed_has_strict_temporal_order() {
let g = growing_random_game(100, 3, true, true, 0xBEEF).unwrap();
for (src, dst) in collect_edges(&g) {
assert!(
dst < src,
"citation edge ({src} -> {dst}) violates temporal ordering"
);
}
}
#[test]
fn citation_mode_directed_no_self_loops() {
let g = growing_random_game(120, 4, true, true, 0xFACE).unwrap();
for (src, dst) in collect_edges(&g) {
assert_ne!(src, dst, "citation mode must not produce self-loops");
}
}
#[test]
fn citation_mode_first_vertex_never_source() {
let g = growing_random_game(75, 2, true, true, 0x42).unwrap();
for (src, _dst) in collect_edges(&g) {
assert_ne!(src, 0, "seed vertex must never be the source in citation");
}
}
#[test]
fn citation_mode_per_step_outdegree_equals_m() {
let n: u32 = 50;
let m: u32 = 3;
let g = growing_random_game(n, m, true, true, 0xC1A0).unwrap();
let mut per_src = vec![0u32; n as usize];
for (src, _dst) in collect_edges(&g) {
per_src[src as usize] += 1;
}
assert_eq!(per_src[0], 0);
for (i, &out) in per_src.iter().enumerate().skip(1) {
assert_eq!(out, m, "vertex {i} out-degree should be m={m}");
}
}
#[test]
fn free_mode_directed_endpoints_within_frontier() {
let g = growing_random_game(100, 3, true, false, 0xBABE).unwrap();
for (src, dst) in collect_edges(&g) {
assert!(
dst >= 1,
"free-mode sink should never be 0 (got edge ({src}, {dst}))"
);
}
}
#[test]
fn validate_huge_overflow_is_rejected_or_handled() {
let huge = u32::MAX;
let res = growing_random_game(huge, huge, true, true, 0);
let _ = res;
}
}