#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
use crate::core::rng::SplitMix64;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
fn validate_inputs(n: u32, radius: f64) -> IgraphResult<()> {
if !radius.is_finite() {
return Err(IgraphError::Internal("grg_game radius must be finite"));
}
let _ = n;
Ok(())
}
pub fn grg_game(n: u32, radius: f64, torus: bool, seed: u64) -> IgraphResult<Graph> {
let (graph, _xs, _ys) = grg_game_with_coords(n, radius, torus, seed)?;
Ok(graph)
}
pub fn grg_game_with_coords(
n: u32,
radius: f64,
torus: bool,
seed: u64,
) -> IgraphResult<(Graph, Vec<f64>, Vec<f64>)> {
validate_inputs(n, radius)?;
let n_usize = n as usize;
let radius = radius.max(0.0);
let r2 = radius * radius;
let mut xs: Vec<f64> = Vec::with_capacity(n_usize);
let mut ys: Vec<f64> = Vec::with_capacity(n_usize);
let mut rng = SplitMix64::new(seed);
for _ in 0..n_usize {
xs.push(rng.gen_unit());
ys.push(rng.gen_unit());
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
if n_usize < 2 || radius <= 0.0 {
let g = Graph::new(n, false)?;
return Ok((g, xs, ys));
}
let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
if torus {
for i in 0..n_usize {
let xi = xs[i];
let yi = ys[i];
let mut j = i + 1;
while j < n_usize {
let dx = xs[j] - xi;
if dx >= radius {
break;
}
let mut dy = (ys[j] - yi).abs();
if dy > 0.5 {
dy = 1.0 - dy;
}
if dx * dx + dy * dy < r2 {
edges.push((i as VertexId, j as VertexId));
}
j += 1;
}
if j == n_usize {
let mut k = 0usize;
while k < i {
let dx = 1.0 - xi + xs[k];
if dx >= radius {
break;
}
if xi - xs[k] < radius {
k += 1;
continue;
}
let mut dy = (ys[k] - yi).abs();
if dy > 0.5 {
dy = 1.0 - dy;
}
if dx * dx + dy * dy < r2 {
edges.push((k as VertexId, i as VertexId));
}
k += 1;
}
}
}
} else {
for i in 0..n_usize {
let xi = xs[i];
let yi = ys[i];
let mut j = i + 1;
while j < n_usize {
let dx = xs[j] - xi;
if dx >= radius {
break;
}
let dy = ys[j] - yi;
if dx * dx + dy * dy < r2 {
edges.push((i as VertexId, j as VertexId));
}
j += 1;
}
}
}
let mut g = Graph::new(n, false)?;
g.add_edges(edges)?;
Ok((g, xs, ys))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn edge_set(g: &Graph) -> HashSet<(VertexId, VertexId)> {
let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
(0..n_edges)
.map(|eid| {
let (a, b) = g.edge(eid).expect("edge id in bounds");
if a <= b { (a, b) } else { (b, a) }
})
.collect()
}
#[test]
fn n_zero_returns_empty_graph() {
let g = grg_game(0, 0.1, false, 1).unwrap();
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn n_one_returns_singleton() {
let g = grg_game(1, 0.5, false, 1).unwrap();
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn zero_radius_yields_empty_graph() {
let g = grg_game(50, 0.0, false, 0xDEAD).unwrap();
assert_eq!(g.vcount(), 50);
assert_eq!(g.ecount(), 0);
}
#[test]
fn negative_radius_treated_as_zero() {
let g = grg_game(50, -1.0, false, 0xDEAD).unwrap();
assert_eq!(g.ecount(), 0);
}
#[test]
fn always_undirected() {
let g = grg_game(10, 0.5, false, 1).unwrap();
assert!(!g.is_directed());
let g = grg_game(10, 0.5, true, 1).unwrap();
assert!(!g.is_directed());
}
#[test]
fn no_self_loops_no_multi_edges() {
for &(n, r, torus) in &[
(50u32, 0.2, false),
(50, 0.2, true),
(200, 0.05, false),
(200, 0.05, true),
] {
let g = grg_game(n, r, torus, 0x5EED).unwrap();
let edges = edge_set(&g);
assert_eq!(edges.len(), g.ecount(), "multi-edges in {n} {r} {torus}");
for &(a, b) in &edges {
assert_ne!(a, b, "self-loop in {n} {r} {torus}");
}
}
}
#[test]
fn dense_radius_connects_almost_everything_plane() {
let n = 30u32;
let g = grg_game(n, 2.0, false, 0xBEEF).unwrap();
let expected = (n as usize) * (n as usize - 1) / 2;
assert_eq!(g.ecount(), expected);
}
#[test]
fn dense_radius_connects_almost_everything_torus() {
let n = 30u32;
let g = grg_game(n, 2.0, true, 0xBEEF).unwrap();
let expected = (n as usize) * (n as usize - 1) / 2;
assert_eq!(g.ecount(), expected);
}
#[test]
fn determinism_same_seed_same_graph() {
let a = grg_game(100, 0.15, false, 0x1234).unwrap();
let b = grg_game(100, 0.15, false, 0x1234).unwrap();
assert_eq!(a.vcount(), b.vcount());
assert_eq!(a.ecount(), b.ecount());
assert_eq!(edge_set(&a), edge_set(&b));
}
#[test]
fn distinct_seeds_yield_distinct_graphs() {
let a = grg_game(200, 0.1, false, 0x1).unwrap();
let b = grg_game(200, 0.1, false, 0x2).unwrap();
assert_ne!(edge_set(&a), edge_set(&b));
}
#[test]
fn torus_adds_edges_versus_plane() {
for seed in 0u64..16 {
let plane = grg_game(80, 0.12, false, seed).unwrap();
let torus = grg_game(80, 0.12, true, seed).unwrap();
assert!(
torus.ecount() >= plane.ecount(),
"seed {seed}: torus={} plane={}",
torus.ecount(),
plane.ecount(),
);
}
}
#[test]
fn coords_returned_match_graph() {
let (g, xs, ys) = grg_game_with_coords(64, 0.1, false, 0x77).unwrap();
assert_eq!(xs.len(), 64);
assert_eq!(ys.len(), 64);
assert!(xs.windows(2).all(|w| w[0] <= w[1]));
let g2 = grg_game(64, 0.1, false, 0x77).unwrap();
assert_eq!(edge_set(&g), edge_set(&g2));
}
#[test]
fn nan_radius_errors() {
assert!(grg_game(10, f64::NAN, false, 1).is_err());
}
#[test]
fn inf_radius_errors() {
assert!(grg_game(10, f64::INFINITY, false, 1).is_err());
}
#[test]
fn expected_density_in_band() {
let n = 400u32;
let r = 0.05;
let predicted = (f64::from(n) * f64::from(n - 1) / 2.0) * std::f64::consts::PI * r * r;
let g = grg_game(n, r, true, 0xBEEF).unwrap();
let actual = g.ecount() as f64;
assert!(
actual > predicted * 0.5 && actual < predicted * 1.5,
"expected ~{predicted:.1} edges, got {actual}",
);
}
}