1#![allow(
67 unknown_lints,
68 clippy::cast_possible_truncation,
69 clippy::cast_precision_loss,
70 clippy::cast_sign_loss,
71 clippy::cast_lossless,
72 clippy::float_cmp,
73 clippy::too_many_arguments,
74 clippy::similar_names,
75 clippy::many_single_char_names
76)]
77
78use std::collections::VecDeque;
79
80use crate::core::rng::SplitMix64;
81use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
82
83struct PsumTree {
86 n: usize,
87 bit: Vec<f64>,
88 values: Vec<f64>,
89 total: f64,
90}
91
92impl PsumTree {
93 fn new(n: usize) -> Self {
94 Self {
95 n,
96 bit: vec![0.0; n + 1],
97 values: vec![0.0; n],
98 total: 0.0,
99 }
100 }
101
102 fn set(&mut self, i: usize, v: f64) {
103 let delta = v - self.values[i];
104 self.values[i] = v;
105 self.total += delta;
106 let mut k = i + 1;
107 while k <= self.n {
108 self.bit[k] += delta;
109 k += k & k.wrapping_neg();
110 }
111 }
112
113 fn total(&self) -> f64 {
114 self.total
115 }
116
117 fn search(&self, target: f64) -> usize {
118 if self.n == 0 {
119 return 0;
120 }
121 let mut idx: usize = 0;
122 let mut remaining = target;
123 let mut step = (self.n + 1).next_power_of_two() >> 1;
124 while step > 0 {
125 let next = idx + step;
126 if next <= self.n && self.bit[next] <= remaining {
127 idx = next;
128 remaining -= self.bit[next];
129 }
130 step >>= 1;
131 }
132 idx.min(self.n - 1)
133 }
134}
135
136type HistoryEntry = Option<u32>;
139
140fn weight_from_degree(degree: u32, power: f64, zero_appeal: f64) -> f64 {
141 let d = f64::from(degree);
144 let term = if degree == 0 && power == 0.0 {
145 1.0
146 } else if degree == 0 {
147 0.0
148 } else {
149 d.powf(power)
150 };
151 term + zero_appeal
152}
153
154fn validate(
155 nodes: u32,
156 power: f64,
157 m: u32,
158 outseq: Option<&[u32]>,
159 zero_appeal: f64,
160) -> IgraphResult<()> {
161 if power.is_nan() || !power.is_finite() {
162 return Err(IgraphError::InvalidArgument(format!(
163 "power must be finite (got {power})"
164 )));
165 }
166 if zero_appeal.is_nan() || !zero_appeal.is_finite() {
167 return Err(IgraphError::InvalidArgument(format!(
168 "zero_appeal must be finite (got {zero_appeal})"
169 )));
170 }
171 if zero_appeal < 0.0 {
172 return Err(IgraphError::InvalidArgument(format!(
173 "zero_appeal must be non-negative (got {zero_appeal})"
174 )));
175 }
176 if let Some(seq) = outseq {
177 if seq.len() != nodes as usize {
178 return Err(IgraphError::InvalidArgument(format!(
179 "outseq length ({}) must equal nodes ({nodes})",
180 seq.len()
181 )));
182 }
183 }
184 let _ = m;
185 Ok(())
186}
187
188pub fn recent_degree_game(
201 nodes: u32,
202 power: f64,
203 time_window: u32,
204 m: u32,
205 outseq: Option<&[u32]>,
206 outpref: bool,
207 zero_appeal: f64,
208 directed: bool,
209 seed: u64,
210) -> IgraphResult<Graph> {
211 validate(nodes, power, m, outseq, zero_appeal)?;
212
213 let mut graph = Graph::new(nodes, directed)?;
214 if nodes == 0 || nodes < 2 {
215 return Ok(graph);
216 }
217 if outseq.is_none() && m == 0 {
219 return Ok(graph);
220 }
221 if let Some(seq) = outseq {
223 let total_after_zero: u64 = seq.iter().skip(1).map(|&x| u64::from(x)).sum();
224 if total_after_zero == 0 {
225 return Ok(graph);
226 }
227 }
228
229 let n = nodes as usize;
230 let mut rng = SplitMix64::new(seed);
231 let mut psum = PsumTree::new(n);
232 let mut degree: Vec<u32> = vec![0; n];
233 let mut history: VecDeque<HistoryEntry> = VecDeque::new();
234
235 let edge_capacity = if let Some(seq) = outseq {
237 seq.iter().skip(1).map(|&x| u64::from(x)).sum::<u64>() as usize
238 } else {
239 (n - 1).saturating_mul(m as usize)
240 };
241 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_capacity);
242
243 psum.set(0, zero_appeal);
246 history.push_back(None); for i in 1..n {
249 let no_of_neighbors = if let Some(seq) = outseq {
250 seq[i] as usize
251 } else {
252 m as usize
253 };
254
255 if i >= time_window as usize {
257 while let Some(entry) = history.pop_front() {
258 match entry {
259 None => break,
260 Some(j) => {
261 let ju = j as usize;
262 degree[ju] -= 1;
263 psum.set(ju, weight_from_degree(degree[ju], power, zero_appeal));
264 }
265 }
266 }
267 }
268
269 for _ in 0..no_of_neighbors {
271 let sum = psum.total();
272 let to = if sum > 0.0 {
273 let target = rng.gen_unit() * sum;
274 psum.search(target)
275 } else {
276 let span = i as u64;
278 (rng.next_u64() % span) as usize
279 };
280 let src = u32::try_from(i)
281 .map_err(|_| IgraphError::InvalidArgument("vertex index overflow".into()))?;
282 let dst = u32::try_from(to)
283 .map_err(|_| IgraphError::InvalidArgument("vertex index overflow".into()))?;
284 degree[to] = degree[to]
285 .checked_add(1)
286 .ok_or_else(|| IgraphError::InvalidArgument("degree overflow".into()))?;
287 edges.push((src, dst));
288 history.push_back(Some(dst));
289 }
290 history.push_back(None); let start = edges.len() - no_of_neighbors;
295 for edge in &edges[start..] {
296 let nn = edge.1 as usize;
297 psum.set(nn, weight_from_degree(degree[nn], power, zero_appeal));
298 }
299
300 if outpref {
302 let bump = u32::try_from(no_of_neighbors)
305 .map_err(|_| IgraphError::InvalidArgument("neighbors count overflow".into()))?;
306 degree[i] = degree[i]
307 .checked_add(bump)
308 .ok_or_else(|| IgraphError::InvalidArgument("degree overflow".into()))?;
309 psum.set(i, weight_from_degree(degree[i], power, zero_appeal));
310 } else {
311 psum.set(i, zero_appeal);
312 }
313 }
314
315 graph.add_edges(edges)?;
316 Ok(graph)
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 fn assert_edges_match(g: &Graph, expected_count: u64) {
324 assert_eq!(g.ecount() as u64, expected_count);
325 }
326
327 #[test]
328 fn empty_graph_returns_empty() {
329 let g = recent_degree_game(0, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
330 assert_eq!(g.vcount(), 0);
331 assert_eq!(g.ecount(), 0);
332 }
333
334 #[test]
335 fn single_vertex_no_edges() {
336 let g = recent_degree_game(1, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
337 assert_eq!(g.vcount(), 1);
338 assert_eq!(g.ecount(), 0);
339 }
340
341 #[test]
342 fn m_zero_no_edges() {
343 let g = recent_degree_game(10, 1.0, 5, 0, None, false, 1.0, true, 42).unwrap();
344 assert_eq!(g.vcount(), 10);
345 assert_eq!(g.ecount(), 0);
346 }
347
348 #[test]
349 fn ecount_exact_constant_m() {
350 let n = 50u32;
351 let m = 3u32;
352 let g = recent_degree_game(n, 1.0, 10, m, None, false, 1.0, true, 42).unwrap();
353 assert_eq!(g.vcount(), n);
354 assert_edges_match(&g, u64::from(n - 1) * u64::from(m));
355 }
356
357 #[test]
358 fn ecount_exact_outseq() {
359 let n = 20u32;
360 let outseq: Vec<u32> = (0..n).map(|i| if i == 0 { 0 } else { 2 }).collect();
361 let g = recent_degree_game(n, 1.0, 5, 0, Some(&outseq), false, 1.0, true, 42).unwrap();
362 let expected: u64 = outseq.iter().skip(1).map(|&x| u64::from(x)).sum();
363 assert_edges_match(&g, expected);
364 }
365
366 #[test]
367 fn determinism_same_seed() {
368 let g1 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 123).unwrap();
369 let g2 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 123).unwrap();
370 assert_eq!(g1.vcount(), g2.vcount());
371 assert_eq!(g1.ecount(), g2.ecount());
372 let m = u32::try_from(g1.ecount()).unwrap();
373 for eid in 0..m {
374 assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
375 }
376 }
377
378 #[test]
379 fn seed_divergence() {
380 let g1 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 1).unwrap();
381 let g2 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 2).unwrap();
382 let m = u32::try_from(g1.ecount()).unwrap();
383 let mut diff = 0;
384 for eid in 0..m {
385 if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
386 diff += 1;
387 }
388 }
389 assert!(diff > 0, "two seeds should yield different graphs");
390 }
391
392 #[test]
393 fn no_self_loops_when_positive_appeal() {
394 let g = recent_degree_game(50, 1.0, 5, 3, None, false, 1.0, true, 42).unwrap();
395 let m = u32::try_from(g.ecount()).unwrap();
396 for eid in 0..m {
397 let (s, d) = g.edge(eid).unwrap();
398 assert_ne!(s, d, "edge {eid} is a self-loop");
399 }
400 }
401
402 #[test]
403 fn source_is_always_step_index() {
404 let n = 30u32;
405 let m = 2u32;
406 let g = recent_degree_game(n, 1.0, 5, m, None, false, 1.0, true, 42).unwrap();
407 let edges_n = u32::try_from(g.ecount()).unwrap();
408 for eid in 0..edges_n {
409 let (s, _) = g.edge(eid).unwrap();
410 let step = eid / m + 1; assert_eq!(s, step, "edge {eid} src {s} ≠ step {step}");
412 }
413 }
414
415 #[test]
416 fn target_in_zero_to_step_minus_one() {
417 let g = recent_degree_game(30, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
418 let edges_n = u32::try_from(g.ecount()).unwrap();
419 for eid in 0..edges_n {
420 let (s, d) = g.edge(eid).unwrap();
421 assert!(d < s, "target {d} not in [0, src={s})");
422 }
423 }
424
425 #[test]
426 fn directed_flag_propagates() {
427 let g1 = recent_degree_game(15, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
428 let g2 = recent_degree_game(15, 1.0, 5, 2, None, false, 1.0, false, 42).unwrap();
429 assert!(g1.is_directed());
430 assert!(!g2.is_directed());
431 }
432
433 #[test]
434 fn outpref_changes_graph() {
435 let g1 = recent_degree_game(40, 1.5, 10, 2, None, false, 1.0, true, 42).unwrap();
436 let g2 = recent_degree_game(40, 1.5, 10, 2, None, true, 1.0, true, 42).unwrap();
437 assert_eq!(g1.ecount(), g2.ecount());
440 let edges_n = u32::try_from(g1.ecount()).unwrap();
441 let mut diff = 0;
442 for eid in 0..edges_n {
443 if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
444 diff += 1;
445 }
446 }
447 assert!(
448 diff > 0,
449 "outpref=true and outpref=false should yield different graphs"
450 );
451 }
452
453 #[test]
454 fn time_window_zero_uses_zero_appeal_only() {
455 let g = recent_degree_game(20, 1.0, 0, 2, None, false, 1.0, true, 42).unwrap();
459 assert_eq!(g.ecount(), 19 * 2);
461 let edges_n = u32::try_from(g.ecount()).unwrap();
463 for eid in 0..edges_n {
464 let (s, d) = g.edge(eid).unwrap();
465 assert_ne!(s, d);
466 }
467 }
468
469 #[test]
470 fn large_time_window_concentrates_on_early_vertices() {
471 let n = 200u32;
474 let m = 3u32;
475 let g = recent_degree_game(n, 1.0, n, m, None, true, 1.0, true, 42).unwrap();
476 let mut indeg = vec![0u32; n as usize];
477 let edges_n = u32::try_from(g.ecount()).unwrap();
478 for eid in 0..edges_n {
479 let (_, d) = g.edge(eid).unwrap();
480 indeg[d as usize] += 1;
481 }
482 let early_cut = (n as usize) / 10;
484 let early_sum: u32 = indeg[..early_cut].iter().sum();
485 let total: u32 = indeg.iter().sum();
486 assert!(
487 early_sum * 5 > total,
488 "expected early-vertex concentration (early_sum={early_sum}, total={total})"
489 );
490 }
491
492 #[test]
493 fn validation_outseq_wrong_length() {
494 let outseq = vec![0u32, 1, 2];
495 let err =
496 recent_degree_game(10, 1.0, 5, 0, Some(&outseq), false, 1.0, true, 42).unwrap_err();
497 match err {
498 IgraphError::InvalidArgument(s) => assert!(s.contains("outseq length")),
499 other => panic!("expected InvalidArgument, got {other:?}"),
500 }
501 }
502
503 #[test]
504 fn validation_nan_power() {
505 let err = recent_degree_game(10, f64::NAN, 5, 2, None, false, 1.0, true, 42).unwrap_err();
506 match err {
507 IgraphError::InvalidArgument(s) => assert!(s.contains("power must be finite")),
508 other => panic!("expected InvalidArgument, got {other:?}"),
509 }
510 }
511
512 #[test]
513 fn validation_inf_power() {
514 let err =
515 recent_degree_game(10, f64::INFINITY, 5, 2, None, false, 1.0, true, 42).unwrap_err();
516 match err {
517 IgraphError::InvalidArgument(s) => assert!(s.contains("power must be finite")),
518 other => panic!("expected InvalidArgument, got {other:?}"),
519 }
520 }
521
522 #[test]
523 fn validation_negative_zero_appeal() {
524 let err = recent_degree_game(10, 1.0, 5, 2, None, false, -0.5, true, 42).unwrap_err();
525 match err {
526 IgraphError::InvalidArgument(s) => {
527 assert!(s.contains("zero_appeal must be non-negative"));
528 }
529 other => panic!("expected InvalidArgument, got {other:?}"),
530 }
531 }
532
533 #[test]
534 fn validation_nan_zero_appeal() {
535 let err = recent_degree_game(10, 1.0, 5, 2, None, false, f64::NAN, true, 42).unwrap_err();
536 match err {
537 IgraphError::InvalidArgument(s) => assert!(s.contains("zero_appeal must be finite")),
538 other => panic!("expected InvalidArgument, got {other:?}"),
539 }
540 }
541
542 #[test]
543 fn weight_from_degree_zero_power_zero() {
544 let w = weight_from_degree(0, 0.0, 0.5);
546 assert_eq!(w, 1.5);
547 }
548
549 #[test]
550 fn weight_from_degree_zero_positive_power() {
551 let w = weight_from_degree(0, 1.0, 0.5);
552 assert_eq!(w, 0.5);
553 }
554
555 #[test]
556 fn weight_from_degree_nonzero() {
557 let w = weight_from_degree(4, 0.5, 1.0);
558 assert!((w - 3.0).abs() < 1e-12);
559 }
560}
561
562#[cfg(all(test, feature = "proptest-harness"))]
563mod proptests {
564 use super::*;
565 use proptest::prelude::*;
566
567 proptest! {
568 #[test]
569 fn determinism_under_proptest(
570 nodes in 1u32..50,
571 m in 0u32..4,
572 power in 0.0f64..3.0,
573 zero_appeal in 0.1f64..5.0,
574 time_window in 1u32..20,
575 directed in any::<bool>(),
576 outpref in any::<bool>(),
577 seed in any::<u64>(),
578 ) {
579 let g1 = recent_degree_game(nodes, power, time_window, m, None, outpref, zero_appeal, directed, seed).unwrap();
580 let g2 = recent_degree_game(nodes, power, time_window, m, None, outpref, zero_appeal, directed, seed).unwrap();
581 prop_assert_eq!(g1.vcount(), g2.vcount());
582 prop_assert_eq!(g1.ecount(), g2.ecount());
583 let edges_n = u32::try_from(g1.ecount()).unwrap();
584 for eid in 0..edges_n {
585 prop_assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
586 }
587 }
588
589 #[test]
590 fn ecount_exact_constant_m(
591 nodes in 1u32..40,
592 m in 0u32..5,
593 time_window in 1u32..20,
594 seed in any::<u64>(),
595 ) {
596 let g = recent_degree_game(nodes, 1.0, time_window, m, None, false, 1.0, true, seed).unwrap();
597 let expected = if nodes < 2 { 0 } else { u64::from(nodes - 1) * u64::from(m) };
598 prop_assert_eq!(g.ecount() as u64, expected);
599 }
600
601 #[test]
602 fn no_self_loops_when_zero_appeal_positive(
603 nodes in 2u32..40,
604 m in 1u32..4,
605 power in 0.0f64..2.0,
606 zero_appeal in 0.1f64..3.0,
607 time_window in 1u32..20,
608 seed in any::<u64>(),
609 ) {
610 let g = recent_degree_game(nodes, power, time_window, m, None, false, zero_appeal, true, seed).unwrap();
611 let edges_n = u32::try_from(g.ecount()).unwrap();
612 for eid in 0..edges_n {
613 let (s, d) = g.edge(eid).unwrap();
614 prop_assert_ne!(s, d);
615 }
616 }
617
618 #[test]
619 fn source_is_always_step_index(
620 nodes in 2u32..30,
621 m in 1u32..4,
622 seed in any::<u64>(),
623 ) {
624 let g = recent_degree_game(nodes, 1.0, 5, m, None, false, 1.0, true, seed).unwrap();
625 let edges_n = u32::try_from(g.ecount()).unwrap();
626 for eid in 0..edges_n {
627 let (s, _) = g.edge(eid).unwrap();
628 let step = eid / m + 1;
629 prop_assert_eq!(s, step);
630 }
631 }
632
633 #[test]
634 fn target_in_zero_to_step_minus_one(
635 nodes in 2u32..30,
636 m in 1u32..4,
637 seed in any::<u64>(),
638 ) {
639 let g = recent_degree_game(nodes, 1.0, 5, m, None, false, 1.0, true, seed).unwrap();
640 let edges_n = u32::try_from(g.ecount()).unwrap();
641 for eid in 0..edges_n {
642 let (s, d) = g.edge(eid).unwrap();
643 prop_assert!(d < s);
644 }
645 }
646 }
647}