1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum PropTrueNullMethod {
12 Lfdr,
14 Mean,
16 Hist,
18 Convest,
20}
21
22pub fn prop_true_null(p: &[f64], method: PropTrueNullMethod, nbins: usize) -> f64 {
27 match method {
28 PropTrueNullMethod::Lfdr => by_local_fdr(p),
29 PropTrueNullMethod::Mean => by_mean_p(p),
30 PropTrueNullMethod::Hist => from_histogram(p, nbins),
31 PropTrueNullMethod::Convest => convest(p, 100, 1e-6),
32 }
33}
34
35fn by_local_fdr(p: &[f64]) -> f64 {
37 let n = p.len();
38 let mut ps = p.to_vec();
39 ps.sort_by(|a, b| b.partial_cmp(a).unwrap()); let nf = n as f64;
41 let n1 = (n + 1) as f64;
42 let mut s = 0.0;
43 for (k, &pv) in ps.iter().enumerate() {
44 let i = (n - k) as f64; let q = (nf / i * pv).min(1.0);
46 s += i * q;
47 }
48 s / nf / n1 * 2.0
49}
50
51fn by_mean_p(p: &[f64]) -> f64 {
53 let n = p.len();
54 let mut ps = p.to_vec();
55 ps.sort_by(|a, b| a.partial_cmp(b).unwrap()); let nf = n as f64;
57 let mut s = 0.0;
58 for (k, &pv) in ps.iter().enumerate() {
59 let i = (k + 1) as f64;
60 s += pv.min((i - 0.5) / nf);
61 }
62 2.0 * s / nf
63}
64
65fn from_histogram(p: &[f64], nbins: usize) -> f64 {
67 let nb = nbins as f64;
68 let mut counts = vec![0.0_f64; nbins];
70 for &pv in p {
71 for t in 1..=nbins {
72 if pv <= t as f64 / nb {
73 counts[t - 1] += 1.0;
74 break;
75 }
76 }
77 }
78 let mut tail_means = vec![0.0_f64; nbins];
80 let mut cum = 0.0;
81 for k in (0..nbins).rev() {
82 cum += counts[k];
83 tail_means[k] = cum / (nbins - k) as f64;
84 }
85 let mut idx = nbins - 1;
88 for k in 0..nbins {
89 if tail_means[k] >= counts[k] {
90 idx = k;
91 break;
92 }
93 }
94 tail_means[idx] / tail_means[0]
95}
96
97pub fn convest(p: &[f64], niter: usize, tol: f64) -> f64 {
104 let m = p.len();
105 if m == 0 {
106 return f64::NAN;
107 }
108 let ny = tol;
109 let mut pv: Vec<f64> = p.to_vec();
110 pv.sort_by(|a, b| a.partial_cmp(b).unwrap());
111 let p = pv; let p_c: Vec<f64> = p.iter().map(|&v| (100.0 * v).ceil() / 100.0).collect();
115
116 let x_grid: Vec<f64> = (0..=100).map(|t| t as f64 / 100.0).collect();
118
119 let mut f_hat = vec![1.0_f64; 101]; let mut f_hat_p = vec![1.0_f64; m]; let mut theta_hat = argmax_theta(&p, None);
123 let mut f_theta_hat = triangular(theta_hat, &x_grid);
124 let mut f_theta_hat_p = triangular(theta_hat, &p);
125
126 let mut pi0 = f_hat[100];
127
128 for _ in 0..niter {
129 let f_hat_diff: Vec<f64> = (0..m).map(|i| f_hat_p[i] - f_theta_hat_p[i]).collect();
130
131 let s: f64 = (0..m).map(|i| f_hat_diff[i] / f_hat_p[i]).sum();
132 let eps = if s > 0.0 {
133 0.0
134 } else {
135 let mut l = 0.0_f64;
136 let mut u = 1.0_f64;
137 let mut e = 0.5_f64;
138 while (u - l).abs() > ny {
139 e = (l + u) / 2.0;
140 let cond: f64 = (0..m)
141 .filter(|&i| f_hat_p[i] > 0.0)
142 .map(|i| f_hat_diff[i] / ((1.0 - e) * f_hat_p[i] + e * f_theta_hat_p[i]))
143 .sum();
144 if cond < 0.0 {
145 l = e;
146 } else {
147 u = e;
148 }
149 }
150 e
151 };
152
153 for k in 0..101 {
154 f_hat[k] = (1.0 - eps) * f_hat[k] + eps * f_theta_hat[k];
155 }
156 pi0 = f_hat[100];
157
158 for i in 0..m {
160 let idx_f = (100.0 * p[i]).floor() as usize;
161 let idx_c = (100.0 * p[i]).ceil() as usize;
162 let ff = f_hat[idx_f];
163 let fc = f_hat[idx_c];
164 f_hat_p[i] = 100.0 * (ff - fc) * (p_c[i] - p[i]) + fc;
165 }
166
167 theta_hat = argmax_theta(&p, Some(&f_hat_p));
168 f_theta_hat = triangular(theta_hat, &x_grid);
169 f_theta_hat_p = triangular(theta_hat, &p);
170
171 let s1: f64 = (0..m).map(|i| f_theta_hat_p[i] / f_hat_p[i]).sum();
173 let s2: f64 = (0..m).map(|i| 1.0 / f_hat_p[i]).sum();
174 if s1 < s2 {
175 f_theta_hat = vec![1.0; 101];
176 f_theta_hat_p = vec![1.0; m];
177 }
178 }
179 pi0
180}
181
182fn argmax_theta(p: &[f64], weights: Option<&[f64]>) -> f64 {
186 let mut best_val = f64::NEG_INFINITY;
187 let mut best_t = 1usize;
188 for t in 1..=100 {
189 let theta = t as f64 / 100.0;
190 let t2 = theta * theta;
191 let mut s = 0.0;
192 for (i, &pi) in p.iter().enumerate() {
193 if pi < theta {
194 let term = 2.0 * (theta - pi) / t2;
195 s += match weights {
196 Some(w) => term / w[i],
197 None => term,
198 };
199 }
200 }
201 if s > best_val {
202 best_val = s;
203 best_t = t;
204 }
205 }
206 best_t as f64 / 100.0
207}
208
209fn triangular(theta: f64, xs: &[f64]) -> Vec<f64> {
211 let t2 = theta * theta;
212 xs.iter()
213 .map(|&x| {
214 if x < theta {
215 2.0 * (theta - x) / t2
216 } else {
217 0.0
218 }
219 })
220 .collect()
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 const P: [f64; 30] = [
230 0.0001, 0.0007, 0.0023, 0.0041, 0.0089, 0.012, 0.018, 0.027, 0.035, 0.052, 0.071, 0.098,
231 0.13, 0.17, 0.21, 0.28, 0.33, 0.41, 0.47, 0.55, 0.58, 0.63, 0.69, 0.74, 0.78, 0.83, 0.88,
232 0.91, 0.95, 0.99,
233 ];
234
235 fn close(a: f64, b: f64, tol: f64) -> bool {
236 (a - b).abs() <= tol + tol * b.abs()
237 }
238
239 #[test]
240 fn closed_form_methods_match_r() {
241 assert!(
242 close(
243 prop_true_null(&P, PropTrueNullMethod::Lfdr, 20),
244 0.700587096774194,
245 1e-12
246 ),
247 "lfdr: {}",
248 prop_true_null(&P, PropTrueNullMethod::Lfdr, 20)
249 );
250 assert!(
251 close(
252 prop_true_null(&P, PropTrueNullMethod::Mean, 20),
253 0.723495555555556,
254 1e-12
255 ),
256 "mean: {}",
257 prop_true_null(&P, PropTrueNullMethod::Mean, 20)
258 );
259 assert!(
260 close(
261 prop_true_null(&P, PropTrueNullMethod::Hist, 20),
262 0.666666666666667,
263 1e-12
264 ),
265 "hist20: {}",
266 prop_true_null(&P, PropTrueNullMethod::Hist, 20)
267 );
268 assert!(
269 close(
270 prop_true_null(&P, PropTrueNullMethod::Hist, 10),
271 0.666666666666667,
272 1e-12
273 ),
274 "hist10: {}",
275 prop_true_null(&P, PropTrueNullMethod::Hist, 10)
276 );
277 }
278
279 #[test]
280 fn convest_matches_r() {
281 assert!(
282 close(convest(&P, 100, 1e-6), 0.654984323974808, 1e-7),
283 "convest100: {}",
284 convest(&P, 100, 1e-6)
285 );
286 assert!(
287 close(convest(&P, 50, 1e-6), 0.654639572943371, 1e-7),
288 "convest50: {}",
289 convest(&P, 50, 1e-6)
290 );
291 assert!(
292 close(
293 prop_true_null(&P, PropTrueNullMethod::Convest, 20),
294 0.654984323974808,
295 1e-7
296 ),
297 "convest via dispatch: {}",
298 prop_true_null(&P, PropTrueNullMethod::Convest, 20)
299 );
300 }
301
302 #[test]
303 fn convest_empty_is_nan() {
304 assert!(convest(&[], 100, 1e-6).is_nan());
305 }
306}