1use modewise_method::MethodError;
4use ndarray::{Array1, Array2, ArrayView2, Axis};
5use rand::SeedableRng;
6use rand::seq::SliceRandom;
7use rand_chacha::ChaCha8Rng;
8use rayon::prelude::*;
9
10use crate::dist::t_upper;
11use crate::glm::{Glm, welch_df, welch_t};
12use crate::nbs::{Component, Edge, components, largest};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Tail {
17 Greater,
19 Less,
21 Both,
23}
24
25const TIE: f64 = 1e-12;
29
30fn at_least(value: f64, observed: f64) -> bool {
31 value >= observed - TIE * observed.abs().max(1.0)
32}
33
34impl Tail {
35 fn stat(self, t: f64) -> f64 {
36 match self {
37 Self::Greater => t,
38 Self::Less => -t,
39 Self::Both => t.abs(),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Exchange {
47 Permute,
49 SignFlip,
51}
52
53#[derive(Debug, Clone)]
55pub enum Statistic {
56 Glm(Glm),
58 Welch { in_a: Vec<bool> },
60}
61
62#[derive(Debug, Clone)]
63pub struct PermutationTest {
64 pub statistic: Statistic,
65 pub exchange: Exchange,
66 pub tail: Tail,
67 pub n_permutations: usize,
68 pub seed: u64,
69 pub nbs: Option<(Vec<Option<Edge>>, f64)>,
71}
72
73#[derive(Debug, Clone, PartialEq)]
75pub struct TestResult {
76 pub t: Array1<f64>,
77 pub df: Array1<f64>,
78 pub p_parametric: Array1<f64>,
80 pub p_permutation: Array1<f64>,
82 pub p_fwer: Array1<f64>,
84 pub components: Vec<Component>,
85 pub n_rearrangements: usize,
86 pub exhaustive: bool,
87}
88
89impl PermutationTest {
90 fn t(&self, y: ArrayView2<f64>) -> Array1<f64> {
91 match &self.statistic {
92 Statistic::Glm(glm) => glm.t(y),
93 Statistic::Welch { in_a } => welch_t(y, in_a),
94 }
95 }
96
97 pub fn run(&self, y: ArrayView2<f64>) -> Result<TestResult, MethodError> {
100 let n = y.nrows();
101 let expected = match &self.statistic {
102 Statistic::Glm(glm) => glm.n_observations(),
103 Statistic::Welch { in_a } => in_a.len(),
104 };
105 if n != expected {
106 return Err(MethodError::ShapeMismatch(format!(
107 "{n} observations but the design has {expected}"
108 )));
109 }
110 if self.n_permutations == 0 {
111 return Err(MethodError::invalid("n_permutations", "must be at least 1"));
112 }
113 if let (Statistic::Welch { .. }, Exchange::SignFlip) = (&self.statistic, self.exchange) {
114 return Err(MethodError::invalid(
115 "exchange",
116 "Welch's t compares groups; use permutation",
117 ));
118 }
119 let base = match &self.statistic {
120 Statistic::Glm(glm) => glm.nuisance_residuals(y),
121 Statistic::Welch { .. } => y.to_owned(),
122 };
123 let t = self.t(y);
124 let observed: Vec<f64> = t.iter().map(|&v| self.tail.stat(v)).collect();
125 let exhaustive = self.exchange == Exchange::SignFlip
126 && n < 63
127 && (1u64 << n) - 1 <= self.n_permutations as u64;
128 let m = if exhaustive {
129 (1usize << n) - 1
130 } else {
131 self.n_permutations
132 };
133
134 let rearranged = |r: usize| -> Array2<f64> {
135 match (self.exchange, exhaustive) {
136 (Exchange::SignFlip, true) => {
137 let flips = r + 1; let mut out = base.clone();
139 for (i, mut row) in out.axis_iter_mut(Axis(0)).enumerate() {
140 if flips >> i & 1 == 1 {
141 row.mapv_inplace(|v| -v);
142 }
143 }
144 out
145 }
146 (Exchange::SignFlip, false) => {
147 let mut rng = ChaCha8Rng::seed_from_u64(self.seed);
148 rng.set_stream(r as u64);
149 let mut out = base.clone();
150 for mut row in out.axis_iter_mut(Axis(0)) {
151 if rand::Rng::random::<bool>(&mut rng) {
152 row.mapv_inplace(|v| -v);
153 }
154 }
155 out
156 }
157 (Exchange::Permute, _) => {
158 let mut rng = ChaCha8Rng::seed_from_u64(self.seed);
159 rng.set_stream(r as u64);
160 let mut order: Vec<usize> = (0..n).collect();
161 order.shuffle(&mut rng);
162 base.select(Axis(0), &order)
163 }
164 }
165 };
166
167 let features = observed.len();
168 let null: Vec<(Vec<bool>, f64, usize)> = (0..m)
169 .into_par_iter()
170 .map(|r| {
171 let stats: Vec<f64> = self
172 .t(rearranged(r).view())
173 .iter()
174 .map(|&v| self.tail.stat(v))
175 .collect();
176 let exceeds = stats
177 .iter()
178 .zip(&observed)
179 .map(|(s, o)| at_least(*s, *o))
180 .collect();
181 let max = stats
182 .iter()
183 .copied()
184 .filter(|v| !v.is_nan())
185 .fold(f64::NEG_INFINITY, f64::max);
186 let biggest = self
187 .nbs
188 .as_ref()
189 .map_or(0, |(edges, threshold)| largest(&stats, edges, *threshold));
190 (exceeds, max, biggest)
191 })
192 .collect();
193
194 let ratio = |count: usize| (count + 1) as f64 / (m + 1) as f64;
195 let nan_or = |o: f64, value: f64| if o.is_nan() { f64::NAN } else { value };
196 let p_permutation = Array1::from_shape_fn(features, |f| {
197 nan_or(
198 observed[f],
199 ratio(null.iter().filter(|(e, _, _)| e[f]).count()),
200 )
201 });
202 let p_fwer = Array1::from_shape_fn(features, |f| {
203 nan_or(
204 observed[f],
205 ratio(
206 null.iter()
207 .filter(|(_, max, _)| at_least(*max, observed[f]))
208 .count(),
209 ),
210 )
211 });
212 let found = match &self.nbs {
213 Some((edges, threshold)) => components(&observed, edges, *threshold)
214 .into_iter()
215 .map(|(network, edges)| {
216 let size = edges.len();
217 Component {
218 network,
219 edges,
220 p: ratio(null.iter().filter(|(_, _, b)| *b >= size).count()),
221 }
222 })
223 .collect(),
224 None => Vec::new(),
225 };
226
227 let df = match &self.statistic {
228 Statistic::Glm(glm) => Array1::from_elem(features, glm.degrees_of_freedom()),
229 Statistic::Welch { in_a } => welch_df(y, in_a),
230 };
231 let p_parametric = Array1::from_shape_fn(features, |f| {
232 let (tf, d) = (t[f], df[f]);
233 match self.tail {
234 Tail::Greater => t_upper(tf, d),
235 Tail::Less => t_upper(-tf, d),
236 Tail::Both => (2.0 * t_upper(tf.abs(), d)).min(1.0),
237 }
238 });
239 Ok(TestResult {
240 t,
241 df,
242 p_parametric,
243 p_permutation,
244 p_fwer,
245 components: found,
246 n_rearrangements: m,
247 exhaustive,
248 })
249 }
250}