1use super::TestResult;
10use crate::error::FdarError;
11use crate::function_on_scalar::integrated_f_statistic;
12use crate::helpers::simpsons_weights;
13use crate::matrix::FdMatrix;
14use rand::rngs::StdRng;
15use rand::SeedableRng;
16
17pub const DEFAULT_N_PERM: usize = 999;
19
20fn validate_two_samples(
25 data_a: &FdMatrix,
26 data_b: &FdMatrix,
27 argvals: &[f64],
28) -> Result<(usize, usize, usize), FdarError> {
29 let (n_a, m_a) = data_a.shape();
30 let (n_b, m_b) = data_b.shape();
31 if m_a == 0 || m_b == 0 {
32 return Err(FdarError::InvalidDimension {
33 parameter: "data",
34 expected: "at least 1 column (grid points)".to_string(),
35 actual: format!("data_a has {m_a} columns, data_b has {m_b} columns"),
36 });
37 }
38 if m_a != m_b {
39 return Err(FdarError::InvalidDimension {
40 parameter: "data_b",
41 expected: format!("{m_a} columns (matching data_a)"),
42 actual: format!("{m_b} columns"),
43 });
44 }
45 if argvals.len() != m_a {
46 return Err(FdarError::InvalidDimension {
47 parameter: "argvals",
48 expected: format!("{m_a} elements (matching data columns)"),
49 actual: format!("{} elements", argvals.len()),
50 });
51 }
52 if n_a < 2 || n_b < 2 {
53 return Err(FdarError::InvalidDimension {
54 parameter: "data",
55 expected: "at least 2 rows per sample".to_string(),
56 actual: format!("data_a has {n_a} rows, data_b has {n_b} rows"),
57 });
58 }
59 Ok((n_a, n_b, m_a))
60}
61
62fn pool_two_samples(
65 data_a: &FdMatrix,
66 data_b: &FdMatrix,
67 n_a: usize,
68 n_b: usize,
69 m: usize,
70) -> FdMatrix {
71 let mut pooled = FdMatrix::zeros(n_a + n_b, m);
72 for j in 0..m {
73 for i in 0..n_a {
74 pooled[(i, j)] = data_a[(i, j)];
75 }
76 for i in 0..n_b {
77 pooled[(n_a + i, j)] = data_b[(i, j)];
78 }
79 }
80 pooled
81}
82
83fn integrated_l2_mean_diff(
87 pooled: &FdMatrix,
88 labels: &[usize],
89 n_a: usize,
90 m: usize,
91 weights: &[f64],
92) -> f64 {
93 let mut mean_a = vec![0.0; m];
94 let mut mean_b = vec![0.0; m];
95 let n_b = labels.len() - n_a;
96 for (i, &lab) in labels.iter().enumerate() {
97 if lab == 0 {
98 for j in 0..m {
99 mean_a[j] += pooled[(i, j)];
100 }
101 } else {
102 for j in 0..m {
103 mean_b[j] += pooled[(i, j)];
104 }
105 }
106 }
107 for j in 0..m {
108 mean_a[j] /= n_a as f64;
109 mean_b[j] /= n_b as f64;
110 }
111 let mut acc = 0.0;
112 for j in 0..m {
113 let d = mean_a[j] - mean_b[j];
114 acc += d * d * weights[j];
115 }
116 acc.sqrt()
117}
118
119fn shuffle_labels(v: &mut [usize], rng: &mut StdRng) {
121 use rand::Rng;
122 let n = v.len();
123 for i in (1..n).rev() {
124 let j = rng.gen_range(0..=i);
125 v.swap(i, j);
126 }
127}
128
129pub fn t_perm_test(
153 data_a: &FdMatrix,
154 data_b: &FdMatrix,
155 argvals: &[f64],
156 n_perm: usize,
157 seed: u64,
158) -> Result<TestResult, FdarError> {
159 let (n_a, n_b, m) = validate_two_samples(data_a, data_b, argvals)?;
160 if n_perm == 0 {
161 return Err(FdarError::InvalidParameter {
162 parameter: "n_perm",
163 message: "must be >= 1".to_string(),
164 });
165 }
166
167 let weights = simpsons_weights(argvals);
168 let pooled = pool_two_samples(data_a, data_b, n_a, n_b, m);
169
170 let mut labels: Vec<usize> = (0..(n_a + n_b)).map(|i| usize::from(i >= n_a)).collect();
171 let observed = integrated_l2_mean_diff(&pooled, &labels, n_a, m, &weights);
172
173 let mut rng = StdRng::seed_from_u64(seed);
174 let mut n_ge = 0usize;
175 for _ in 0..n_perm {
176 shuffle_labels(&mut labels, &mut rng);
177 let perm_stat = integrated_l2_mean_diff(&pooled, &labels, n_a, m, &weights);
178 if perm_stat >= observed {
179 n_ge += 1;
180 }
181 }
182
183 let p_value = (n_ge as f64 + 1.0) / (n_perm as f64 + 1.0);
184 Ok(TestResult {
185 statistic: observed,
186 p_value,
187 n_perm,
188 })
189}
190
191pub fn f_perm_test(
215 data_a: &FdMatrix,
216 data_b: &FdMatrix,
217 argvals: &[f64],
218 n_perm: usize,
219 seed: u64,
220) -> Result<TestResult, FdarError> {
221 let (n_a, n_b, m) = validate_two_samples(data_a, data_b, argvals)?;
222 if n_perm == 0 {
223 return Err(FdarError::InvalidParameter {
224 parameter: "n_perm",
225 message: "must be >= 1".to_string(),
226 });
227 }
228
229 let pooled = pool_two_samples(data_a, data_b, n_a, n_b, m);
230 let labels_dedup = [0usize, 1usize];
231
232 let mut groups: Vec<usize> = (0..(n_a + n_b)).map(|i| usize::from(i >= n_a)).collect();
234 let observed = integrated_f_statistic(&pooled, &groups, &labels_dedup);
235
236 let mut rng = StdRng::seed_from_u64(seed);
237 let mut n_ge = 0usize;
238 for _ in 0..n_perm {
239 shuffle_labels(&mut groups, &mut rng);
240 let perm_stat = integrated_f_statistic(&pooled, &groups, &labels_dedup);
241 if perm_stat >= observed {
242 n_ge += 1;
243 }
244 }
245
246 let p_value = (n_ge as f64 + 1.0) / (n_perm as f64 + 1.0);
247 Ok(TestResult {
248 statistic: observed,
249 p_value,
250 n_perm,
251 })
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::test_helpers::uniform_grid;
258
259 fn make_sample(n: usize, argvals: &[f64], shift: f64, seed: u64) -> FdMatrix {
263 let m = argvals.len();
264 let mut mat = FdMatrix::zeros(n, m);
265 let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1);
267 for i in 0..n {
268 for (j, &t) in argvals.iter().enumerate() {
269 state = state
270 .wrapping_mul(6_364_136_223_846_793_005)
271 .wrapping_add(1_442_695_040_888_963_407);
272 let noise = ((state >> 33) as f64 / (1u64 << 31) as f64) - 1.0; mat[(i, j)] = (2.0 * std::f64::consts::PI * t).sin() + 0.1 * noise + shift;
274 }
275 }
276 mat
277 }
278
279 #[test]
280 fn t_perm_separated_small_p() {
281 let argvals = uniform_grid(25);
282 let a = make_sample(15, &argvals, 0.0, 1);
283 let b = make_sample(15, &argvals, 5.0, 2); let res = t_perm_test(&a, &b, &argvals, 199, 42).unwrap();
285 assert!(
286 res.p_value < 0.05,
287 "separated samples should give small p, got {}",
288 res.p_value
289 );
290 }
291
292 #[test]
293 fn t_perm_null_large_p() {
294 let argvals = uniform_grid(25);
295 let a = make_sample(15, &argvals, 0.0, 10);
296 let b = make_sample(15, &argvals, 0.0, 20); let res = t_perm_test(&a, &b, &argvals, 199, 7).unwrap();
298 assert!(
299 res.p_value > 0.1,
300 "null samples should give large p, got {}",
301 res.p_value
302 );
303 }
304
305 #[test]
306 fn t_perm_deterministic() {
307 let argvals = uniform_grid(20);
308 let a = make_sample(10, &argvals, 0.0, 3);
309 let b = make_sample(12, &argvals, 1.0, 4);
310 let r1 = t_perm_test(&a, &b, &argvals, 99, 123).unwrap();
311 let r2 = t_perm_test(&a, &b, &argvals, 99, 123).unwrap();
312 assert_eq!(r1, r2, "same seed must give bit-identical result");
313 }
314
315 #[test]
316 fn t_perm_invalid_input() {
317 let argvals = uniform_grid(20);
318 let a = make_sample(10, &argvals, 0.0, 5);
319 let argvals_b = uniform_grid(15);
321 let b = make_sample(10, &argvals_b, 0.0, 6);
322 assert!(matches!(
323 t_perm_test(&a, &b, &argvals, 99, 1),
324 Err(FdarError::InvalidDimension { .. })
325 ));
326 let b2 = make_sample(10, &argvals, 0.0, 7);
328 assert!(matches!(
329 t_perm_test(&a, &b2, &argvals, 0, 1),
330 Err(FdarError::InvalidParameter { .. })
331 ));
332 let a_small = make_sample(1, &argvals, 0.0, 8);
334 assert!(matches!(
335 t_perm_test(&a_small, &b2, &argvals, 99, 1),
336 Err(FdarError::InvalidDimension { .. })
337 ));
338 }
339
340 #[test]
341 fn f_perm_separated_small_p() {
342 let argvals = uniform_grid(25);
343 let a = make_sample(15, &argvals, 0.0, 11);
344 let b = make_sample(15, &argvals, 5.0, 12);
345 let res = f_perm_test(&a, &b, &argvals, 199, 42).unwrap();
346 assert!(
347 res.p_value < 0.05,
348 "separated samples should give small p, got {}",
349 res.p_value
350 );
351 }
352
353 #[test]
354 fn f_perm_null_large_p() {
355 let argvals = uniform_grid(25);
356 let a = make_sample(15, &argvals, 0.0, 30);
357 let b = make_sample(15, &argvals, 0.0, 40);
358 let res = f_perm_test(&a, &b, &argvals, 199, 7).unwrap();
359 assert!(
360 res.p_value > 0.1,
361 "null samples should give large p, got {}",
362 res.p_value
363 );
364 }
365
366 #[test]
367 fn f_perm_deterministic() {
368 let argvals = uniform_grid(20);
369 let a = make_sample(10, &argvals, 0.0, 3);
370 let b = make_sample(12, &argvals, 1.0, 4);
371 let r1 = f_perm_test(&a, &b, &argvals, 99, 555).unwrap();
372 let r2 = f_perm_test(&a, &b, &argvals, 99, 555).unwrap();
373 assert_eq!(r1, r2);
374 }
375
376 #[test]
377 fn f_perm_agrees_with_fanova_decision() {
378 use crate::function_on_scalar::fanova;
379 let argvals = uniform_grid(25);
380 let a = make_sample(15, &argvals, 0.0, 111);
381 let b = make_sample(15, &argvals, 5.0, 112);
382 let n_a = 15usize;
384 let n_b = 15usize;
385 let m = argvals.len();
386 let mut pooled = FdMatrix::zeros(n_a + n_b, m);
387 for j in 0..m {
388 for i in 0..n_a {
389 pooled[(i, j)] = a[(i, j)];
390 }
391 for i in 0..n_b {
392 pooled[(n_a + i, j)] = b[(i, j)];
393 }
394 }
395 let groups: Vec<usize> = (0..(n_a + n_b)).map(|i| usize::from(i >= n_a)).collect();
396 let fa = fanova(&pooled, &groups, 199).unwrap();
397 let fp = f_perm_test(&a, &b, &argvals, 199, 42).unwrap();
398 assert!(fa.p_value < 0.05);
400 assert!(fp.p_value < 0.05);
401 }
402
403 #[test]
404 fn f_perm_invalid_input() {
405 let argvals = uniform_grid(20);
406 let a = make_sample(10, &argvals, 0.0, 5);
407 let b2 = make_sample(10, &argvals, 0.0, 7);
408 assert!(matches!(
409 f_perm_test(&a, &b2, &argvals, 0, 1),
410 Err(FdarError::InvalidParameter { .. })
411 ));
412 let a_small = make_sample(1, &argvals, 0.0, 8);
413 assert!(matches!(
414 f_perm_test(&a_small, &b2, &argvals, 99, 1),
415 Err(FdarError::InvalidDimension { .. })
416 ));
417 }
418}