sc_neurocore_engine/analysis/
statistics.rs1pub fn significance_bootstrap<F>(
18 statistic_func: F,
19 train_a: &[f64],
20 train_b: &[f64],
21 n_surrogates: usize,
22 seed: u64,
23) -> (f64, f64)
24where
25 F: Fn(&[f64], &[f64]) -> f64,
26{
27 let observed = statistic_func(train_a, train_b);
28
29 let mut combined: Vec<f64> = Vec::with_capacity(train_a.len() + train_b.len());
30 combined.extend_from_slice(train_a);
31 combined.extend_from_slice(train_b);
32
33 let n_a = train_a.len();
34 let n_total = combined.len();
35
36 let mut rng = SimpleRng::new(seed);
38 let mut count_extreme = 0u64;
39 let mut perm = combined.clone();
40
41 for _ in 0..n_surrogates {
42 perm.copy_from_slice(&combined);
43 for i in (1..n_total).rev() {
45 let j = rng.next_u64() as usize % (i + 1);
46 perm.swap(i, j);
47 }
48 let surr_val = statistic_func(&perm[..n_a], &perm[n_a..]);
49 if surr_val.abs() >= observed.abs() {
50 count_extreme += 1;
51 }
52 }
53
54 let p_value = (count_extreme + 1) as f64 / (n_surrogates + 1) as f64;
55 (observed, p_value)
56}
57
58struct SimpleRng {
60 state: u64,
61}
62
63impl SimpleRng {
64 fn new(seed: u64) -> Self {
65 Self { state: seed }
66 }
67
68 fn next_u64(&mut self) -> u64 {
69 self.state = self.state.wrapping_add(0x9e3779b97f4a7c15);
70 let mut z = self.state;
71 z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
72 z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
73 z ^ (z >> 31)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn dummy_statistic(a: &[f64], b: &[f64]) -> f64 {
82 let mean_a: f64 = if a.is_empty() {
83 0.0
84 } else {
85 a.iter().sum::<f64>() / a.len() as f64
86 };
87 let mean_b: f64 = if b.is_empty() {
88 0.0
89 } else {
90 b.iter().sum::<f64>() / b.len() as f64
91 };
92 mean_a - mean_b
93 }
94
95 #[test]
96 fn test_bootstrap_identical() {
97 let a = vec![1.0, 1.0, 1.0, 1.0, 1.0];
98 let b = vec![1.0, 1.0, 1.0, 1.0, 1.0];
99 let (obs, p) = significance_bootstrap(dummy_statistic, &a, &b, 200, 42);
100 assert!((obs).abs() < 1e-12);
101 assert!(p > 0.5);
103 }
104
105 #[test]
106 fn test_bootstrap_different() {
107 let a = vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0];
108 let b = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
109 let (obs, p) = significance_bootstrap(dummy_statistic, &a, &b, 500, 42);
110 assert!((obs - 10.0).abs() < 1e-12);
111 assert!(
113 p < 0.05,
114 "p-value {p} not significant for clearly different groups"
115 );
116 }
117
118 #[test]
119 fn test_bootstrap_p_range() {
120 let a = vec![1.0, 2.0, 3.0];
121 let b = vec![4.0, 5.0, 6.0];
122 let (_, p) = significance_bootstrap(dummy_statistic, &a, &b, 100, 42);
123 assert!(p > 0.0 && p <= 1.0);
124 }
125
126 #[test]
127 fn test_bootstrap_deterministic() {
128 let a = vec![1.0, 2.0, 3.0, 4.0];
129 let b = vec![5.0, 6.0, 7.0, 8.0];
130 let (obs1, p1) = significance_bootstrap(dummy_statistic, &a, &b, 100, 42);
131 let (obs2, p2) = significance_bootstrap(dummy_statistic, &a, &b, 100, 42);
132 assert!((obs1 - obs2).abs() < 1e-12);
133 assert!((p1 - p2).abs() < 1e-12);
134 }
135
136 #[test]
137 fn test_bootstrap_single_element() {
138 let a = vec![5.0];
139 let b = vec![0.0];
140 let (obs, p) = significance_bootstrap(dummy_statistic, &a, &b, 50, 42);
141 assert!((obs - 5.0).abs() < 1e-12);
142 assert!(p > 0.0);
143 }
144
145 #[test]
146 fn test_simple_rng_deterministic() {
147 let mut rng1 = SimpleRng::new(123);
148 let mut rng2 = SimpleRng::new(123);
149 for _ in 0..100 {
150 assert_eq!(rng1.next_u64(), rng2.next_u64());
151 }
152 }
153}