bbx_core/
random.rs

1/// Used for generating pseudo-random numbers
2/// quickly.
3pub struct XorShiftRng {
4    state: u64,
5}
6
7impl XorShiftRng {
8    /// Create an instance of the `XorShiftRng` pseudo-random
9    /// number generator with a specific seed.
10    pub fn new(seed: u64) -> Self {
11        Self { state: seed.max(1) }
12    }
13
14    /// Generate the next pseudo-random number.
15    pub fn next_noise_sample(&mut self) -> f64 {
16        self.state ^= self.state << 13;
17        self.state ^= self.state >> 7;
18        self.state ^= self.state << 17;
19
20        ((self.state as f64) / (u64::MAX as f64)) * 2.0 - 1.0
21    }
22}
23
24impl Default for XorShiftRng {
25    /// Create a default instance of the `XorShiftRng`
26    /// pseudo-random number generator.
27    fn default() -> Self {
28        Self { state: 1 }
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    #[test]
37    fn test_output_range() {
38        let mut rng = XorShiftRng::new(12345);
39        for _ in 0..10000 {
40            let sample = rng.next_noise_sample();
41            assert!(
42                sample >= -1.0 && sample <= 1.0,
43                "Sample {} out of [-1, 1] range",
44                sample
45            );
46        }
47    }
48
49    #[test]
50    fn test_deterministic_with_same_seed() {
51        let mut rng1 = XorShiftRng::new(42);
52        let mut rng2 = XorShiftRng::new(42);
53
54        for _ in 0..100 {
55            let s1 = rng1.next_noise_sample();
56            let s2 = rng2.next_noise_sample();
57            assert!((s1 - s2).abs() < 1e-15, "Same seed should produce identical sequences");
58        }
59    }
60
61    #[test]
62    fn test_different_seeds_produce_different_sequences() {
63        let mut rng1 = XorShiftRng::new(1);
64        let mut rng2 = XorShiftRng::new(2);
65
66        let mut all_same = true;
67        for _ in 0..100 {
68            let s1 = rng1.next_noise_sample();
69            let s2 = rng2.next_noise_sample();
70            if (s1 - s2).abs() > 1e-15 {
71                all_same = false;
72                break;
73            }
74        }
75        assert!(!all_same, "Different seeds should produce different sequences");
76    }
77
78    #[test]
79    fn test_zero_seed_handled() {
80        let mut rng = XorShiftRng::new(0);
81        let sample = rng.next_noise_sample();
82        assert!(
83            sample >= -1.0 && sample <= 1.0,
84            "Zero seed should work (converted to 1)"
85        );
86    }
87
88    #[test]
89    fn test_default_produces_valid_output() {
90        let mut rng = XorShiftRng::default();
91        for _ in 0..100 {
92            let sample = rng.next_noise_sample();
93            assert!(sample >= -1.0 && sample <= 1.0);
94        }
95    }
96
97    #[test]
98    fn test_default_matches_seed_one() {
99        let mut rng_default = XorShiftRng::default();
100        let mut rng_one = XorShiftRng::new(1);
101
102        for _ in 0..100 {
103            let s1 = rng_default.next_noise_sample();
104            let s2 = rng_one.next_noise_sample();
105            assert!((s1 - s2).abs() < 1e-15, "Default should match seed=1");
106        }
107    }
108
109    #[test]
110    fn test_not_constant() {
111        let mut rng = XorShiftRng::new(42);
112        let first = rng.next_noise_sample();
113        let mut found_different = false;
114
115        for _ in 0..100 {
116            let sample = rng.next_noise_sample();
117            if (sample - first).abs() > 1e-10 {
118                found_different = true;
119                break;
120            }
121        }
122        assert!(found_different, "RNG should produce varying values");
123    }
124
125    #[test]
126    fn test_distribution_rough_balance() {
127        let mut rng = XorShiftRng::new(54321);
128        let mut positive_count = 0;
129        let mut negative_count = 0;
130        let num_samples = 10000;
131
132        for _ in 0..num_samples {
133            let sample = rng.next_noise_sample();
134            if sample > 0.0 {
135                positive_count += 1;
136            } else if sample < 0.0 {
137                negative_count += 1;
138            }
139        }
140
141        let ratio = positive_count as f64 / negative_count as f64;
142        assert!(
143            ratio > 0.8 && ratio < 1.25,
144            "Distribution should be roughly balanced: pos={}, neg={}, ratio={}",
145            positive_count,
146            negative_count,
147            ratio
148        );
149    }
150}