tt_plan_core/
bootstrap.rs1use rand::{Rng, SeedableRng};
8use rand_chacha::ChaCha8Rng;
9
10#[must_use]
18pub fn bootstrap_ci(
19 samples: &[f64],
20 seed: u64,
21 iterations: u32,
22 percentiles: (f64, f64),
23) -> (f64, f64) {
24 let n = samples.len();
25 if n == 0 || iterations == 0 {
26 return (0.0, 0.0);
27 }
28 let mut rng = ChaCha8Rng::seed_from_u64(seed);
29 let n_f = n as f64;
30 let mut means: Vec<f64> = Vec::with_capacity(iterations as usize);
31 for _ in 0..iterations {
32 let mut sum = 0.0;
33 for _ in 0..n {
34 let idx = rng.gen_range(0..n);
35 sum += samples[idx];
36 }
37 means.push(sum / n_f);
38 }
39 means.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
40 let iter_f = iterations as f64;
41 let lo_idx = ((percentiles.0 * iter_f) as usize).min(means.len() - 1);
42 let hi_idx = ((percentiles.1 * iter_f) as usize).min(means.len() - 1);
43 (means[lo_idx], means[hi_idx])
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn empty_input_returns_zero() {
52 assert_eq!(bootstrap_ci(&[], 1, 1000, (0.025, 0.975)), (0.0, 0.0));
53 }
54
55 #[test]
56 fn zero_iterations_returns_zero() {
57 assert_eq!(
58 bootstrap_ci(&[1.0, 2.0, 3.0], 1, 0, (0.025, 0.975)),
59 (0.0, 0.0)
60 );
61 }
62
63 #[test]
64 fn determinism_same_seed() {
65 let samples: Vec<f64> = (0..100).map(|i| i as f64).collect();
66 let a = bootstrap_ci(&samples, 42, 1000, (0.025, 0.975));
67 let b = bootstrap_ci(&samples, 42, 1000, (0.025, 0.975));
68 assert_eq!(a, b);
69 }
70
71 #[test]
72 fn different_seed_different_output() {
73 let samples: Vec<f64> = (0..100).map(|i| i as f64).collect();
74 let a = bootstrap_ci(&samples, 42, 1000, (0.025, 0.975));
75 let b = bootstrap_ci(&samples, 43, 1000, (0.025, 0.975));
76 assert_ne!(a, b);
77 }
78
79 #[test]
80 fn constant_samples_zero_width_ci() {
81 let samples = vec![5.0; 50];
82 let (lo, hi) = bootstrap_ci(&samples, 7, 500, (0.025, 0.975));
83 assert!((lo - 5.0).abs() < 1e-12);
85 assert!((hi - 5.0).abs() < 1e-12);
86 }
87}