1#![allow(
49 clippy::cast_possible_truncation,
50 clippy::cast_sign_loss,
51 clippy::many_single_char_names,
52 clippy::similar_names
53)]
54
55use std::collections::HashSet;
56
57use crate::core::rng::SplitMix64;
58use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
59
60fn validate(size: u32, nei: u32, p: f64) -> IgraphResult<()> {
61 if size == 0 {
62 return Err(IgraphError::InvalidArgument(
63 "watts_strogatz_game: size must be at least 1".into(),
64 ));
65 }
66 if !p.is_finite() || !(0.0..=1.0).contains(&p) {
67 return Err(IgraphError::InvalidArgument(
68 "watts_strogatz_game: p must be a finite real in [0, 1]".into(),
69 ));
70 }
71 let two_nei = u64::from(nei).saturating_mul(2);
74 if two_nei + 1 > u64::from(size) {
75 return Err(IgraphError::InvalidArgument(format!(
76 "watts_strogatz_game: 2*nei + 1 = {} must not exceed size = {}",
77 two_nei + 1,
78 size,
79 )));
80 }
81 Ok(())
82}
83
84fn build_ring_lattice_edges(size: u32, nei: u32) -> Vec<(u32, u32)> {
88 let n = size as usize;
89 let k = nei as usize;
90 let mut edges: Vec<(u32, u32)> = Vec::with_capacity(n * k);
91 for v in 0..size {
92 for j in 1..=nei {
93 let u = (v + j) % size;
94 edges.push((v, u));
95 }
96 }
97 edges
98}
99
100#[inline]
102fn canonical(a: u32, b: u32) -> (u32, u32) {
103 if a <= b { (a, b) } else { (b, a) }
104}
105
106#[inline]
113fn draw_vertex(size: u32, forbidden: u32, loops: bool, rng: &mut SplitMix64) -> u32 {
114 if loops {
115 rng.gen_index(size as usize) as u32
116 } else {
117 let r = rng.gen_index((size - 1) as usize) as u32;
118 if r == forbidden { size - 1 } else { r }
119 }
120}
121
122fn rewire_one_endpoint(
129 eid: usize,
130 which: u8,
131 edges: &mut [(u32, u32)],
132 adj: Option<&mut HashSet<(u32, u32)>>,
133 size: u32,
134 loops: bool,
135 rng: &mut SplitMix64,
136) {
137 let (a, b) = edges[eid];
138 let (old_v, kept) = if which == 0 { (a, b) } else { (b, a) };
139
140 if let Some(set) = adj.as_ref() {
141 debug_assert!(set.contains(&canonical(a, b)));
142 }
143
144 if let Some(set) = adj {
145 set.remove(&canonical(a, b));
149
150 let mut tries = 0u32;
156 let cap = size.saturating_mul(4).max(64);
157 let pick = loop {
158 let cand = draw_vertex(size, kept, loops, rng);
159 let pair = canonical(cand, kept);
160 if !set.contains(&pair) {
161 break cand;
162 }
163 tries += 1;
164 if tries >= cap {
165 break old_v;
170 }
171 };
172 let new_pair = canonical(pick, kept);
173 set.insert(new_pair);
174 if which == 0 {
175 edges[eid] = (pick, kept);
176 } else {
177 edges[eid] = (kept, pick);
178 }
179 } else {
180 let pick = draw_vertex(size, kept, loops, rng);
182 if which == 0 {
183 edges[eid] = (pick, kept);
184 } else {
185 edges[eid] = (kept, pick);
186 }
187 }
188}
189
190pub fn watts_strogatz_game(
223 size: u32,
224 nei: u32,
225 p: f64,
226 loops: bool,
227 multiple: bool,
228 seed: u64,
229) -> IgraphResult<Graph> {
230 validate(size, nei, p)?;
231
232 let mut edges = build_ring_lattice_edges(size, nei);
233 let m = edges.len();
234
235 if p > 0.0 && m > 0 {
237 let mut rng = SplitMix64::new(seed);
238
239 let mut adj_set: Option<HashSet<(u32, u32)>> = if multiple {
241 None
242 } else {
243 let mut s = HashSet::with_capacity(m);
244 for &(a, b) in &edges {
245 s.insert(canonical(a, b));
246 }
247 Some(s)
248 };
249
250 let mut to_rewire = rng.gen_geom(p);
254 while to_rewire.is_finite() && (to_rewire as usize) < m {
255 let eid = to_rewire as usize;
256 rewire_one_endpoint(eid, 0, &mut edges, adj_set.as_mut(), size, loops, &mut rng);
257 to_rewire += rng.gen_geom(p) + 1.0;
258 }
259
260 let mut to_rewire = rng.gen_geom(p);
262 while to_rewire.is_finite() && (to_rewire as usize) < m {
263 let eid = to_rewire as usize;
264 rewire_one_endpoint(eid, 1, &mut edges, adj_set.as_mut(), size, loops, &mut rng);
265 to_rewire += rng.gen_geom(p) + 1.0;
266 }
267 }
268
269 let mut g = Graph::with_vertices(size);
270 let edges_iter = edges
271 .into_iter()
272 .map(|(a, b)| (a as VertexId, b as VertexId));
273 g.add_edges(edges_iter)?;
274 Ok(g)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use std::collections::HashSet;
281
282 fn count_unique_edges(g: &Graph) -> usize {
283 let mut canonical: HashSet<(u32, u32)> = HashSet::with_capacity(g.ecount());
284 let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
285 for eid in 0..m {
286 let (a, b) = g.edge(eid).expect("edge id in bounds");
287 canonical.insert(if a <= b { (a, b) } else { (b, a) });
288 }
289 canonical.len()
290 }
291
292 fn degrees(g: &Graph) -> Vec<u64> {
293 let mut d = vec![0u64; g.vcount() as usize];
294 let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
295 for eid in 0..m {
296 let (a, b) = g.edge(eid).expect("edge id in bounds");
297 d[a as usize] += 1;
298 d[b as usize] += 1;
299 }
300 d
301 }
302
303 #[test]
304 fn ring_lattice_p0_undirected_undirected_count() {
305 let g = watts_strogatz_game(8, 2, 0.0, false, false, 1).unwrap();
308 assert_eq!(g.vcount(), 8);
309 assert_eq!(g.ecount(), 16);
310 assert!(!g.is_directed());
311 let d = degrees(&g);
312 for (v, deg) in d.iter().enumerate() {
313 assert_eq!(*deg, 4, "vertex {v} has degree {deg}, expected 4");
314 }
315 }
316
317 #[test]
318 fn ring_lattice_nei_zero_is_edgeless() {
319 let g = watts_strogatz_game(10, 0, 0.5, false, false, 7).unwrap();
320 assert_eq!(g.vcount(), 10);
321 assert_eq!(g.ecount(), 0);
322 }
323
324 #[test]
325 fn p_one_rewires_every_endpoint_undirected() {
326 let g = watts_strogatz_game(20, 3, 1.0, false, false, 0x_C0FFEE).unwrap();
330 assert_eq!(g.vcount(), 20);
331 assert_eq!(g.ecount(), 20 * 3); let n_unique = count_unique_edges(&g);
333 assert_eq!(
334 n_unique,
335 g.ecount(),
336 "rewire should not introduce multi-edges"
337 );
338 for eid in 0..u32::try_from(g.ecount()).unwrap() {
339 let (a, b) = g.edge(eid).unwrap();
340 assert_ne!(a, b, "no self-loops when loops=false (edge {eid})");
341 }
342 }
343
344 #[test]
345 fn rewire_preserves_edge_count() {
346 for &p in &[0.0, 0.1, 0.3, 0.5, 0.9, 1.0] {
347 let g = watts_strogatz_game(50, 3, p, false, false, 0xABCD).unwrap();
348 assert_eq!(
349 g.ecount(),
350 50 * 3,
351 "edge count should equal size·nei at p={p}"
352 );
353 }
354 }
355
356 #[test]
357 fn simple_output_has_no_loops_or_multi() {
358 let g = watts_strogatz_game(30, 4, 0.5, false, false, 0xDEAD).unwrap();
359 let n_unique = count_unique_edges(&g);
360 assert_eq!(n_unique, g.ecount(), "no multi-edges");
361 for eid in 0..u32::try_from(g.ecount()).unwrap() {
362 let (a, b) = g.edge(eid).unwrap();
363 assert_ne!(a, b, "no self-loops");
364 }
365 }
366
367 #[test]
368 fn multigraph_path_runs_without_panic() {
369 let g = watts_strogatz_game(20, 2, 0.7, true, true, 0xBEEF).unwrap();
371 assert_eq!(g.vcount(), 20);
372 assert_eq!(g.ecount(), 20 * 2);
373 }
374
375 #[test]
376 fn loops_true_multiple_false_may_self_loop() {
377 let g = watts_strogatz_game(10, 1, 1.0, true, false, 0x1357).unwrap();
381 assert_eq!(g.ecount(), 10);
382 let n_unique = count_unique_edges(&g);
385 assert_eq!(n_unique, g.ecount());
386 }
387
388 #[test]
389 fn determinism_same_seed_same_graph() {
390 let a = watts_strogatz_game(30, 3, 0.2, false, false, 123_456).unwrap();
391 let b = watts_strogatz_game(30, 3, 0.2, false, false, 123_456).unwrap();
392 assert_eq!(a.vcount(), b.vcount());
393 assert_eq!(a.ecount(), b.ecount());
394 let ea: Vec<(u32, u32)> = (0..u32::try_from(a.ecount()).unwrap())
395 .map(|eid| {
396 let (u, v) = a.edge(eid).unwrap();
397 if u <= v { (u, v) } else { (v, u) }
398 })
399 .collect();
400 let eb: Vec<(u32, u32)> = (0..u32::try_from(b.ecount()).unwrap())
401 .map(|eid| {
402 let (u, v) = b.edge(eid).unwrap();
403 if u <= v { (u, v) } else { (v, u) }
404 })
405 .collect();
406 let mut sa = ea.clone();
407 sa.sort_unstable();
408 let mut sb = eb.clone();
409 sb.sort_unstable();
410 assert_eq!(sa, sb);
411 }
412
413 #[test]
414 fn different_seeds_diverge() {
415 let a = watts_strogatz_game(30, 3, 0.5, false, false, 1).unwrap();
416 let b = watts_strogatz_game(30, 3, 0.5, false, false, 2).unwrap();
417 let ea: HashSet<(u32, u32)> = (0..u32::try_from(a.ecount()).unwrap())
418 .map(|eid| {
419 let (u, v) = a.edge(eid).unwrap();
420 if u <= v { (u, v) } else { (v, u) }
421 })
422 .collect();
423 let eb: HashSet<(u32, u32)> = (0..u32::try_from(b.ecount()).unwrap())
424 .map(|eid| {
425 let (u, v) = b.edge(eid).unwrap();
426 if u <= v { (u, v) } else { (v, u) }
427 })
428 .collect();
429 assert_ne!(
430 ea, eb,
431 "different seeds should yield different rewired graphs"
432 );
433 }
434
435 #[test]
436 fn size_zero_rejected() {
437 let err = watts_strogatz_game(0, 0, 0.0, false, false, 0).unwrap_err();
438 match err {
439 IgraphError::InvalidArgument(msg) => assert!(msg.contains("size")),
440 other => panic!("unexpected error: {other:?}"),
441 }
442 }
443
444 #[test]
445 fn nei_too_large_rejected() {
446 let err = watts_strogatz_game(6, 3, 0.0, false, false, 0).unwrap_err();
448 match err {
449 IgraphError::InvalidArgument(msg) => assert!(msg.contains("nei")),
450 other => panic!("unexpected error: {other:?}"),
451 }
452 }
453
454 #[test]
455 fn p_out_of_range_rejected() {
456 for &p in &[-0.1_f64, 1.1, f64::NAN, f64::INFINITY] {
457 let err = watts_strogatz_game(10, 2, p, false, false, 0).unwrap_err();
458 matches!(err, IgraphError::InvalidArgument(_));
459 }
460 }
461
462 #[test]
463 fn ring_lattice_2_nei_equals_size_minus_one_is_almost_complete() {
464 let g = watts_strogatz_game(5, 2, 0.0, false, false, 0).unwrap();
467 assert_eq!(g.ecount(), 10); let d = degrees(&g);
469 for deg in d {
470 assert_eq!(deg, 4);
471 }
472 }
473
474 #[test]
475 fn p_zero_seed_independence() {
476 let a = watts_strogatz_game(20, 2, 0.0, false, false, 1).unwrap();
477 let b = watts_strogatz_game(20, 2, 0.0, false, false, 999).unwrap();
478 assert_eq!(a.ecount(), b.ecount());
481 for eid in 0..u32::try_from(a.ecount()).unwrap() {
482 assert_eq!(a.edge(eid).unwrap(), b.edge(eid).unwrap());
483 }
484 }
485}
486
487#[cfg(all(test, feature = "proptest-harness"))]
488mod proptests {
489 use super::*;
490 use proptest::prelude::*;
491 use std::collections::HashSet;
492
493 fn canonical_set(g: &Graph) -> HashSet<(u32, u32)> {
494 let mut s = HashSet::with_capacity(g.ecount());
495 let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
496 for eid in 0..m {
497 let (a, b) = g.edge(eid).expect("edge id in bounds");
498 s.insert(if a <= b { (a, b) } else { (b, a) });
499 }
500 s
501 }
502
503 proptest! {
504 #![proptest_config(ProptestConfig {
505 cases: 64, .. ProptestConfig::default()
506 })]
507
508 #[test]
509 fn invariant_simple_graph_no_loops_no_multi(
510 size in 4u32..30,
511 nei_raw in 1u32..5,
512 p in 0.0_f64..=1.0,
513 seed in any::<u64>(),
514 ) {
515 let nei = std::cmp::min(nei_raw, (size - 1) / 2);
517 prop_assume!(nei >= 1);
518
519 let g = watts_strogatz_game(size, nei, p, false, false, seed).unwrap();
520 prop_assert_eq!(g.vcount(), size);
521 prop_assert_eq!(g.ecount(), (size * nei) as usize);
522
523 let canonical = canonical_set(&g);
525 prop_assert_eq!(canonical.len(), g.ecount());
526 for eid in 0..u32::try_from(g.ecount()).unwrap() {
527 let (a, b) = g.edge(eid).unwrap();
528 prop_assert_ne!(a, b);
529 }
530 }
531
532 #[test]
533 fn invariant_edge_count_preserved_under_any_p(
534 size in 4u32..30,
535 nei_raw in 1u32..5,
536 p in 0.0_f64..=1.0,
537 loops in any::<bool>(),
538 multiple in any::<bool>(),
539 seed in any::<u64>(),
540 ) {
541 let nei = std::cmp::min(nei_raw, (size - 1) / 2);
542 prop_assume!(nei >= 1);
543 let g = watts_strogatz_game(size, nei, p, loops, multiple, seed).unwrap();
544 prop_assert_eq!(g.ecount(), (size * nei) as usize);
545 prop_assert_eq!(g.vcount(), size);
546 }
547
548 #[test]
549 fn invariant_determinism(
550 size in 4u32..30,
551 nei_raw in 1u32..5,
552 p in 0.0_f64..=1.0,
553 loops in any::<bool>(),
554 multiple in any::<bool>(),
555 seed in any::<u64>(),
556 ) {
557 let nei = std::cmp::min(nei_raw, (size - 1) / 2);
558 prop_assume!(nei >= 1);
559 let a = watts_strogatz_game(size, nei, p, loops, multiple, seed).unwrap();
560 let b = watts_strogatz_game(size, nei, p, loops, multiple, seed).unwrap();
561 prop_assert_eq!(a.vcount(), b.vcount());
562 prop_assert_eq!(a.ecount(), b.ecount());
563
564 let mut ea: Vec<(u32, u32)> = (0..u32::try_from(a.ecount()).unwrap())
565 .map(|eid| {
566 let (u, v) = a.edge(eid).unwrap();
567 if u <= v { (u, v) } else { (v, u) }
568 })
569 .collect();
570 let mut eb: Vec<(u32, u32)> = (0..u32::try_from(b.ecount()).unwrap())
571 .map(|eid| {
572 let (u, v) = b.edge(eid).unwrap();
573 if u <= v { (u, v) } else { (v, u) }
574 })
575 .collect();
576 ea.sort_unstable();
577 eb.sort_unstable();
578 prop_assert_eq!(ea, eb);
579 }
580 }
581}