#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
use crate::core::rng::SplitMix64;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
const IEA_ECOUNT_MAX: u64 = u32::MAX as u64;
fn validate(n: u32, m: u64, loops: bool) -> IgraphResult<()> {
if m > IEA_ECOUNT_MAX {
return Err(IgraphError::InvalidArgument(format!(
"iea_game requested {m} edges, exceeds the {IEA_ECOUNT_MAX} cap"
)));
}
if m == 0 {
return Ok(());
}
if n == 0 {
return Err(IgraphError::InvalidArgument(format!(
"iea_game cannot place {m} edges on 0 vertices"
)));
}
if !loops && n < 2 {
return Err(IgraphError::InvalidArgument(format!(
"iea_game without loops requires n >= 2 when m > 0 (got n = {n})"
)));
}
Ok(())
}
pub fn iea_game(n: u32, m: u64, directed: bool, loops: bool, seed: u64) -> IgraphResult<Graph> {
validate(n, m, loops)?;
if m == 0 {
return Graph::new(n, directed);
}
let mut rng = SplitMix64::new(seed);
let m_usize = usize::try_from(m).map_err(|_| {
IgraphError::Internal("iea_game edge count does not fit in usize on this target")
})?;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m_usize);
let n_usize = n as usize;
if loops {
for _ in 0..m {
let u = rng.gen_index(n_usize) as VertexId;
let v = rng.gen_index(n_usize) as VertexId;
edges.push((u, v));
}
} else {
let n_minus_1 = n_usize - 1;
for _ in 0..m {
let u = rng.gen_index(n_usize) as VertexId;
let mut v_idx = rng.gen_index(n_minus_1) as u32;
if v_idx >= u {
v_idx += 1;
}
edges.push((u, v_idx as VertexId));
}
}
let mut g = Graph::new(n, directed)?;
g.add_edges(edges)?;
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
(0..m)
.map(|eid| g.edge(eid).expect("edge id in bounds"))
.collect()
}
#[test]
fn empty_request_returns_empty_graph() {
let g = iea_game(50, 0, true, true, 42).unwrap();
assert_eq!(g.vcount(), 50);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn zero_vertices_with_zero_edges_is_allowed() {
let g = iea_game(0, 0, false, false, 7).unwrap();
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn zero_vertices_with_edges_is_rejected() {
let err = iea_game(0, 1, true, true, 0).unwrap_err();
match err {
IgraphError::InvalidArgument(_) => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn no_loops_requires_two_vertices() {
let err = iea_game(1, 1, false, false, 1).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
let g = iea_game(1, 0, false, false, 1).unwrap();
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn exact_edge_count_holds_directed_with_loops() {
let g = iea_game(80, 1234, true, true, 0xBEEF).unwrap();
assert_eq!(g.vcount(), 80);
assert_eq!(g.ecount(), 1234);
assert!(g.is_directed());
}
#[test]
fn exact_edge_count_holds_undirected_no_loops() {
let g = iea_game(80, 500, false, false, 0x00C0_FFEE).unwrap();
assert_eq!(g.vcount(), 80);
assert_eq!(g.ecount(), 500);
assert!(!g.is_directed());
}
#[test]
fn no_loops_branch_yields_no_self_loops() {
let g = iea_game(50, 4000, true, false, 0x9999).unwrap();
for (u, v) in collect_edges(&g) {
assert_ne!(u, v, "no-loops branch produced self-loop ({u}, {v})");
}
}
#[test]
fn loops_branch_can_produce_self_loops_eventually() {
let g = iea_game(5, 1000, true, true, 0x1234_5678).unwrap();
let any_self = collect_edges(&g).iter().any(|(u, v)| u == v);
assert!(any_self, "loops=true should sometimes produce self-loops");
}
#[test]
fn deterministic_with_seed() {
let a = iea_game(40, 200, true, true, 0xCAFE).unwrap();
let b = iea_game(40, 200, true, true, 0xCAFE).unwrap();
assert_eq!(collect_edges(&a), collect_edges(&b));
}
#[test]
fn different_seeds_yield_different_graphs() {
let a = iea_game(40, 200, true, true, 1).unwrap();
let b = iea_game(40, 200, true, true, 2).unwrap();
assert_ne!(
collect_edges(&a),
collect_edges(&b),
"different seeds must produce different graphs"
);
}
#[test]
fn endpoint_indices_in_range() {
let n = 30u32;
let g = iea_game(n, 1000, true, true, 0x42).unwrap();
for (u, v) in collect_edges(&g) {
assert!(u < n && v < n, "endpoint out of range: ({u}, {v})");
}
}
#[test]
fn marginal_endpoint_distribution_is_roughly_uniform() {
let n = 8u32;
let m = 200_000u64;
let g = iea_game(n, m, true, true, 0xC0DE).unwrap();
let mut hits = vec![0u64; n as usize];
for (u, v) in collect_edges(&g) {
hits[u as usize] += 1;
hits[v as usize] += 1;
}
let expected = (2 * m) as f64 / f64::from(n);
for (i, &h) in hits.iter().enumerate() {
let dev = ((h as f64) - expected).abs() / expected;
assert!(
dev < 0.05,
"endpoint {i} deviation {dev} exceeds 5%: hits={h}, expected={expected}"
);
}
}
#[test]
fn no_loops_marginal_distribution_is_roughly_uniform() {
let n = 8u32;
let m = 200_000u64;
let g = iea_game(n, m, true, false, 0xD00D).unwrap();
let mut hits = vec![0u64; n as usize];
for (u, v) in collect_edges(&g) {
assert_ne!(u, v);
hits[u as usize] += 1;
hits[v as usize] += 1;
}
let expected = (2 * m) as f64 / f64::from(n);
for (i, &h) in hits.iter().enumerate() {
let dev = ((h as f64) - expected).abs() / expected;
assert!(
dev < 0.05,
"endpoint {i} deviation {dev} exceeds 5%: hits={h}, expected={expected}"
);
}
}
#[test]
fn allows_multi_edges() {
let g = iea_game(4, 1000, true, true, 0x1357).unwrap();
let mut counts: HashMap<(VertexId, VertexId), u32> = HashMap::new();
for e in collect_edges(&g) {
*counts.entry(e).or_default() += 1;
}
let max_mult = counts.values().copied().max().unwrap_or(0);
assert!(
max_mult > 1,
"expected multi-edges with m=1000 on n=4, got max multiplicity {max_mult}"
);
}
#[test]
fn cap_rejects_overlarge_m() {
let err = iea_game(2, u64::from(u32::MAX) + 1, true, true, 0).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(60))]
#[test]
fn vcount_and_ecount_invariants(
n in 2u32..40,
m in 0u64..400,
directed in any::<bool>(),
loops in any::<bool>(),
seed in any::<u64>(),
) {
let g = iea_game(n, m, directed, loops, seed).unwrap();
prop_assert_eq!(g.vcount(), n);
prop_assert_eq!(g.ecount(), m as usize);
prop_assert_eq!(g.is_directed(), directed);
}
#[test]
fn no_loops_branch_has_no_self_loops(
n in 2u32..40,
m in 1u64..400,
directed in any::<bool>(),
seed in any::<u64>(),
) {
let g = iea_game(n, m, directed, false, seed).unwrap();
let m_u32 = u32::try_from(g.ecount()).unwrap();
for eid in 0..m_u32 {
let (u, v) = g.edge(eid).unwrap();
prop_assert_ne!(u, v);
}
}
#[test]
fn determinism_is_seed_only(
n in 2u32..30,
m in 0u64..150,
directed in any::<bool>(),
loops in any::<bool>(),
seed in any::<u64>(),
) {
let a = iea_game(n, m, directed, loops, seed).unwrap();
let b = iea_game(n, m, directed, loops, seed).unwrap();
let am = u32::try_from(a.ecount()).unwrap();
let bm = u32::try_from(b.ecount()).unwrap();
prop_assert_eq!(am, bm);
for eid in 0..am {
prop_assert_eq!(a.edge(eid).unwrap(), b.edge(eid).unwrap());
}
}
#[test]
fn endpoints_within_range(
n in 2u32..40,
m in 0u64..200,
directed in any::<bool>(),
loops in any::<bool>(),
seed in any::<u64>(),
) {
let g = iea_game(n, m, directed, loops, seed).unwrap();
let m_u32 = u32::try_from(g.ecount()).unwrap();
for eid in 0..m_u32 {
let (u, v) = g.edge(eid).unwrap();
prop_assert!(u < n);
prop_assert!(v < n);
}
}
}
}