unirand/
lib.rs

1//! # Unirand Crate
2//!
3//! This crate implements Marsaglia's Universal Random Number Generator.
4//! [More details on the original paper](https://www.sciencedirect.com/science/article/abs/pii/016771529090092L).
5//! 
6//! ## Overview
7//!
8//! The RNG uses a sequence of operations to generate uniformly distributed
9//! random numbers between 0 and 1. It has been designed with simplicity and 
10//! reproducibility in mind.
11//!
12//! ## Usage Instructions
13//!
14//! To use this crate, add the following dependency to your `Cargo.toml`:
15//!
16//! ```toml
17//! [dependencies]
18//! unirand = "0.1.2"
19//! ```
20//!
21//! Then, you can initialise and use the RNG in your project as follows:
22//!
23//! ```rust
24//!     use unirand::MarsagliaUniRng;
25
26//!     let mut rng = MarsagliaUniRng::new();
27//!     rng.rinit(170);
28//!     println!("Random number: {}", rng.uni());
29//! ```
30//!
31//! ## Further Information
32//!
33//! See the documentation for individual functions and methods below for more details.
34
35const LEN_U: usize = 98; // Length of the random values array.
36
37/// A struct representing Marsaglia's Universal Random Number Generator.
38pub struct MarsagliaUniRng {
39    uni_u: [f32; LEN_U], // Array holding the recent random numbers.
40    uni_c: f32,          // Correction to avoid periodicity.
41    uni_cd: f32,         // Correction delta value.
42    uni_cm: f32,         // Correction modulus.
43    uni_ui: usize,       // Current position in the random values array.
44    uni_uj: usize,       // Second index used for generating new numbers.
45}
46
47impl Default for MarsagliaUniRng {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl MarsagliaUniRng {
54    /// Creates a new instance of the RNG.
55    ///
56    /// # Example
57    ///
58    /// ```rust
59    /// use unirand::MarsagliaUniRng;
60    ///
61    /// let rng = MarsagliaUniRng::new();
62    /// ```
63    pub fn new() -> Self {
64        Self {
65            uni_u: [0.0; LEN_U],
66            uni_c: 0.0,
67            uni_cd: 0.0,
68            uni_cm: 0.0,
69            uni_ui: 0,
70            uni_uj: 0,
71        }
72    }
73
74    /// Generates a new random float value between 0 and 1.
75    ///
76    /// # Example
77    ///
78    /// ```rust
79    /// use unirand::MarsagliaUniRng;
80    ///
81    /// let mut rng = MarsagliaUniRng::new();
82    /// rng.rinit(170);
83    /// let number = rng.uni();
84    /// println!("Random number: {}", number);
85    /// ```
86    pub fn uni(&mut self) -> f32 {
87        let mut luni = self.uni_u[self.uni_ui] - self.uni_u[self.uni_uj];
88        if luni < 0.0 {
89            luni += 1.0;
90        }
91        self.uni_u[self.uni_ui] = luni;
92        
93        // Adjust indices for the next random number generation.
94        if self.uni_ui == 0 {
95            self.uni_ui = 97;
96        } else {
97            self.uni_ui -= 1;
98        }
99        if self.uni_uj == 0 {
100            self.uni_uj = 97;
101        } else {
102            self.uni_uj -= 1;
103        }
104    
105        self.uni_c -= self.uni_cd;
106        if self.uni_c < 0.0 {
107            self.uni_c += self.uni_cm;
108        }
109    
110        luni -= self.uni_c;
111        if luni < 0.0 {
112            luni += 1.0;
113        }
114        luni
115    }
116
117    /// Initialises the random values array using four seeds.
118    ///
119    /// # Parameters
120    ///
121    /// - `i`, `j`, `k`, `l`: The seed values used for initialisation.
122    pub fn rstart(&mut self, mut i: i32, mut j: i32, mut k: i32, mut l: i32) {
123        for ii in 1..=97 {
124            let mut s = 0.0;
125            let mut t = 0.5;
126            for _ in 1..=24 {
127                let m = ((i * j % 179) * k) % 179;
128                i = j;
129                j = k;
130                k = m;
131                l = (53 * l + 1) % 169;
132                if l * m % 64 >= 32 {
133                    s += t;
134                }
135                t *= 0.5;
136            }
137            self.uni_u[ii] = s;
138        }
139        // Set fixed correction values.
140        self.uni_c = 362436.0 / 16777216.0;
141        self.uni_cd = 7654321.0 / 16777216.0;
142        self.uni_cm = 16777213.0 / 16777216.0;
143        self.uni_ui = 97;
144        self.uni_uj = 33;
145    }
146
147    /// Validates and decomposes a single seed into four seeds, then initialises the random values array.
148    ///
149    /// # Panics
150    ///
151    /// Panics if the seed (`ijkl`) is out of range or if the generated seeds are invalid.
152    pub fn rinit(&mut self, ijkl: i32) {
153        if !(0..=900_000_000).contains(&ijkl) {
154            panic!("rinit: ijkl = {ijkl} -- out of range");
155        }
156
157        let ij = ijkl / 30082;
158        let kl = ijkl - (30082 * ij);
159        let i = ((ij / 177) % 177) + 2;
160        let j = (ij % 177) + 2;
161        let k = ((kl / 169) % 178) + 1;
162        let l = kl % 169;
163
164        if !(1..=178).contains(&i) {
165            panic!("rinit: i = {i} -- out of range");
166        }
167        if !(2..=178).contains(&j) {
168            panic!("rinit: j = {j} -- out of range");
169        }
170        if !(1..=178).contains(&k) {
171            panic!("rinit: k = {k} -- out of range");
172        }
173        if !(0..=168).contains(&l) {
174            panic!("rinit: l = {l} -- out of range");
175        }
176        if i == 1 && j == 1 && k == 1 {
177            panic!("rinit: 1 1 1 not allowed for 1st 3 seeds");
178        }
179
180        self.rstart(i, j, k, l);
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::MarsagliaUniRng;
187
188    /// This test checks that a known valid seed produces the expected output.
189    #[test]
190    fn test_rng_output() {
191        let mut rng = MarsagliaUniRng::new();
192        rng.rinit(170);
193        let random_value = rng.uni();
194        let expected = 0.68753344;
195        let tolerance = 1e-6;
196        assert!(
197            (random_value - expected).abs() < tolerance,
198            "Expected {}, got {}",
199            expected,
200            random_value
201        );
202    }
203
204    /// This test verifies that out-of-range seeds cause expected panics.
205    #[test]
206    #[should_panic(expected = "rinit: ijkl = -1 -- out of range")]
207    fn test_rinit_panics_low_seed() {
208        let mut rng = MarsagliaUniRng::new();
209        rng.rinit(-1); // Below valid range
210    }
211
212    #[test]
213    #[should_panic(expected = "rinit: ijkl = 900000001 -- out of range")]
214    fn test_rinit_panics_high_seed() {
215        let mut rng = MarsagliaUniRng::new();
216        rng.rinit(900_000_001); // Above valid range
217    }
218
219    /// This is a Statistical Quality Test (SQT) to ensure the RNG produces a uniform distribution.
220    #[test]
221    fn test_rng_statistics() {
222        let mut rng = MarsagliaUniRng::new();
223        rng.rinit(170);
224        let n = 10_000;
225        let sum: f32 = (0..n).map(|_| rng.uni()).sum();
226        let mean = sum / n as f32;
227        assert!(
228            (mean - 0.5).abs() < 0.01,
229            "Mean out of expected range: {}",
230            mean
231        );
232    }
233
234    /// This test checks for reproducibility with repeated initialisations using the same seed.
235    #[test]
236    fn test_rng_reproducibility() {
237        let mut rng1 = MarsagliaUniRng::new();
238        let mut rng2 = MarsagliaUniRng::new();
239        rng1.rinit(42);
240        rng2.rinit(42);
241        for _ in 0..100 {
242            assert!((rng1.uni() - rng2.uni()).abs() < 1e-7);
243        }
244    }
245}