rand_sequence/
sequence.rs

1use crate::builder::{QuadraticResidue, RandomSequenceBuilder};
2
3/// Generate a deterministic pseudo-random sequence of unique numbers.
4///
5/// Not cryptographically secure.
6///
7/// Properties:
8/// - The sequence is deterministic and repeatable.
9/// - The sequence will only include each number once (every index is unique).
10/// - Computing the value for any random index in the sequence is an O(1) operation.
11///
12/// Based on the article by @preshing:
13/// Article: http://preshing.com/20121224/how-to-generate-a-sequence-of-unique-random-integers/
14/// Source: https://github.com/preshing/RandomSequence/blob/master/randomsequence.h
15#[derive(Debug, Clone)]
16pub struct RandomSequence<T>
17where
18    T: QuadraticResidue
19{
20    /// The config/builder holds the parameters that define the sequence.
21    pub config: RandomSequenceBuilder<T>,
22
23    /// Internal iterator-only state.
24    pub(crate) start_index: T,
25    pub(crate) current_index: T,
26    pub(crate) intermediate_offset: T,
27
28    /// The end marker, required for the ExactSizeIterator so that we terminate correctly.
29    pub(crate) ended: bool,
30}
31
32impl<T> RandomSequence<T>
33where
34    T: QuadraticResidue
35{
36    /// Get the next element in the sequence.
37    #[inline]
38    pub fn next(&mut self) -> Option<T> {
39        let next = self.n_internal(self.start_index.wrapping_add(&self.current_index));
40        self.current_index = match self.current_index.checked_add(&T::one()) {
41            Some(v) => {
42                self.ended = false;
43                v
44            },
45            None => {
46                if !self.ended {
47                    self.ended = true;
48                    self.current_index
49                } else {
50                    return None
51                }
52            },
53        };
54        Some(next)
55    }
56
57    /// Get the next element in the sequence, cycling the sequence once we reach the end.
58    ///
59    /// This will ignore the internal [RandomSequence::ended] marker, and potentially confuse an
60    /// exact size iterator if it had reached the end.
61    #[inline]
62    pub fn wrapping_next(&mut self) -> T {
63        let next = self.n_internal(self.start_index.wrapping_add(&self.current_index));
64        self.current_index = self.current_index.wrapping_add(&T::one());
65        next
66    }
67
68    /// Get the previous element in the sequence.
69    #[inline]
70    pub fn prev(&mut self) -> Option<T> {
71        // decrement then compute, opposite to next()
72        self.current_index = match self.current_index.checked_sub(&T::one()) {
73            Some(v) => v,
74            None => return None,
75        };
76        self.ended = false;
77        Some(self.n_internal(self.start_index.wrapping_add(&self.current_index)))
78    }
79
80    /// Get the previous element in the sequence, cycling the sequence once we reach the start.
81    #[inline]
82    pub fn wrapping_prev(&mut self) -> T {
83        // decrement then compute, opposite to next()
84        self.current_index = self.current_index.wrapping_sub(&T::one());
85        self.n_internal(self.start_index.wrapping_add(&self.current_index))
86    }
87
88    /// Get the nth element in the sequence.
89    #[inline]
90    pub fn n(&self, index: T) -> T {
91        let actual_index = self.start_index.wrapping_add(&index);
92        self.n_internal(actual_index)
93    }
94
95    /// Get the nth element in the sequence, but using the absolute index rather than relative to `start_index`.
96    ///
97    /// `qpr(qpr(index + intermediate_offset) ^ intermediate_xor)`
98    #[inline(always)]
99    fn n_internal(&self, index: T) -> T {
100        let inner_residue = self.config.permute_qpr(index).wrapping_add(&self.intermediate_offset);
101        self.config.permute_qpr(inner_residue ^ self.config.intermediate_xor)
102    }
103
104    /// Get the current position in the sequence. Will return `None` if the sequence has been exhausted.
105    #[inline]
106    pub fn index(&self) -> Option<T> {
107        match self.ended {
108            false => Some(self.current_index),
109            true => None,
110        }
111    }
112
113    /// Check if this sequence has been exhausted.
114    #[inline]
115    pub fn exhausted(&self) -> bool {
116        self.ended
117    }
118
119    /// Set the index for the iterator.
120    ///
121    /// If the iterator was exhausted, this will reset it to the index set.
122    #[inline]
123    pub fn set_index(&mut self, index: T) {
124        self.current_index = index;
125        self.ended = false;
126    }
127}
128
129macro_rules! impl_unsized_iterator {
130    ($T:ident) => {
131        impl Iterator for RandomSequence<$T> {
132            type Item = $T;
133
134            #[inline]
135            fn next(&mut self) -> Option<Self::Item> {
136                self.next()
137            }
138
139            #[inline]
140            fn size_hint(&self) -> (usize, Option<usize>) {
141                ($T::MAX as usize - self.current_index as usize, None)
142            }
143        }
144    };
145}
146
147macro_rules! impl_exact_size_iterator {
148    ($T:ident) => {
149        impl Iterator for RandomSequence<$T> {
150            type Item = $T;
151
152            #[inline]
153            fn next(&mut self) -> Option<Self::Item> {
154                self.next()
155            }
156
157            #[inline]
158            fn size_hint(&self) -> (usize, Option<usize>) {
159                ($T::MAX as usize + 1 - self.current_index as usize, Some($T::MAX as usize + 1 - self.current_index as usize))
160            }
161        }
162
163        impl ExactSizeIterator for RandomSequence<$T> {}
164    };
165}
166
167// Can only fit exact size iterators in types smaller than usize. As usize will have usize+1 elements.
168impl_exact_size_iterator!(u8);
169impl_exact_size_iterator!(u16);
170#[cfg(target_pointer_width = "64")]
171impl_exact_size_iterator!(u32);
172#[cfg(target_pointer_width = "32")]
173impl_unsized_iterator!(u32);
174impl_unsized_iterator!(u64);
175impl_unsized_iterator!(usize);
176
177impl<T> DoubleEndedIterator for RandomSequence<T>
178where
179    T: QuadraticResidue,
180    RandomSequence<T>: Iterator<Item = T>,
181{
182    #[inline]
183    fn next_back(&mut self) -> Option<Self::Item> {
184        self.prev()
185    }
186}
187
188impl<T> From<RandomSequenceBuilder<T>> for RandomSequence<T>
189where
190    T: QuadraticResidue,
191    RandomSequence<T>: Iterator<Item = T>,
192{
193    fn from(value: RandomSequenceBuilder<T>) -> Self {
194        value.into_iter()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use std::collections::{HashMap, HashSet};
201    use std::vec::Vec;
202
203    use rand::rngs::OsRng;
204    use statrs::distribution::{ChiSquared, ContinuousCDF};
205
206    use super::*;
207
208    fn is_send<T: Send>() {}
209    fn is_sync<T: Sync>() {}
210
211    macro_rules! test_sequence {
212        ($name:ident, $type:ident, $check:literal) => {
213            #[test]
214            fn $name() {
215                let config = RandomSequenceBuilder::<$type>::new(0, 0);
216                let sequence = config.into_iter();
217
218                for (i, num) in std::iter::zip(0..10, sequence.clone()) {
219                    assert_eq!(sequence.n(i as $type), num);
220                }
221
222                for (i, num) in std::iter::zip(0..10, sequence.clone().rev()) {
223                    assert_eq!(sequence.n($type::MAX.wrapping_sub(i as $type)), num);
224                }
225
226                // check the exact size iterator ends correctly for u8 and u16
227                if ($type::MAX as usize) < $check {
228                    let nums_vec: Vec<$type> = config.into_iter().take($check + 10).collect();
229                    assert_eq!(nums_vec.len(), $type::MAX as usize + 1);
230                }
231
232                // check that we see each value only once
233                let nums: HashSet<$type> = config.into_iter().take($check).collect();
234                assert_eq!(nums.len(), $check);
235
236                // check exhaustion
237                {
238                    let mut sequence = config.into_iter();
239
240                    // initial index = 0
241                    assert!(!sequence.exhausted());
242                    assert_eq!(sequence.index(), Some(0));
243                    let first = sequence.next().unwrap();
244
245                    // final index
246                    sequence.set_index($type::MAX);
247                    assert!(!sequence.exhausted());
248                    assert_eq!(sequence.index(), Some($type::MAX));
249                    let last = sequence.next().unwrap();
250
251                    // exhausted
252                    assert!(sequence.exhausted());
253                    assert_eq!(sequence.index(), None);
254                    assert!(sequence.next().is_none());
255
256                    // reset
257                    sequence.set_index($type::MAX);
258                    assert!(!sequence.exhausted());
259                    assert_eq!(sequence.index(), Some($type::MAX));
260                    assert_eq!(sequence.next(), Some(last));
261                    assert!(sequence.exhausted());  // gets exhausted again
262
263                    // reset to 0
264                    sequence.set_index(0);
265                    assert!(!sequence.exhausted());
266                    assert_eq!(sequence.index(), Some(0));
267                    assert_eq!(sequence.next(), Some(first));
268                }
269
270                // check sequence is send and sync (although index won't be synced between threads)
271                is_send::<RandomSequence<$type>>();
272                is_sync::<RandomSequence<$type>>();
273            }
274        };
275    }
276
277    test_sequence!(test_u8_sequence, u8, 256);
278    test_sequence!(test_u16_sequence, u16, 65536);
279    test_sequence!(test_u32_sequence, u32, 100_000);
280    test_sequence!(test_u64_sequence, u64, 100_000);
281    test_sequence!(test_usize_sequence, usize, 100_000);
282
283    macro_rules! test_exact_size_iterator {
284        ($name:ident, $type:ident) => {
285            #[test]
286            fn $name() {
287                let config = RandomSequenceBuilder::<$type>::new(0, 0);
288                let mut sequence = config.clone().into_iter();
289                assert_eq!(sequence.len(), $type::MAX as usize + 1);  // eg. 256 items, but u8::MAX = 255
290                sequence.next();
291                assert_eq!(sequence.len(), $type::MAX as usize);
292                sequence.next();
293                assert_eq!(sequence.len(), $type::MAX as usize - 1);
294                sequence.next();
295                assert_eq!(sequence.len(), $type::MAX as usize - 2);
296                sequence.prev();
297                assert_eq!(sequence.len(), $type::MAX as usize - 1);
298
299                sequence.next();
300                sequence.next();
301                sequence.next();
302                assert_eq!(sequence.len(), $type::MAX as usize - 4);
303
304                // don't collect a u32, only test this on u8 and u16
305                if sequence.len() <= u16::MAX as usize + 1 {
306                    let remaining_len = sequence.len();
307                    let remaining_items: Vec<_> = sequence.collect();
308                    assert_eq!(remaining_len, remaining_items.len());
309
310                    // double check a fresh sequence
311                    let sequence = config.into_iter();
312                    assert_eq!(sequence.len(), $type::MAX as usize + 1);
313                    let all_items: Vec<_> = sequence.collect();
314                    assert_eq!(all_items.len(), $type::MAX as usize + 1);
315                    assert_eq!(all_items.len(), remaining_items.len() + 5);
316                }
317            }
318        };
319    }
320
321    test_exact_size_iterator!(test_u8_exact_size_iterator, u8);
322    test_exact_size_iterator!(test_u16_exact_size_iterator, u16);
323    #[cfg(target_pointer_width = "64")]
324    test_exact_size_iterator!(test_u32_exact_size_iterator, u32);
325
326    macro_rules! test_distribution {
327        ($name:ident, $type:ident, $check:literal) => {
328            #[ignore]  // ChiSquared p value is too unreliable
329            #[test]
330            fn $name() {
331                const BUCKETS: usize = 100;
332                let config = RandomSequenceBuilder::<$type>::rand(&mut OsRng);
333
334                // compute a normalised histogram over the sequence with BUCKETS buckets, where each bucket value
335                // is the percentage of values that fall into this bucket
336                let mut data_buckets: HashMap<usize, usize> = HashMap::with_capacity(BUCKETS + 1);
337                config
338                    .into_iter()
339                    .take($check)
340                    .map(|i| ((i as f64 / $type::MAX as f64) * BUCKETS as f64) as usize)
341                    .for_each(|i| *data_buckets.entry(i).or_insert(0) += 1);
342                let data_buckets: Vec<f64> = (0..=BUCKETS)
343                    .map(|i| *data_buckets.get(&i).unwrap_or(&0) as f64)
344                    .collect();
345
346                // compute the probability of each bucket being hit, assuming a uniform distribution.
347                // careful for u8 where we have 256 for only 100 buckets; and so some buckets have 2 vs 3 expected values,
348                // as this represents the percentage of values that should fall into each bucket assuming perfectly uniform.
349                let mut uniform_buckets: Vec<f64> = (0..BUCKETS)
350                    .map(|_| ($check as f64 / BUCKETS as f64))
351                    .collect();
352                uniform_buckets.push($check as f64 / $type::MAX as f64); // last bucket for value=$type::MAX
353
354                // compute chi-squared statistic
355                assert_eq!(data_buckets.len(), uniform_buckets.len(), "Data and uniform buckets logic issue.");
356                let chi_squared = std::iter::zip(data_buckets.iter(), uniform_buckets.iter())
357                    .map(|(x, e)| (x - e).powi(2) / e)
358                    .sum::<f64>();
359
360                // compute p_value from chi-squared statistic
361                let chi_dist = ChiSquared::new((BUCKETS - 1) as f64).unwrap();
362                let p_value = 1.0 - chi_dist.cdf(chi_squared);
363
364                // FIXME: choose a better test, because this doesn't strictly confirm the uniform distribution
365                //   and there is a suspiciously large amount of variance in the p_values between test runs.
366                // p_value <= 0.05 would say with 95% certainty that this distribution is _not_ uniform
367                assert!(p_value > 0.05, "Unexpectedly rejected the null hypothesis with high probability. stat: {}, p: {}", chi_squared, p_value);
368            }
369        };
370    }
371
372    test_distribution!(test_u8_distribution, u8, 256);
373    test_distribution!(test_u16_distribution, u16, 65536);
374    test_distribution!(test_u32_distribution, u32, 100_000);
375    test_distribution!(test_u64_distribution, u64, 100_000);
376    test_distribution!(test_usize_distribution, usize, 100_000);
377}