Skip to main content

ixa/random/
sampling_algorithms.rs

1//! Algorithms for uniform random sampling from hash sets or iterators. These algorithms are written to be generic
2//! over the container type using zero-cost trait abstractions.
3
4use std::borrow::Borrow;
5
6use crate::rand::seq::index::sample as choose_range;
7use crate::rand::Rng;
8
9/// Samples one element uniformly at random from an iterator whose length is known at runtime.
10///
11/// The caller must ensure that `(len, Some(len)) == iter.size_hint()`, i.e. the iterator
12/// reports its exact length via `size_hint`. We do not require `ExactSizeIterator`
13/// because that is a compile-time guarantee, whereas our requirement is a runtime condition.
14///
15/// The implementation selects a random index and uses `Iterator::nth`. For iterators
16/// with O(1) `nth` (e.g., randomly indexable structures), this is very efficient.
17/// The selected value is cloned.
18///
19/// The iterator need only support iteration; random indexing is not required.
20/// This function is intended for use when the result set is indexed and its length is known.
21#[must_use]
22pub fn sample_single_from_known_length<I, R, T>(rng: &mut R, mut iter: I) -> Option<T>
23where
24    R: Rng,
25    I: Iterator<Item = T>,
26{
27    // It is the caller's responsibility to ensure that `(len, Some(len)) == iter.size_hint()`.
28    let (length, _) = iter.size_hint();
29    if length == 0 {
30        return None;
31    }
32    // This little trick with `u32` makes this function 30% faster.
33    let index = rng.random_range(0..length as u32) as usize;
34    // The set need not be randomly indexable, so we have to use the `nth` method.
35    iter.nth(index)
36}
37
38/// Sample a random element uniformly from an iterator of unknown length.
39///
40/// We do not assume the container is randomly indexable, only that it can be iterated over.
41///
42/// This function implements "Algorithm L" from KIM-HUNG LI
43/// Reservoir-Sampling Algorithms of Time Complexity O(n(1 + log(N/n)))
44/// <https://dl.acm.org/doi/pdf/10.1145/198429.198435>
45///
46/// This algorithm is significantly slower than the "known length" algorithm (factor
47/// of 10^4). The reservoir algorithm from [`rand`](crate::rand) reduces to the "known length"
48/// algorithm when `iterator.size_hint()` returns `(k, Some(k))` for some `k`. Otherwise,
49/// this algorithm is much faster than the [`rand`](crate::rand)  implementation (factor of 100).
50#[must_use]
51pub fn sample_single_l_reservoir<I, R, T>(rng: &mut R, iterable: I) -> Option<T>
52where
53    R: Rng,
54    I: IntoIterator<Item = T>,
55{
56    let mut iter = iterable.into_iter();
57    let mut weight: f64 = rng.random(); // controls skip distance distribution
58    let mut log_one_minus_weight = (-weight).ln_1p();
59    let mut chosen_item: T = iter.next()?; // the currently selected element
60
61    // Number of elements to skip before the next candidate to consider for the reservoir.
62    // `iter.nth(skip)` skips `skip` elements and returns the next one.
63    let mut skip = (rng.random::<f64>().ln() / log_one_minus_weight).floor() as usize;
64    weight *= rng.random::<f64>();
65    log_one_minus_weight = (-weight).ln_1p();
66
67    loop {
68        match iter.nth(skip) {
69            Some(item) => {
70                chosen_item = item;
71                skip = (rng.random::<f64>().ln() / log_one_minus_weight).floor() as usize;
72                weight *= rng.random::<f64>();
73                log_one_minus_weight = (-weight).ln_1p();
74            }
75            None => return Some(chosen_item),
76        }
77    }
78}
79
80/// Count elements and sample one element uniformly from an iterator of unknown
81/// length.
82///
83/// Returns `(count, sample)` where `count` is the total number of items observed
84/// and `sample` is `None` iff `count == 0`.
85///
86/// This uses single-item reservoir sampling while tracking total count.
87#[must_use]
88pub fn count_and_sample_single_l_reservoir<I, R, T>(rng: &mut R, iterable: I) -> (usize, Option<T>)
89where
90    R: Rng,
91    I: IntoIterator<Item = T>,
92{
93    let mut count = 0usize;
94    let mut chosen_item: Option<T> = None;
95
96    for item in iterable {
97        count += 1;
98        if rng.random_range(0..count as u64) == 0 {
99            chosen_item = Some(item);
100        }
101    }
102
103    (count, chosen_item)
104}
105
106/// Samples `requested` elements uniformly at random without replacement from an iterator
107/// whose length is known at runtime. Requires `len >= requested`.
108///
109/// The caller must ensure that `(len, Some(len)) == iter.size_hint()`, i.e. the iterator
110/// reports its exact length via `size_hint`. We do not require `ExactSizeIterator`
111/// because that is a compile-time guarantee, whereas our requirement is a runtime condition.
112///
113/// The implementation selects random indices and uses `Iterator::nth`. For iterators
114/// with O(1) `nth` (e.g., randomly indexable structures), this is very efficient.
115/// Selected values are cloned.
116///
117/// This strategy is particularly effective for small `requested` (≤ 5), since it
118/// avoids iterating over the entire set and is typically faster than reservoir sampling.
119#[must_use]
120pub fn sample_multiple_from_known_length<I, R, T>(rng: &mut R, iter: I, requested: usize) -> Vec<T>
121where
122    R: Rng,
123    I: IntoIterator<Item = T>,
124{
125    let mut iter = iter.into_iter();
126    // It is the caller's responsibility to ensure that `(length, Some(length)) == iter.size_hint()`.
127    let (length, _) = iter.size_hint();
128
129    let mut indexes = Vec::with_capacity(requested);
130    indexes.extend(choose_range(rng, length, requested));
131    indexes.sort_unstable();
132
133    let mut selected = Vec::with_capacity(requested);
134    let mut consumed: usize = 0; // number of elements consumed from the iterator so far
135
136    // `iter.nth(n)` skips `n` elements and returns the next one, so to reach
137    // index `idx` we skip `idx - consumed` where `consumed` tracks how many
138    // elements have already been consumed.
139    for idx in indexes {
140        if let Some(item) = iter.nth(idx - consumed) {
141            selected.push(item);
142        }
143        consumed = idx + 1;
144    }
145
146    selected
147}
148
149/// Sample multiple random elements uniformly without replacement from a container of unknown length. If
150/// more samples are requested than are in the set, the function returns as many items as it can.
151///
152/// The implementation uses `Iterator::nth`. Randomly indexable structures will have a O(1) `nth`
153/// implementation and will be very efficient. The values are cloned.
154///
155/// This function implements "Algorithm L" from KIM-HUNG LI
156/// Reservoir-Sampling Algorithms of Time Complexity O(n(1 + log(N/n)))
157/// <https://dl.acm.org/doi/pdf/10.1145/198429.198435>
158///
159/// This algorithm is significantly faster than the reservoir algorithm in `rand` and is
160/// on par with the "known length" algorithm for large `requested` values.
161#[must_use]
162pub fn sample_multiple_l_reservoir<I, R, T>(rng: &mut R, iter: I, requested: usize) -> Vec<T>
163where
164    R: Rng,
165    I: IntoIterator<Item = T>,
166{
167    if requested == 0 {
168        return Vec::new();
169    }
170
171    let requested_recip = 1.0 / requested as f64;
172    let mut weight: f64 = rng.random(); // controls skip distance distribution
173    weight = weight.powf(requested_recip);
174    let mut log_one_minus_weight = (-weight).ln_1p();
175    let mut iter = iter.into_iter();
176    let mut reservoir: Vec<T> = iter.by_ref().take(requested).collect(); // the sample reservoir
177
178    if reservoir.len() < requested {
179        return reservoir;
180    }
181
182    // Number of elements to skip before the next candidate to consider for the reservoir.
183    // `iter.nth(skip)` skips `skip` elements and returns the next one.
184    let mut skip = (rng.random::<f64>().ln() / log_one_minus_weight).floor() as usize;
185    let uniform_random: f64 = rng.random();
186    weight *= uniform_random.powf(requested_recip);
187    log_one_minus_weight = (-weight).ln_1p();
188
189    loop {
190        match iter.nth(skip) {
191            Some(item) => {
192                let to_remove = rng.random_range(0..reservoir.len());
193                reservoir.swap_remove(to_remove);
194                reservoir.push(item);
195
196                skip = (rng.random::<f64>().ln() / log_one_minus_weight).floor() as usize;
197                let uniform_random: f64 = rng.random();
198                weight *= uniform_random.powf(requested_recip);
199                log_one_minus_weight = (-weight).ln_1p();
200            }
201            None => return reservoir,
202        }
203    }
204}
205
206/// Samples one element uniformly at random from `slice`, excluding any element
207/// equal to `excluded`. Returns `None` if the slice is empty or every element
208/// equals `excluded`.
209///
210/// `excluded` accepts either an owned `T` or a borrowed `&T` via the
211/// `Borrow<T>` bound. Dispatches to `sample_single_excluding_iteration` for slices of
212/// length `< 4` and `sample_single_excluding_rejection` otherwise. Tuned via
213/// `ixa-bench/criterion/sample_single_excluding.rs`.
214#[must_use]
215pub fn sample_single_excluding<'a, R, T, E>(
216    rng: &mut R,
217    slice: &'a [T],
218    excluded: E,
219) -> Option<&'a T>
220where
221    R: Rng,
222    T: PartialEq,
223    E: Borrow<T>,
224{
225    const SMALL_SLICE: usize = 4;
226    let excluded = excluded.borrow();
227    if slice.len() < SMALL_SLICE {
228        sample_single_excluding_iteration(rng, slice, excluded)
229    } else {
230        sample_single_excluding_rejection(rng, slice, excluded)
231    }
232}
233
234/// Linear-scan implementation of `sample_single_excluding`. Counts non-excluded
235/// entries, then picks the k-th. Wins for very small slices (`n <= 3`) where
236/// the per-trial overhead of rejection sampling exceeds the cost of a tiny
237/// filter. Exposed so benchmarks can compare strategies directly.
238#[must_use]
239pub fn sample_single_excluding_iteration<'a, R, T, E>(
240    rng: &mut R,
241    slice: &'a [T],
242    excluded: E,
243) -> Option<&'a T>
244where
245    R: Rng,
246    T: PartialEq,
247    E: Borrow<T>,
248{
249    let excluded = excluded.borrow();
250    let valid_count = slice.iter().filter(|&x| x != excluded).count();
251    if valid_count == 0 {
252        return None;
253    }
254    let k = rng.random_range(0..valid_count as u32) as usize;
255    slice.iter().filter(|&x| x != excluded).nth(k)
256}
257
258/// Rejection-sampling implementation of `sample_single_excluding`. Picks a
259/// uniform index, accepts if not equal to `excluded`. Wins at `n >= 4` and is
260/// essentially constant time when `excluded` appears 0 or 1 times. Falls
261/// through to `sample_single_excluding_iteration` after at most 16 consecutive
262/// matches (or `n`, whichever is smaller), which also returns `None` when
263/// every element matches. Exposed so benchmarks can compare strategies
264/// directly.
265#[must_use]
266pub fn sample_single_excluding_rejection<'a, R, T, E>(
267    rng: &mut R,
268    slice: &'a [T],
269    excluded: E,
270) -> Option<&'a T>
271where
272    R: Rng,
273    T: PartialEq,
274    E: Borrow<T>,
275{
276    // The `u32` cast on `random_range` arguments is faster than the `usize`
277    // form (see `sample_single_from_known_length`).
278    //
279    // Cap trials at `min(MAX_REJECTIONS, n)`: once we've drawn `n` indices
280    // and all matched, almost the entire slice equals `excluded` and the
281    // iteration path is cheaper than retrying.
282    const MAX_REJECTIONS: usize = 16;
283    if slice.is_empty() {
284        return None;
285    }
286    let excluded = excluded.borrow();
287    let trials = MAX_REJECTIONS.min(slice.len());
288    for _ in 0..trials {
289        let i = rng.random_range(0..slice.len() as u32) as usize;
290        let candidate = &slice[i];
291        if candidate != excluded {
292            return Some(candidate);
293        }
294    }
295    sample_single_excluding_iteration(rng, slice, excluded)
296}
297
298/// Sample one element uniformly from an iterator, excluding any element equal
299/// to `excluded`. Returns `None` if the iterator is empty or every element
300/// equals `excluded`.
301///
302/// This is the iterator counterpart to [`sample_single_excluding`]. It runs
303/// in O(n) time and is correct even when the iterator does not report an
304/// exact length. Prefer [`sample_single_excluding`] for slices, which can
305/// dispatch to a faster rejection-sampling strategy backed by random access.
306#[must_use]
307pub fn sample_single_excluding_l_reservoir<I, R, T, E>(
308    rng: &mut R,
309    iterable: I,
310    excluded: E,
311) -> Option<T>
312where
313    R: Rng,
314    T: PartialEq,
315    I: IntoIterator<Item = T>,
316    E: Borrow<T>,
317{
318    let excluded = excluded.borrow();
319    let (_, chosen) = count_and_sample_single_l_reservoir(
320        rng,
321        iterable.into_iter().filter(|item| item != excluded),
322    );
323    chosen
324}
325
326#[cfg(test)]
327mod tests {
328    use rand::rngs::StdRng;
329    use rand::SeedableRng;
330
331    use super::*;
332    use crate::hashing::{HashSet, HashSetExt};
333
334    #[test]
335    fn test_sample_single_l_reservoir_basic() {
336        let data: Vec<u32> = (0..1000).collect();
337        let seed: u64 = 42;
338        let mut rng = StdRng::seed_from_u64(seed);
339        let sample = sample_single_l_reservoir(&mut rng, data);
340
341        // Should return Some value
342        assert!(sample.is_some());
343
344        // Value should be in valid range
345        let value = sample.unwrap();
346        assert!(value < 1000);
347    }
348
349    #[test]
350    fn test_sample_single_l_reservoir_empty() {
351        let data: Vec<u32> = Vec::new();
352        let mut rng = StdRng::seed_from_u64(42);
353        let sample = sample_single_l_reservoir(&mut rng, data);
354
355        // Should return None for empty container
356        assert!(sample.is_none());
357    }
358
359    #[test]
360    fn test_sample_single_l_reservoir_single_element() {
361        let data: Vec<u32> = vec![42];
362        let mut rng = StdRng::seed_from_u64(1);
363        let sample = sample_single_l_reservoir(&mut rng, data);
364
365        // Should return the only element
366        assert_eq!(sample, Some(42));
367    }
368
369    #[test]
370    fn test_sample_single_l_reservoir_uniformity() {
371        let population: u32 = 1000;
372        let data: Vec<u32> = (0..population).collect();
373        let num_runs = 10000;
374        let num_bins = 10;
375        let mut counts = vec![0usize; num_bins];
376
377        for run in 0..num_runs {
378            let mut rng = StdRng::seed_from_u64(42 + run as u64);
379            let sample = sample_single_l_reservoir(&mut rng, data.iter().cloned());
380
381            if let Some(value) = sample {
382                let bin = (value as usize) / (population as usize / num_bins);
383                counts[bin] += 1;
384            }
385        }
386
387        // Expected count per bin for uniform sampling
388        let expected = num_runs as f64 / num_bins as f64;
389
390        // Compute chi-square statistic
391        let chi_square: f64 = counts
392            .iter()
393            .map(|&obs| {
394                let diff = (obs as f64) - expected;
395                diff * diff / expected
396            })
397            .sum();
398
399        // Degrees of freedom = num_bins - 1 = 9
400        // Critical χ²₀.₉₉₉ for df=9 is 27.877
401        let critical = 27.877;
402
403        println!("χ² = {}, counts = {:?}", chi_square, counts);
404
405        assert!(
406            chi_square < critical,
407            "Single sample fails uniformity test: χ² = {}, counts = {:?}",
408            chi_square,
409            counts
410        );
411    }
412
413    #[test]
414    fn test_sample_single_l_reservoir_hashset() {
415        let mut data = HashSet::new();
416        for i in 0..100 {
417            data.insert(i);
418        }
419
420        let mut rng = StdRng::seed_from_u64(42);
421        let sample = sample_single_l_reservoir(&mut rng, &data);
422
423        assert!(sample.is_some());
424        let value = sample.unwrap();
425        assert!(data.contains(value));
426    }
427
428    #[test]
429    fn test_count_and_sample_single_l_reservoir_empty() {
430        let data: Vec<u32> = Vec::new();
431        let mut rng = StdRng::seed_from_u64(42);
432        let (count, sample) = count_and_sample_single_l_reservoir(&mut rng, data);
433        assert_eq!(count, 0);
434        assert!(sample.is_none());
435    }
436
437    #[test]
438    fn test_count_and_sample_single_l_reservoir_count_matches() {
439        let data: Vec<u32> = (0..1000).collect();
440        let mut rng = StdRng::seed_from_u64(42);
441        let (count, sample) = count_and_sample_single_l_reservoir(&mut rng, data);
442        assert_eq!(count, 1000);
443        assert!(sample.is_some());
444    }
445
446    #[test]
447    fn test_count_and_sample_single_l_reservoir_single_element() {
448        let data: Vec<u32> = vec![7];
449        let mut rng = StdRng::seed_from_u64(42);
450        let (count, sample) = count_and_sample_single_l_reservoir(&mut rng, data);
451        assert_eq!(count, 1);
452        assert_eq!(sample, Some(7));
453    }
454
455    #[test]
456    fn test_sample_multiple_l_reservoir_basic() {
457        let data: Vec<u32> = (0..1000).collect();
458        let requested = 100;
459        let seed: u64 = 42;
460        let mut rng = StdRng::seed_from_u64(seed);
461        let sample = sample_multiple_l_reservoir(&mut rng, data, requested);
462
463        // Correct sample size
464        assert_eq!(sample.len(), requested);
465
466        // All sampled values are within the valid range
467        assert!(sample.iter().all(|v| *v < 1000));
468
469        // The sample should not have duplicates
470        let unique: HashSet<_> = sample.iter().collect();
471        assert_eq!(unique.len(), sample.len());
472    }
473
474    #[test]
475    fn test_sample_multiple_l_reservoir_empty() {
476        let data: Vec<u32> = Vec::new();
477        let mut rng = StdRng::seed_from_u64(42);
478        let sample = sample_multiple_l_reservoir(&mut rng, &data, 10);
479
480        // Should return empty vector for empty container
481        assert_eq!(sample.len(), 0);
482    }
483
484    #[test]
485    fn test_sample_multiple_l_reservoir_zero_requested() {
486        let data: Vec<u32> = (0..100).collect();
487        let mut rng = StdRng::seed_from_u64(42);
488        let sample = sample_multiple_l_reservoir(&mut rng, &data, 0);
489
490        // Should return empty vector when 0 requested
491        assert_eq!(sample.len(), 0);
492    }
493
494    #[test]
495    fn test_sample_multiple_l_reservoir_requested_exceeds_population() {
496        let data: Vec<u32> = (0..50).collect();
497        let requested = 100;
498        let mut rng = StdRng::seed_from_u64(42);
499        let sample = sample_multiple_l_reservoir(&mut rng, data, requested);
500
501        // Should return all available items when requested > population
502        assert_eq!(sample.len(), 50);
503
504        // All elements should be unique
505        let unique: HashSet<_> = sample.iter().collect();
506        assert_eq!(unique.len(), 50);
507
508        // All elements should be from the original data
509        assert!(sample.iter().all(|v| *v < 50));
510    }
511
512    #[test]
513    fn test_sample_multiple_l_reservoir_exact_population() {
514        let data: Vec<u32> = (0..100).collect();
515        let mut rng = StdRng::seed_from_u64(42);
516        let sample = sample_multiple_l_reservoir(&mut rng, data, 100);
517
518        // Should return all elements when requested == population
519        assert_eq!(sample.len(), 100);
520
521        let unique: HashSet<_> = sample.iter().collect();
522        assert_eq!(unique.len(), 100);
523    }
524
525    #[test]
526    fn test_sample_multiple_l_reservoir_single_element() {
527        let data: Vec<u32> = vec![42];
528        let mut rng = StdRng::seed_from_u64(1);
529        let sample = sample_multiple_l_reservoir(&mut rng, data, 1);
530
531        assert_eq!(sample.len(), 1);
532        assert_eq!(sample[0], 42);
533    }
534
535    #[test]
536    fn test_sample_multiple_l_reservoir_hashset() {
537        let mut data = HashSet::new();
538        for i in 0..100 {
539            data.insert(i);
540        }
541
542        let mut rng = StdRng::seed_from_u64(42);
543        let sample = sample_multiple_l_reservoir(&mut rng, &data, 10);
544
545        assert_eq!(sample.len(), 10);
546
547        // All sampled values should be in the original set
548        assert!(sample.iter().all(|v| data.contains(v)));
549
550        // No duplicates
551        let unique: HashSet<_> = sample.iter().collect();
552        assert_eq!(unique.len(), 10);
553    }
554
555    #[test]
556    fn test_sample_multiple_l_reservoir_small_sample() {
557        let data: Vec<u32> = (0..1000).collect();
558        let requested = 5;
559        let mut rng = StdRng::seed_from_u64(42);
560        let sample = sample_multiple_l_reservoir(&mut rng, &data, requested);
561
562        assert_eq!(sample.len(), requested);
563
564        // No duplicates
565        let unique: HashSet<_> = sample.iter().collect();
566        assert_eq!(unique.len(), requested);
567    }
568
569    #[test]
570    fn test_sample_multiple_l_reservoir_large_sample() {
571        let data: Vec<u32> = (0..1000).collect();
572        let requested = 900;
573        let mut rng = StdRng::seed_from_u64(42);
574        let sample = sample_multiple_l_reservoir(&mut rng, &data, requested);
575
576        assert_eq!(sample.len(), requested);
577
578        // No duplicates
579        let unique: HashSet<_> = sample.iter().collect();
580        assert_eq!(unique.len(), requested);
581    }
582
583    // Verifies that the reservoir sampling algorithm produces uniformly distributed
584    // samples by running it 1000 times and checking that the resulting chi-square
585    // statistics follow the expected chi-square(9) distribution. Note that this
586    // test is only approximately correct, reasonable only when `requested` is small
587    // relative to `population`, because `sample_multiple_l_reservoir` samples
588    // without replacement, while the chi-squared test assumes independent samples.
589    #[test]
590    fn test_sample_multiple_l_reservoir_uniformity() {
591        let population: u32 = 10000;
592        let data: Vec<u32> = (0..population).collect();
593        let requested = 100;
594        let num_runs = 1000;
595        let mut chi_squares = Vec::with_capacity(num_runs);
596
597        for run in 0..num_runs {
598            let mut rng = StdRng::seed_from_u64(42 + run as u64);
599            let sample = sample_multiple_l_reservoir(&mut rng, data.iter().cloned(), requested);
600
601            // Partition range 0..population into 10 equal-width bins
602            let mut counts = [0usize; 10];
603            for &value in &sample {
604                let bin = (value as usize) / (population as usize / 10);
605                counts[bin] += 1;
606            }
607
608            // Expected count per bin for uniform sampling
609            let expected = requested as f64 / 10.0; // = 10.0
610
611            // Compute chi-square statistic
612            let chi_square: f64 = counts
613                .iter()
614                .map(|&obs| {
615                    let diff = (obs as f64) - expected;
616                    diff * diff / expected
617                })
618                .sum();
619
620            chi_squares.push(chi_square);
621        }
622
623        // Now test that chi_squares follow a chi-square distribution with df=9
624        // We use quantiles of the chi-square(9) distribution to create bins
625        // and check if the observed counts match the expected uniform distribution
626
627        // Quantiles of chi-square distribution with df=9 at deciles (10 bins)
628        // These values define the bin boundaries such that each bin should contain
629        // 10% of the observations if they truly follow chi-square(9).
630        // Generate with Mathematica:
631        //     Table[Quantile[ChiSquareDistribution[9], p/10], {p, 0, 10}]//N
632        let quantiles = [
633            0.0,           // 0th percentile (minimum)
634            4.16816,       // 10th percentile
635            5.38005,       // 20th percentile
636            6.39331,       // 30th percentile
637            7.35703,       // 40th percentile
638            8.34283,       // 50th percentile (median)
639            9.41364,       // 60th percentile
640            10.6564,       // 70th percentile
641            12.2421,       // 80th percentile
642            14.6837,       // 90th percentile
643            f64::INFINITY, // 100th percentile (maximum)
644        ];
645
646        let num_bins = quantiles.len() - 1;
647        let mut chi_square_counts = vec![0usize; num_bins];
648
649        for &chi_sq in &chi_squares {
650            // Find which bin this chi-square value falls into
651            for i in 0..num_bins {
652                if chi_sq >= quantiles[i] && chi_sq < quantiles[i + 1] {
653                    chi_square_counts[i] += 1;
654                    break;
655                }
656            }
657        }
658
659        // Each bin should contain approximately num_runs / num_bins observations
660        let expected_per_bin = num_runs as f64 / num_bins as f64;
661        let chi_square_of_chi_squares: f64 = chi_square_counts
662            .iter()
663            .map(|&obs| {
664                let diff = (obs as f64) - expected_per_bin;
665                diff * diff / expected_per_bin
666            })
667            .sum();
668
669        // Degrees of freedom = (#bins - 1) = 9
670        // Critical χ²₀.₉₉₉ for df=9 is 27.877
671        let critical = 27.877;
672
673        println!(
674            "χ² = {}, counts = {:?}",
675            chi_square_of_chi_squares, chi_square_counts
676        );
677
678        assert!(
679            chi_square_of_chi_squares < critical,
680            "Chi-square statistics fail to follow chi-square(9) distribution: χ² = {}, counts = {:?}",
681            chi_square_of_chi_squares,
682            chi_square_counts
683        );
684    }
685
686    // Test that each element has equal probability of being selected
687    #[test]
688    fn test_sample_multiple_l_reservoir_element_probability() {
689        let population: u32 = 100;
690        let data: Vec<u32> = (0..population).collect();
691        let requested = 10;
692        let num_runs = 10000;
693        let mut selection_counts = vec![0usize; population as usize];
694
695        for run in 0..num_runs {
696            let mut rng = StdRng::seed_from_u64(42 + run as u64);
697            let sample = sample_multiple_l_reservoir(&mut rng, data.iter().cloned(), requested);
698
699            for &value in &sample {
700                selection_counts[value as usize] += 1;
701            }
702        }
703
704        // Each element should be selected with probability requested/population
705        // Expected count per element
706        let expected = (num_runs * requested) as f64 / population as f64;
707
708        // Compute chi-square statistic
709        let chi_square: f64 = selection_counts
710            .iter()
711            .map(|&obs| {
712                let diff = (obs as f64) - expected;
713                diff * diff / expected
714            })
715            .sum();
716
717        // Degrees of freedom = population - 1 = 99.
718        // Critical value uses p = 0.999 (alpha = 0.001): χ²_{0.999, 99} ≈ 148.23
719        // from the inverse chi-square CDF.
720        let critical = 148.23;
721
722        assert!(
723            chi_square < critical,
724            "Element selection probabilities are not uniform: χ² = {}",
725            chi_square
726        );
727    }
728
729    // Test reproducibility with same seed
730    #[test]
731    fn test_sample_multiple_l_reservoir_reproducibility() {
732        let data: Vec<u32> = (0..1000).collect();
733        let test_sizes = [1, 2, 5, 10, 100, 500];
734
735        for &requested in &test_sizes {
736            let seed: u64 = 12345;
737
738            let mut rng1 = StdRng::seed_from_u64(seed);
739            let sample1 = sample_multiple_l_reservoir(&mut rng1, &data, requested);
740
741            let mut rng2 = StdRng::seed_from_u64(seed);
742            let sample2 = sample_multiple_l_reservoir(&mut rng2, &data, requested);
743
744            // Verify correct sample size
745            assert_eq!(
746                sample1.len(),
747                requested,
748                "Sample size {} doesn't match requested size {}",
749                sample1.len(),
750                requested
751            );
752            assert_eq!(
753                sample2.len(),
754                requested,
755                "Sample size {} doesn't match requested size {}",
756                sample2.len(),
757                requested
758            );
759
760            // Same seed should produce identical samples
761            assert_eq!(
762                sample1, sample2,
763                "Reproducibility failed for requested={}",
764                requested
765            );
766        }
767    }
768
769    #[test]
770    fn test_sample_single_l_reservoir_reproducibility() {
771        let data: Vec<u32> = (0..1000).collect();
772        let seed: u64 = 12345;
773
774        let mut rng1 = StdRng::seed_from_u64(seed);
775        let sample1 = sample_single_l_reservoir(&mut rng1, &data);
776
777        let mut rng2 = StdRng::seed_from_u64(seed);
778        let sample2 = sample_single_l_reservoir(&mut rng2, &data);
779
780        // Same seed should produce identical samples
781        assert_eq!(sample1, sample2);
782    }
783
784    #[test]
785    fn sample_single_excluding_empty_slice_returns_none() {
786        let data: [u32; 0] = [];
787        let mut rng = StdRng::seed_from_u64(42);
788        assert_eq!(sample_single_excluding(&mut rng, &data, 7), None);
789    }
790
791    #[test]
792    fn sample_single_excluding_only_excluded_returns_none() {
793        let data = [9, 9, 9, 9];
794        let mut rng = StdRng::seed_from_u64(42);
795        assert_eq!(sample_single_excluding(&mut rng, &data, 9), None);
796    }
797
798    #[test]
799    fn sample_single_excluding_never_returns_excluded_small() {
800        let data: Vec<u32> = (0..10).collect();
801        let mut rng = StdRng::seed_from_u64(42);
802        for _ in 0..1000 {
803            let v = sample_single_excluding(&mut rng, &data, 3).unwrap();
804            assert_ne!(*v, 3);
805            assert!(*v < 10);
806        }
807    }
808
809    #[test]
810    fn sample_single_excluding_never_returns_excluded_large() {
811        // Large slice: exercises the rejection-sampling path.
812        let data: Vec<u32> = (0..1000).collect();
813        let mut rng = StdRng::seed_from_u64(42);
814        for _ in 0..1000 {
815            let v = sample_single_excluding(&mut rng, &data, 500).unwrap();
816            assert_ne!(*v, 500);
817            assert!(*v < 1000);
818        }
819    }
820
821    #[test]
822    fn sample_single_excluding_excluded_not_in_slice() {
823        let data: Vec<u32> = (0..10).collect();
824        let mut rng = StdRng::seed_from_u64(42);
825        for _ in 0..100 {
826            let v = sample_single_excluding(&mut rng, &data, 999).unwrap();
827            assert!(*v < 10);
828        }
829    }
830
831    #[test]
832    fn sample_single_excluding_falls_back_when_excluded_dominates() {
833        // 99.8% of values equal `excluded`: rejection retries get exhausted
834        // and the scan fallback runs. Both rare non-excluded values must show
835        // up across 200 samples.
836        let mut data = vec![0u32; 1000];
837        data[42] = 1;
838        data[800] = 2;
839        let mut rng = StdRng::seed_from_u64(42);
840        let mut seen: Vec<u32> = (0..200)
841            .map(|_| *sample_single_excluding(&mut rng, &data, 0).unwrap())
842            .collect();
843        seen.sort();
844        seen.dedup();
845        assert_eq!(seen, vec![1, 2]);
846    }
847
848    #[test]
849    fn sample_single_excluding_uniformity_small() {
850        // Small slice → iteration path.
851        let data: Vec<u32> = (0..20).collect();
852        let excluded = 7u32;
853        let num_runs = 50_000;
854        let mut counts = [0usize; 20];
855        let mut rng = StdRng::seed_from_u64(42);
856        for _ in 0..num_runs {
857            let v = sample_single_excluding(&mut rng, &data, excluded).unwrap();
858            counts[*v as usize] += 1;
859        }
860        assert_eq!(counts[excluded as usize], 0);
861
862        let expected = num_runs as f64 / 19.0;
863        let chi_square: f64 = counts
864            .iter()
865            .enumerate()
866            .filter(|(i, _)| *i != excluded as usize)
867            .map(|(_, &obs)| {
868                let diff = obs as f64 - expected;
869                diff * diff / expected
870            })
871            .sum();
872        // df = 18, χ²_{0.999} ≈ 42.31
873        assert!(chi_square < 42.31, "χ² = {chi_square}");
874    }
875
876    #[test]
877    fn sample_single_excluding_uniformity_large() {
878        // Large slice: rejection-sampling path.
879        let data: Vec<u32> = (0..200).collect();
880        let excluded = 99u32;
881        let num_runs = 200_000;
882        let mut counts = vec![0usize; 200];
883        let mut rng = StdRng::seed_from_u64(42);
884        for _ in 0..num_runs {
885            let v = sample_single_excluding(&mut rng, &data, excluded).unwrap();
886            counts[*v as usize] += 1;
887        }
888        assert_eq!(counts[excluded as usize], 0);
889
890        let expected = num_runs as f64 / 199.0;
891        let chi_square: f64 = counts
892            .iter()
893            .enumerate()
894            .filter(|(i, _)| *i != excluded as usize)
895            .map(|(_, &obs)| {
896                let diff = obs as f64 - expected;
897                diff * diff / expected
898            })
899            .sum();
900        // df = 198, χ²_{0.999} ≈ 264.69
901        assert!(chi_square < 264.69, "χ² = {chi_square}");
902    }
903
904    #[test]
905    #[allow(clippy::needless_borrows_for_generic_args)]
906    fn sample_single_excluding_accepts_owned_or_borrowed_excluded() {
907        // The `Borrow<T>` bound on `excluded` lets callers pass either an owned
908        // `T` or a `&T`; both must compile and produce the same answer.
909        let data: Vec<u32> = (0..10).collect();
910        let mut rng_owned = StdRng::seed_from_u64(42);
911        let mut rng_ref = StdRng::seed_from_u64(42);
912        let excluded = 3u32;
913        for _ in 0..50 {
914            let owned = sample_single_excluding(&mut rng_owned, &data, excluded);
915            let borrowed = sample_single_excluding(&mut rng_ref, &data, &excluded);
916            assert_eq!(owned, borrowed);
917        }
918    }
919
920    #[test]
921    fn sample_single_excluding_reproducibility() {
922        let data: Vec<u32> = (0..1000).collect();
923        let seed = 12345u64;
924
925        let mut rng1 = StdRng::seed_from_u64(seed);
926        let mut rng2 = StdRng::seed_from_u64(seed);
927        for _ in 0..100 {
928            let a = sample_single_excluding(&mut rng1, &data, 500);
929            let b = sample_single_excluding(&mut rng2, &data, 500);
930            assert_eq!(a, b);
931        }
932    }
933
934    #[test]
935    fn sample_single_excluding_l_reservoir_empty_returns_none() {
936        let data: [u32; 0] = [];
937        let mut rng = StdRng::seed_from_u64(42);
938        assert_eq!(
939            sample_single_excluding_l_reservoir(&mut rng, data, 7u32),
940            None
941        );
942    }
943
944    #[test]
945    fn sample_single_excluding_l_reservoir_only_excluded_returns_none() {
946        let data = [9u32, 9, 9, 9];
947        let mut rng = StdRng::seed_from_u64(42);
948        assert_eq!(
949            sample_single_excluding_l_reservoir(&mut rng, data, 9u32),
950            None
951        );
952    }
953
954    #[test]
955    fn sample_single_excluding_l_reservoir_never_returns_excluded() {
956        let data: Vec<u32> = (0..50).collect();
957        let mut rng = StdRng::seed_from_u64(42);
958        for _ in 0..1000 {
959            let v =
960                sample_single_excluding_l_reservoir(&mut rng, data.iter().copied(), 17u32).unwrap();
961            assert_ne!(v, 17);
962            assert!(v < 50);
963        }
964    }
965
966    #[test]
967    fn sample_single_excluding_l_reservoir_uniformity() {
968        let excluded = 7u32;
969        let data: Vec<u32> = (0..20).collect();
970        let num_runs = 50_000;
971        let mut counts = [0usize; 20];
972        let mut rng = StdRng::seed_from_u64(42);
973        for _ in 0..num_runs {
974            let v = sample_single_excluding_l_reservoir(&mut rng, data.iter().copied(), excluded)
975                .unwrap();
976            counts[v as usize] += 1;
977        }
978        assert_eq!(counts[excluded as usize], 0);
979
980        let expected = num_runs as f64 / 19.0;
981        let chi_square: f64 = counts
982            .iter()
983            .enumerate()
984            .filter(|(i, _)| *i != excluded as usize)
985            .map(|(_, &obs)| {
986                let diff = obs as f64 - expected;
987                diff * diff / expected
988            })
989            .sum();
990        // df = 18, χ²_{0.999} ≈ 42.31
991        assert!(chi_square < 42.31, "χ² = {chi_square}");
992    }
993
994    #[test]
995    #[allow(clippy::needless_borrows_for_generic_args)]
996    fn sample_single_excluding_l_reservoir_accepts_owned_or_borrowed_excluded() {
997        let data: Vec<u32> = (0..10).collect();
998        let excluded = 3u32;
999        let mut rng_owned = StdRng::seed_from_u64(42);
1000        let mut rng_ref = StdRng::seed_from_u64(42);
1001        for _ in 0..50 {
1002            let owned =
1003                sample_single_excluding_l_reservoir(&mut rng_owned, data.iter().copied(), excluded);
1004            let borrowed =
1005                sample_single_excluding_l_reservoir(&mut rng_ref, data.iter().copied(), &excluded);
1006            assert_eq!(owned, borrowed);
1007        }
1008    }
1009
1010    #[test]
1011    fn sample_single_excluding_l_reservoir_reproducibility() {
1012        let data: Vec<u32> = (0..1000).collect();
1013        let seed = 12345u64;
1014        let mut rng1 = StdRng::seed_from_u64(seed);
1015        let mut rng2 = StdRng::seed_from_u64(seed);
1016        for _ in 0..100 {
1017            let a = sample_single_excluding_l_reservoir(&mut rng1, data.iter().copied(), 500u32);
1018            let b = sample_single_excluding_l_reservoir(&mut rng2, data.iter().copied(), 500u32);
1019            assert_eq!(a, b);
1020        }
1021    }
1022}