1#![allow(
55 clippy::cast_possible_truncation,
56 clippy::cast_precision_loss,
57 clippy::cast_sign_loss
58)]
59
60use crate::core::rng::SplitMix64;
61use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
62
63const IEA_ECOUNT_MAX: u64 = u32::MAX as u64;
66
67fn validate(n: u32, m: u64, loops: bool) -> IgraphResult<()> {
68 if m > IEA_ECOUNT_MAX {
69 return Err(IgraphError::InvalidArgument(format!(
70 "iea_game requested {m} edges, exceeds the {IEA_ECOUNT_MAX} cap"
71 )));
72 }
73 if m == 0 {
74 return Ok(());
75 }
76 if n == 0 {
77 return Err(IgraphError::InvalidArgument(format!(
78 "iea_game cannot place {m} edges on 0 vertices"
79 )));
80 }
81 if !loops && n < 2 {
82 return Err(IgraphError::InvalidArgument(format!(
83 "iea_game without loops requires n >= 2 when m > 0 (got n = {n})"
84 )));
85 }
86 Ok(())
87}
88
89pub fn iea_game(n: u32, m: u64, directed: bool, loops: bool, seed: u64) -> IgraphResult<Graph> {
122 validate(n, m, loops)?;
123
124 if m == 0 {
125 return Graph::new(n, directed);
126 }
127
128 let mut rng = SplitMix64::new(seed);
129 let m_usize = usize::try_from(m).map_err(|_| {
130 IgraphError::Internal("iea_game edge count does not fit in usize on this target")
131 })?;
132 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m_usize);
133
134 let n_usize = n as usize;
135 if loops {
136 for _ in 0..m {
137 let u = rng.gen_index(n_usize) as VertexId;
138 let v = rng.gen_index(n_usize) as VertexId;
139 edges.push((u, v));
140 }
141 } else {
142 let n_minus_1 = n_usize - 1;
152 for _ in 0..m {
153 let u = rng.gen_index(n_usize) as VertexId;
154 let mut v_idx = rng.gen_index(n_minus_1) as u32;
155 if v_idx >= u {
156 v_idx += 1;
157 }
158 edges.push((u, v_idx as VertexId));
159 }
160 }
161
162 let mut g = Graph::new(n, directed)?;
163 g.add_edges(edges)?;
164 Ok(g)
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use std::collections::HashMap;
171
172 fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
173 let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
174 (0..m)
175 .map(|eid| g.edge(eid).expect("edge id in bounds"))
176 .collect()
177 }
178
179 #[test]
180 fn empty_request_returns_empty_graph() {
181 let g = iea_game(50, 0, true, true, 42).unwrap();
182 assert_eq!(g.vcount(), 50);
183 assert_eq!(g.ecount(), 0);
184 assert!(g.is_directed());
185 }
186
187 #[test]
188 fn zero_vertices_with_zero_edges_is_allowed() {
189 let g = iea_game(0, 0, false, false, 7).unwrap();
190 assert_eq!(g.vcount(), 0);
191 assert_eq!(g.ecount(), 0);
192 }
193
194 #[test]
195 fn zero_vertices_with_edges_is_rejected() {
196 let err = iea_game(0, 1, true, true, 0).unwrap_err();
197 match err {
198 IgraphError::InvalidArgument(_) => {}
199 other => panic!("unexpected error: {other:?}"),
200 }
201 }
202
203 #[test]
204 fn no_loops_requires_two_vertices() {
205 let err = iea_game(1, 1, false, false, 1).unwrap_err();
206 assert!(matches!(err, IgraphError::InvalidArgument(_)));
207 let g = iea_game(1, 0, false, false, 1).unwrap();
209 assert_eq!(g.vcount(), 1);
210 assert_eq!(g.ecount(), 0);
211 }
212
213 #[test]
214 fn exact_edge_count_holds_directed_with_loops() {
215 let g = iea_game(80, 1234, true, true, 0xBEEF).unwrap();
216 assert_eq!(g.vcount(), 80);
217 assert_eq!(g.ecount(), 1234);
218 assert!(g.is_directed());
219 }
220
221 #[test]
222 fn exact_edge_count_holds_undirected_no_loops() {
223 let g = iea_game(80, 500, false, false, 0x00C0_FFEE).unwrap();
224 assert_eq!(g.vcount(), 80);
225 assert_eq!(g.ecount(), 500);
226 assert!(!g.is_directed());
227 }
228
229 #[test]
230 fn no_loops_branch_yields_no_self_loops() {
231 let g = iea_game(50, 4000, true, false, 0x9999).unwrap();
232 for (u, v) in collect_edges(&g) {
233 assert_ne!(u, v, "no-loops branch produced self-loop ({u}, {v})");
234 }
235 }
236
237 #[test]
238 fn loops_branch_can_produce_self_loops_eventually() {
239 let g = iea_game(5, 1000, true, true, 0x1234_5678).unwrap();
243 let any_self = collect_edges(&g).iter().any(|(u, v)| u == v);
244 assert!(any_self, "loops=true should sometimes produce self-loops");
245 }
246
247 #[test]
248 fn deterministic_with_seed() {
249 let a = iea_game(40, 200, true, true, 0xCAFE).unwrap();
250 let b = iea_game(40, 200, true, true, 0xCAFE).unwrap();
251 assert_eq!(collect_edges(&a), collect_edges(&b));
252 }
253
254 #[test]
255 fn different_seeds_yield_different_graphs() {
256 let a = iea_game(40, 200, true, true, 1).unwrap();
257 let b = iea_game(40, 200, true, true, 2).unwrap();
258 assert_ne!(
259 collect_edges(&a),
260 collect_edges(&b),
261 "different seeds must produce different graphs"
262 );
263 }
264
265 #[test]
266 fn endpoint_indices_in_range() {
267 let n = 30u32;
268 let g = iea_game(n, 1000, true, true, 0x42).unwrap();
269 for (u, v) in collect_edges(&g) {
270 assert!(u < n && v < n, "endpoint out of range: ({u}, {v})");
271 }
272 }
273
274 #[test]
275 fn marginal_endpoint_distribution_is_roughly_uniform() {
276 let n = 8u32;
281 let m = 200_000u64;
282 let g = iea_game(n, m, true, true, 0xC0DE).unwrap();
283 let mut hits = vec![0u64; n as usize];
284 for (u, v) in collect_edges(&g) {
285 hits[u as usize] += 1;
286 hits[v as usize] += 1;
287 }
288 let expected = (2 * m) as f64 / f64::from(n);
289 for (i, &h) in hits.iter().enumerate() {
290 let dev = ((h as f64) - expected).abs() / expected;
291 assert!(
292 dev < 0.05,
293 "endpoint {i} deviation {dev} exceeds 5%: hits={h}, expected={expected}"
294 );
295 }
296 }
297
298 #[test]
299 fn no_loops_marginal_distribution_is_roughly_uniform() {
300 let n = 8u32;
301 let m = 200_000u64;
302 let g = iea_game(n, m, true, false, 0xD00D).unwrap();
303 let mut hits = vec![0u64; n as usize];
304 for (u, v) in collect_edges(&g) {
305 assert_ne!(u, v);
306 hits[u as usize] += 1;
307 hits[v as usize] += 1;
308 }
309 let expected = (2 * m) as f64 / f64::from(n);
310 for (i, &h) in hits.iter().enumerate() {
311 let dev = ((h as f64) - expected).abs() / expected;
312 assert!(
313 dev < 0.05,
314 "endpoint {i} deviation {dev} exceeds 5%: hits={h}, expected={expected}"
315 );
316 }
317 }
318
319 #[test]
320 fn allows_multi_edges() {
321 let g = iea_game(4, 1000, true, true, 0x1357).unwrap();
324 let mut counts: HashMap<(VertexId, VertexId), u32> = HashMap::new();
325 for e in collect_edges(&g) {
326 *counts.entry(e).or_default() += 1;
327 }
328 let max_mult = counts.values().copied().max().unwrap_or(0);
329 assert!(
330 max_mult > 1,
331 "expected multi-edges with m=1000 on n=4, got max multiplicity {max_mult}"
332 );
333 }
334
335 #[test]
336 fn cap_rejects_overlarge_m() {
337 let err = iea_game(2, u64::from(u32::MAX) + 1, true, true, 0).unwrap_err();
338 assert!(matches!(err, IgraphError::InvalidArgument(_)));
339 }
340}
341
342#[cfg(all(test, feature = "proptest-harness"))]
343mod proptests {
344 use super::*;
345 use proptest::prelude::*;
346
347 proptest! {
348 #![proptest_config(ProptestConfig::with_cases(60))]
349
350 #[test]
351 fn vcount_and_ecount_invariants(
352 n in 2u32..40,
353 m in 0u64..400,
354 directed in any::<bool>(),
355 loops in any::<bool>(),
356 seed in any::<u64>(),
357 ) {
358 let g = iea_game(n, m, directed, loops, seed).unwrap();
359 prop_assert_eq!(g.vcount(), n);
360 prop_assert_eq!(g.ecount(), m as usize);
361 prop_assert_eq!(g.is_directed(), directed);
362 }
363
364 #[test]
365 fn no_loops_branch_has_no_self_loops(
366 n in 2u32..40,
367 m in 1u64..400,
368 directed in any::<bool>(),
369 seed in any::<u64>(),
370 ) {
371 let g = iea_game(n, m, directed, false, seed).unwrap();
372 let m_u32 = u32::try_from(g.ecount()).unwrap();
373 for eid in 0..m_u32 {
374 let (u, v) = g.edge(eid).unwrap();
375 prop_assert_ne!(u, v);
376 }
377 }
378
379 #[test]
380 fn determinism_is_seed_only(
381 n in 2u32..30,
382 m in 0u64..150,
383 directed in any::<bool>(),
384 loops in any::<bool>(),
385 seed in any::<u64>(),
386 ) {
387 let a = iea_game(n, m, directed, loops, seed).unwrap();
388 let b = iea_game(n, m, directed, loops, seed).unwrap();
389 let am = u32::try_from(a.ecount()).unwrap();
390 let bm = u32::try_from(b.ecount()).unwrap();
391 prop_assert_eq!(am, bm);
392 for eid in 0..am {
393 prop_assert_eq!(a.edge(eid).unwrap(), b.edge(eid).unwrap());
394 }
395 }
396
397 #[test]
398 fn endpoints_within_range(
399 n in 2u32..40,
400 m in 0u64..200,
401 directed in any::<bool>(),
402 loops in any::<bool>(),
403 seed in any::<u64>(),
404 ) {
405 let g = iea_game(n, m, directed, loops, seed).unwrap();
406 let m_u32 = u32::try_from(g.ecount()).unwrap();
407 for eid in 0..m_u32 {
408 let (u, v) = g.edge(eid).unwrap();
409 prop_assert!(u < n);
410 prop_assert!(v < n);
411 }
412 }
413 }
414}