Skip to main content

dupehell_core/
rng.rs

1// DupeHell -- MIT License
2//
3// Synthetic multi-domain dataset generator for record linkage benchmarking.
4// EDUCATIONAL AND RESEARCH PURPOSES ONLY -- see ETHICS.md for prohibited uses.
5// No liability for misuse.
6
7use rand::RngCore;
8use rand::SeedableRng;
9use rand_pcg::Pcg64Mcg;
10
11/// A thin wrapper around PCG64 for deterministic generation.
12///
13/// Python passes a master seed; this RNG can produce sub-seeds for
14/// parallel batches via `fork()`.
15#[derive(Clone, Debug)]
16pub struct Rng {
17    inner: Pcg64Mcg,
18}
19
20impl Rng {
21    /// Create a new RNG from a 64-bit seed.
22    pub fn new(seed: u64) -> Self {
23        Self {
24            inner: Pcg64Mcg::seed_from_u64(seed),
25        }
26    }
27
28    /// Return a random `u64`.
29    pub fn next_u64(&mut self) -> u64 {
30        self.inner.next_u64()
31    }
32
33    /// Return a random `usize` in `0..bound`.
34    ///
35    /// Uses Lemire's multiply-shift method instead of `% bound`, which would
36    /// otherwise skew low values for any `bound` that doesn't divide 2^64.
37    pub fn next_usize(&mut self, bound: usize) -> usize {
38        if bound <= 1 {
39            return 0;
40        }
41        ((self.next_u64() as u128 * bound as u128) >> 64) as usize
42    }
43
44    /// Return a random `f64` in `[0, 1)`.
45    pub fn next_f64(&mut self) -> f64 {
46        // convert to [0, 1) — 53 bits of precision
47        (self.next_u64() >> 11) as f64 * (1.0 / 9007199254740992.0)
48    }
49
50    /// Fork: create a fresh RNG seeded with a value from this one.
51    /// Useful for spawning per-batch or per-column RNGs.
52    pub fn fork(&mut self) -> Self {
53        Self::new(self.next_u64())
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_deterministic() {
63        let mut a = Rng::new(42);
64        let mut b = Rng::new(42);
65        for _ in 0..100 {
66            assert_eq!(a.next_u64(), b.next_u64());
67        }
68    }
69
70    #[test]
71    fn test_fork_differs() {
72        let mut parent = Rng::new(42);
73        let mut child = parent.fork();
74        // parent and child should produce different sequences
75        assert_ne!(parent.next_u64(), child.next_u64());
76    }
77
78    #[test]
79    fn test_next_usize_bounded() {
80        let mut rng = Rng::new(0);
81        for _ in 0..1000 {
82            let v = rng.next_usize(10);
83            assert!(v < 10, "value {v} out of range");
84        }
85    }
86
87    #[test]
88    fn test_next_f64_range() {
89        let mut rng = Rng::new(1);
90        for _ in 0..1000 {
91            let v = rng.next_f64();
92            assert!((0.0..1.0).contains(&v), "value {v} out of [0,1)");
93        }
94    }
95}