#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::many_single_char_names,
clippy::similar_names
)]
use std::collections::HashSet;
use crate::core::rng::SplitMix64;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
fn validate(size: u32, nei: u32, p: f64) -> IgraphResult<()> {
if size == 0 {
return Err(IgraphError::InvalidArgument(
"watts_strogatz_game: size must be at least 1".into(),
));
}
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(IgraphError::InvalidArgument(
"watts_strogatz_game: p must be a finite real in [0, 1]".into(),
));
}
let two_nei = u64::from(nei).saturating_mul(2);
if two_nei + 1 > u64::from(size) {
return Err(IgraphError::InvalidArgument(format!(
"watts_strogatz_game: 2*nei + 1 = {} must not exceed size = {}",
two_nei + 1,
size,
)));
}
Ok(())
}
fn build_ring_lattice_edges(size: u32, nei: u32) -> Vec<(u32, u32)> {
let n = size as usize;
let k = nei as usize;
let mut edges: Vec<(u32, u32)> = Vec::with_capacity(n * k);
for v in 0..size {
for j in 1..=nei {
let u = (v + j) % size;
edges.push((v, u));
}
}
edges
}
#[inline]
fn canonical(a: u32, b: u32) -> (u32, u32) {
if a <= b { (a, b) } else { (b, a) }
}
#[inline]
fn draw_vertex(size: u32, forbidden: u32, loops: bool, rng: &mut SplitMix64) -> u32 {
if loops {
rng.gen_index(size as usize) as u32
} else {
let r = rng.gen_index((size - 1) as usize) as u32;
if r == forbidden { size - 1 } else { r }
}
}
fn rewire_one_endpoint(
eid: usize,
which: u8,
edges: &mut [(u32, u32)],
adj: Option<&mut HashSet<(u32, u32)>>,
size: u32,
loops: bool,
rng: &mut SplitMix64,
) {
let (a, b) = edges[eid];
let (old_v, kept) = if which == 0 { (a, b) } else { (b, a) };
if let Some(set) = adj.as_ref() {
debug_assert!(set.contains(&canonical(a, b)));
}
if let Some(set) = adj {
set.remove(&canonical(a, b));
let mut tries = 0u32;
let cap = size.saturating_mul(4).max(64);
let pick = loop {
let cand = draw_vertex(size, kept, loops, rng);
let pair = canonical(cand, kept);
if !set.contains(&pair) {
break cand;
}
tries += 1;
if tries >= cap {
break old_v;
}
};
let new_pair = canonical(pick, kept);
set.insert(new_pair);
if which == 0 {
edges[eid] = (pick, kept);
} else {
edges[eid] = (kept, pick);
}
} else {
let pick = draw_vertex(size, kept, loops, rng);
if which == 0 {
edges[eid] = (pick, kept);
} else {
edges[eid] = (kept, pick);
}
}
}
pub fn watts_strogatz_game(
size: u32,
nei: u32,
p: f64,
loops: bool,
multiple: bool,
seed: u64,
) -> IgraphResult<Graph> {
validate(size, nei, p)?;
let mut edges = build_ring_lattice_edges(size, nei);
let m = edges.len();
if p > 0.0 && m > 0 {
let mut rng = SplitMix64::new(seed);
let mut adj_set: Option<HashSet<(u32, u32)>> = if multiple {
None
} else {
let mut s = HashSet::with_capacity(m);
for &(a, b) in &edges {
s.insert(canonical(a, b));
}
Some(s)
};
let mut to_rewire = rng.gen_geom(p);
while to_rewire.is_finite() && (to_rewire as usize) < m {
let eid = to_rewire as usize;
rewire_one_endpoint(eid, 0, &mut edges, adj_set.as_mut(), size, loops, &mut rng);
to_rewire += rng.gen_geom(p) + 1.0;
}
let mut to_rewire = rng.gen_geom(p);
while to_rewire.is_finite() && (to_rewire as usize) < m {
let eid = to_rewire as usize;
rewire_one_endpoint(eid, 1, &mut edges, adj_set.as_mut(), size, loops, &mut rng);
to_rewire += rng.gen_geom(p) + 1.0;
}
}
let mut g = Graph::with_vertices(size);
let edges_iter = edges
.into_iter()
.map(|(a, b)| (a as VertexId, b as VertexId));
g.add_edges(edges_iter)?;
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn count_unique_edges(g: &Graph) -> usize {
let mut canonical: HashSet<(u32, u32)> = HashSet::with_capacity(g.ecount());
let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
for eid in 0..m {
let (a, b) = g.edge(eid).expect("edge id in bounds");
canonical.insert(if a <= b { (a, b) } else { (b, a) });
}
canonical.len()
}
fn degrees(g: &Graph) -> Vec<u64> {
let mut d = vec![0u64; g.vcount() as usize];
let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
for eid in 0..m {
let (a, b) = g.edge(eid).expect("edge id in bounds");
d[a as usize] += 1;
d[b as usize] += 1;
}
d
}
#[test]
fn ring_lattice_p0_undirected_undirected_count() {
let g = watts_strogatz_game(8, 2, 0.0, false, false, 1).unwrap();
assert_eq!(g.vcount(), 8);
assert_eq!(g.ecount(), 16);
assert!(!g.is_directed());
let d = degrees(&g);
for (v, deg) in d.iter().enumerate() {
assert_eq!(*deg, 4, "vertex {v} has degree {deg}, expected 4");
}
}
#[test]
fn ring_lattice_nei_zero_is_edgeless() {
let g = watts_strogatz_game(10, 0, 0.5, false, false, 7).unwrap();
assert_eq!(g.vcount(), 10);
assert_eq!(g.ecount(), 0);
}
#[test]
fn p_one_rewires_every_endpoint_undirected() {
let g = watts_strogatz_game(20, 3, 1.0, false, false, 0x_C0FFEE).unwrap();
assert_eq!(g.vcount(), 20);
assert_eq!(g.ecount(), 20 * 3); let n_unique = count_unique_edges(&g);
assert_eq!(
n_unique,
g.ecount(),
"rewire should not introduce multi-edges"
);
for eid in 0..u32::try_from(g.ecount()).unwrap() {
let (a, b) = g.edge(eid).unwrap();
assert_ne!(a, b, "no self-loops when loops=false (edge {eid})");
}
}
#[test]
fn rewire_preserves_edge_count() {
for &p in &[0.0, 0.1, 0.3, 0.5, 0.9, 1.0] {
let g = watts_strogatz_game(50, 3, p, false, false, 0xABCD).unwrap();
assert_eq!(
g.ecount(),
50 * 3,
"edge count should equal size·nei at p={p}"
);
}
}
#[test]
fn simple_output_has_no_loops_or_multi() {
let g = watts_strogatz_game(30, 4, 0.5, false, false, 0xDEAD).unwrap();
let n_unique = count_unique_edges(&g);
assert_eq!(n_unique, g.ecount(), "no multi-edges");
for eid in 0..u32::try_from(g.ecount()).unwrap() {
let (a, b) = g.edge(eid).unwrap();
assert_ne!(a, b, "no self-loops");
}
}
#[test]
fn multigraph_path_runs_without_panic() {
let g = watts_strogatz_game(20, 2, 0.7, true, true, 0xBEEF).unwrap();
assert_eq!(g.vcount(), 20);
assert_eq!(g.ecount(), 20 * 2);
}
#[test]
fn loops_true_multiple_false_may_self_loop() {
let g = watts_strogatz_game(10, 1, 1.0, true, false, 0x1357).unwrap();
assert_eq!(g.ecount(), 10);
let n_unique = count_unique_edges(&g);
assert_eq!(n_unique, g.ecount());
}
#[test]
fn determinism_same_seed_same_graph() {
let a = watts_strogatz_game(30, 3, 0.2, false, false, 123_456).unwrap();
let b = watts_strogatz_game(30, 3, 0.2, false, false, 123_456).unwrap();
assert_eq!(a.vcount(), b.vcount());
assert_eq!(a.ecount(), b.ecount());
let ea: Vec<(u32, u32)> = (0..u32::try_from(a.ecount()).unwrap())
.map(|eid| {
let (u, v) = a.edge(eid).unwrap();
if u <= v { (u, v) } else { (v, u) }
})
.collect();
let eb: Vec<(u32, u32)> = (0..u32::try_from(b.ecount()).unwrap())
.map(|eid| {
let (u, v) = b.edge(eid).unwrap();
if u <= v { (u, v) } else { (v, u) }
})
.collect();
let mut sa = ea.clone();
sa.sort_unstable();
let mut sb = eb.clone();
sb.sort_unstable();
assert_eq!(sa, sb);
}
#[test]
fn different_seeds_diverge() {
let a = watts_strogatz_game(30, 3, 0.5, false, false, 1).unwrap();
let b = watts_strogatz_game(30, 3, 0.5, false, false, 2).unwrap();
let ea: HashSet<(u32, u32)> = (0..u32::try_from(a.ecount()).unwrap())
.map(|eid| {
let (u, v) = a.edge(eid).unwrap();
if u <= v { (u, v) } else { (v, u) }
})
.collect();
let eb: HashSet<(u32, u32)> = (0..u32::try_from(b.ecount()).unwrap())
.map(|eid| {
let (u, v) = b.edge(eid).unwrap();
if u <= v { (u, v) } else { (v, u) }
})
.collect();
assert_ne!(
ea, eb,
"different seeds should yield different rewired graphs"
);
}
#[test]
fn size_zero_rejected() {
let err = watts_strogatz_game(0, 0, 0.0, false, false, 0).unwrap_err();
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("size")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn nei_too_large_rejected() {
let err = watts_strogatz_game(6, 3, 0.0, false, false, 0).unwrap_err();
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("nei")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn p_out_of_range_rejected() {
for &p in &[-0.1_f64, 1.1, f64::NAN, f64::INFINITY] {
let err = watts_strogatz_game(10, 2, p, false, false, 0).unwrap_err();
matches!(err, IgraphError::InvalidArgument(_));
}
}
#[test]
fn ring_lattice_2_nei_equals_size_minus_one_is_almost_complete() {
let g = watts_strogatz_game(5, 2, 0.0, false, false, 0).unwrap();
assert_eq!(g.ecount(), 10); let d = degrees(&g);
for deg in d {
assert_eq!(deg, 4);
}
}
#[test]
fn p_zero_seed_independence() {
let a = watts_strogatz_game(20, 2, 0.0, false, false, 1).unwrap();
let b = watts_strogatz_game(20, 2, 0.0, false, false, 999).unwrap();
assert_eq!(a.ecount(), b.ecount());
for eid in 0..u32::try_from(a.ecount()).unwrap() {
assert_eq!(a.edge(eid).unwrap(), b.edge(eid).unwrap());
}
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
use std::collections::HashSet;
fn canonical_set(g: &Graph) -> HashSet<(u32, u32)> {
let mut s = HashSet::with_capacity(g.ecount());
let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
for eid in 0..m {
let (a, b) = g.edge(eid).expect("edge id in bounds");
s.insert(if a <= b { (a, b) } else { (b, a) });
}
s
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 64, .. ProptestConfig::default()
})]
#[test]
fn invariant_simple_graph_no_loops_no_multi(
size in 4u32..30,
nei_raw in 1u32..5,
p in 0.0_f64..=1.0,
seed in any::<u64>(),
) {
let nei = std::cmp::min(nei_raw, (size - 1) / 2);
prop_assume!(nei >= 1);
let g = watts_strogatz_game(size, nei, p, false, false, seed).unwrap();
prop_assert_eq!(g.vcount(), size);
prop_assert_eq!(g.ecount(), (size * nei) as usize);
let canonical = canonical_set(&g);
prop_assert_eq!(canonical.len(), g.ecount());
for eid in 0..u32::try_from(g.ecount()).unwrap() {
let (a, b) = g.edge(eid).unwrap();
prop_assert_ne!(a, b);
}
}
#[test]
fn invariant_edge_count_preserved_under_any_p(
size in 4u32..30,
nei_raw in 1u32..5,
p in 0.0_f64..=1.0,
loops in any::<bool>(),
multiple in any::<bool>(),
seed in any::<u64>(),
) {
let nei = std::cmp::min(nei_raw, (size - 1) / 2);
prop_assume!(nei >= 1);
let g = watts_strogatz_game(size, nei, p, loops, multiple, seed).unwrap();
prop_assert_eq!(g.ecount(), (size * nei) as usize);
prop_assert_eq!(g.vcount(), size);
}
#[test]
fn invariant_determinism(
size in 4u32..30,
nei_raw in 1u32..5,
p in 0.0_f64..=1.0,
loops in any::<bool>(),
multiple in any::<bool>(),
seed in any::<u64>(),
) {
let nei = std::cmp::min(nei_raw, (size - 1) / 2);
prop_assume!(nei >= 1);
let a = watts_strogatz_game(size, nei, p, loops, multiple, seed).unwrap();
let b = watts_strogatz_game(size, nei, p, loops, multiple, seed).unwrap();
prop_assert_eq!(a.vcount(), b.vcount());
prop_assert_eq!(a.ecount(), b.ecount());
let mut ea: Vec<(u32, u32)> = (0..u32::try_from(a.ecount()).unwrap())
.map(|eid| {
let (u, v) = a.edge(eid).unwrap();
if u <= v { (u, v) } else { (v, u) }
})
.collect();
let mut eb: Vec<(u32, u32)> = (0..u32::try_from(b.ecount()).unwrap())
.map(|eid| {
let (u, v) = b.edge(eid).unwrap();
if u <= v { (u, v) } else { (v, u) }
})
.collect();
ea.sort_unstable();
eb.sort_unstable();
prop_assert_eq!(ea, eb);
}
}
}