1use std::f64::consts::LN_2;
29
30use rayon::prelude::*;
31
32use crate::core::dgelist::DgeList;
33use crate::core::expression::{ave_log_cpm, column_sums};
34use crate::glm::deviance::unit_nb_deviance;
35use crate::glm::one_group::mglm_one_group;
36use crate::numeric::dist::nbinom_ln_pmf;
37use crate::numeric::dist::{beta_cdf, beta_ppf, beta_sf, chisq_sf, gamma_cdf, gamma_ppf};
38use crate::numeric::gamma::ln_gamma;
39use crate::prelude::*;
40
41pub const BIG_COUNT_DEFAULT: f64 = 900.0;
51
52pub const PRIOR_COUNT_DEFAULT: f64 = 0.125;
57
58const AVE_LOG_CPM_PRIOR: f64 = 2.0;
63
64const Q2Q_EPS: f64 = 1e-14;
69
70const Q2Q_ZERO_NUDGE: f64 = 0.25;
75
76const GAMMA_ISF_REL_TOL: f64 = 1e-15;
81
82const GAMMA_ISF_MAX_ITER: usize = 256;
87
88const LENTZ_TINY: f64 = 1e-300;
94
95const LENTZ_EPS: f64 = 2.220446049250313e-16;
97
98const BINOM_TIE_TOL: f64 = 1.0 + 1e-7;
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub enum RejectionRegion {
118 DoubleTail,
121 Deviance,
124 SmallP,
126}
127
128#[derive(Clone, Debug)]
134pub struct ExactTestResult {
135 pub log_fc: Vec<f64>,
137 pub log_cpm: Vec<f64>,
139 pub p_value: Vec<f64>,
141}
142
143#[derive(Clone, Copy, Debug)]
149pub struct ExactTestParams {
150 pub rejection_region: RejectionRegion,
152 pub big_count: f64,
155 pub prior_count: f64,
157}
158
159impl Default for ExactTestParams {
160 fn default() -> Self {
162 Self {
163 rejection_region: RejectionRegion::DoubleTail,
164 big_count: BIG_COUNT_DEFAULT,
165 prior_count: PRIOR_COUNT_DEFAULT,
166 }
167 }
168}
169
170impl ExactTestParams {
171 pub fn new(rejection_region: RejectionRegion, big_count: f64, prior_count: f64) -> Self {
184 Self {
185 rejection_region,
186 big_count,
187 prior_count,
188 }
189 }
190}
191
192fn gamma_cont_frac(a: f64, x: f64) -> f64 {
215 let mut b = x + 1.0 - a;
216 let mut c = 1.0 / LENTZ_TINY;
217 let mut d = 1.0 / b;
218 let mut h = d;
219 for i in 1..=GAMMA_ISF_MAX_ITER * 4 {
220 let i = i as f64;
221 let an = -i * (i - a);
222 b += 2.0;
223 d = an * d + b;
224 if d.abs() < LENTZ_TINY {
225 d = LENTZ_TINY;
226 }
227 c = b + an / c;
228 if c.abs() < LENTZ_TINY {
229 c = LENTZ_TINY;
230 }
231 d = 1.0 / d;
232 let delta = d * c;
233 h *= delta;
234 if (delta - 1.0).abs() <= LENTZ_EPS {
235 break;
236 }
237 }
238 h
239}
240
241fn ln_reg_gamma_upper(a: f64, x: f64) -> Result<f64, EdgeErrors> {
262 if x <= 0.0 {
263 return Ok(0.0);
264 }
265 if x < a + 1.0 {
266 return Ok(chisq_sf(2.0 * x, 2.0 * a)?.ln());
268 }
269 Ok(a * x.ln() - x - ln_gamma(a) + gamma_cont_frac(a, x).ln())
270}
271
272fn gamma_isf_start(ln_q: f64, a: f64) -> f64 {
291 let c = -ln_q - ln_gamma(a);
292 if !c.is_finite() || c <= a {
293 return a.max(f64::MIN_POSITIVE);
294 }
295 let mut x = c.max(a + 1.0);
296 for _ in 0..8 {
297 let next = c + (a - 1.0) * x.ln();
298 if !(next.is_finite() && next > 0.0) {
299 break;
300 }
301 x = next;
302 }
303 x
304}
305
306fn ln_reg_gamma_upper_inv(ln_q: f64, a: f64) -> Result<f64, EdgeErrors> {
322 if ln_q >= 0.0 {
323 return Ok(0.0);
324 }
325 if ln_q == f64::NEG_INFINITY {
326 return Ok(f64::INFINITY);
327 }
328 let ln_norm = ln_gamma(a);
329
330 let residual =
332 |t: f64| -> Result<f64, EdgeErrors> { Ok(ln_reg_gamma_upper(a, t.exp())? - ln_q) };
333
334 let mut t = gamma_isf_start(ln_q, a).ln();
335 let mut lo = t;
336 let mut hi = t;
337 if residual(t)? > 0.0 {
338 for _ in 0..GAMMA_ISF_MAX_ITER {
340 hi += 1.0;
341 if residual(hi)? <= 0.0 {
342 break;
343 }
344 lo = hi;
345 }
346 } else {
347 for _ in 0..GAMMA_ISF_MAX_ITER {
348 lo -= 1.0;
349 if residual(lo)? > 0.0 {
350 break;
351 }
352 hi = lo;
353 }
354 }
355
356 for _ in 0..GAMMA_ISF_MAX_ITER {
357 if !(t > lo && t < hi) {
358 t = 0.5 * (lo + hi);
359 }
360 let f = residual(t)?;
361 if f > 0.0 {
363 lo = t;
364 } else {
365 hi = t;
366 }
367 let tol = GAMMA_ISF_REL_TOL * (1.0 + t.abs());
368 if hi - lo <= tol {
369 return Ok(t.exp());
370 }
371 let slope = (a * t - t.exp() - ln_norm - (f + ln_q)).exp();
373 let t_next = if slope > 0.0 && slope.is_finite() {
374 t + f / slope
375 } else {
376 0.5 * (lo + hi)
377 };
378 let step = (t_next - t).abs();
379 t = t_next;
380 if step <= tol {
381 return Ok(t.clamp(lo, hi).exp());
382 }
383 }
384
385 Err(EdgeErrors::NoConvergence {
386 routine: "ln_reg_gamma_upper_inv",
387 iterations: GAMMA_ISF_MAX_ITER,
388 last_delta: hi - lo,
389 })
390}
391
392fn check_dispersion(dispersion: f64) -> Result<(), EdgeErrors> {
406 if !dispersion.is_finite() || dispersion < 0.0 {
407 return Err(EdgeErrors::InvalidDispersion(dispersion));
408 }
409 Ok(())
410}
411
412fn check_non_negative(name: &str, values: &[f64]) -> Result<(), EdgeErrors> {
423 if let Some(bad) = values.iter().find(|v| !v.is_finite() || **v < 0.0) {
424 return Err(EdgeErrors::InvalidArgument(format!(
425 "'{name}' must be non-negative and finite, found {bad}"
426 )));
427 }
428 Ok(())
429}
430
431#[inline]
450fn q2q_one(x: f64, input_mean: f64, output_mean: f64, dispersion: f64) -> Result<f64, EdgeErrors> {
451 let (mi, mo) = if input_mean < Q2Q_EPS || output_mean < Q2Q_EPS {
454 (input_mean + Q2Q_ZERO_NUDGE, output_mean + Q2Q_ZERO_NUDGE)
455 } else {
456 (input_mean, output_mean)
457 };
458
459 let ri = 1.0 + dispersion * mi;
460 let vi = mi * ri;
461 let ro = 1.0 + dispersion * mo;
462 let vo = mo * ro;
463 let q1 = mo + (vo / vi).sqrt() * (x - mi);
464
465 let shape_i = mi / ri;
466 let shape_o = mo / ro;
467 let q2 = if x >= mi {
468 let ln_q = ln_reg_gamma_upper(shape_i, x / ri)?;
469 ro * ln_reg_gamma_upper_inv(ln_q, shape_o)?
470 } else {
471 let p = gamma_cdf(x, shape_i, ri)?;
472 gamma_ppf(p, shape_o, ro)?
473 };
474
475 Ok(0.5 * (q1 + q2))
476}
477
478pub fn q2q_nbinom(
520 x: &[f64],
521 input_mean: &[f64],
522 output_mean: &[f64],
523 dispersion: f64,
524) -> Result<Vec<f64>, EdgeErrors> {
525 if input_mean.len() != x.len() {
526 return Err(EdgeErrors::LengthMismatch {
527 name: "input_mean",
528 expected: x.len(),
529 got: input_mean.len(),
530 });
531 }
532 if output_mean.len() != x.len() {
533 return Err(EdgeErrors::LengthMismatch {
534 name: "output_mean",
535 expected: x.len(),
536 got: output_mean.len(),
537 });
538 }
539 check_dispersion(dispersion)?;
540 check_non_negative("x", x)?;
541 check_non_negative("input_mean", input_mean)?;
542 check_non_negative("output_mean", output_mean)?;
543
544 x.iter()
545 .zip(input_mean.iter())
546 .zip(output_mean.iter())
547 .map(|((v, im), om)| q2q_one(*v, *im, *om, dispersion))
548 .collect()
549}
550
551fn group_columns(group: &[usize]) -> Vec<Vec<usize>> {
567 let mut labels: Vec<usize> = Vec::new();
568 let mut columns: Vec<Vec<usize>> = Vec::new();
569 for (sample, label) in group.iter().enumerate() {
570 match labels.iter().position(|l| l == label) {
571 Some(k) => columns[k].push(sample),
572 None => {
573 labels.push(*label);
574 columns.push(vec![sample]);
575 }
576 }
577 }
578 columns
579}
580
581fn select_columns<T: EdgeFloat>(
594 counts: &[T],
595 n_genes: usize,
596 n_samples: usize,
597 columns: &[usize],
598) -> Vec<f64> {
599 let mut out = vec![0.0_f64; n_genes * columns.len()];
600 out.par_chunks_mut(columns.len())
601 .enumerate()
602 .for_each(|(gene, row)| {
603 let src = &counts[gene * n_samples..(gene + 1) * n_samples];
604 for (dst, &col) in row.iter_mut().zip(columns.iter()) {
605 *dst = src[col].to_f64().unwrap_or(f64::NAN);
606 }
607 });
608 out
609}
610
611fn per_gene_dispersion(dispersion: &Recycled<f64>, n_genes: usize) -> Result<Vec<f64>, EdgeErrors> {
627 let out = match dispersion {
628 Recycled::Scalar(v) => vec![*v; n_genes],
629 Recycled::ByGene(v) => {
630 if v.len() != n_genes {
631 return Err(EdgeErrors::LengthMismatch {
632 name: "by_gene",
633 expected: n_genes,
634 got: v.len(),
635 });
636 }
637 v.clone()
638 }
639 Recycled::BySample(_) | Recycled::Full(_) => {
640 return Err(EdgeErrors::InvalidArgument(
641 "the exact test needs a dispersion that varies by gene only; got a \
642 sample-varying form"
643 .to_string(),
644 ));
645 }
646 };
647 for d in &out {
648 check_dispersion(*d)?;
649 }
650 Ok(out)
651}
652
653pub fn equalize_lib_sizes<T: EdgeFloat>(
682 counts: &[T],
683 n_genes: usize,
684 n_samples: usize,
685 group: Option<&[usize]>,
686 dispersion: &Recycled<f64>,
687 lib_size: Option<&[f64]>,
688) -> Result<Vec<f64>, EdgeErrors> {
689 if n_genes == 0 || n_samples == 0 {
690 return Err(EdgeErrors::EmptyCounts { n_genes, n_samples });
691 }
692 if counts.len() != n_genes * n_samples {
693 return Err(EdgeErrors::LengthMismatch {
694 name: "counts",
695 expected: n_genes * n_samples,
696 got: counts.len(),
697 });
698 }
699 let per_gene = per_gene_dispersion(dispersion, n_genes)?;
700
701 let ones = vec![0_usize; n_samples];
702 let group = match group {
703 Some(g) => {
704 if g.len() != n_samples {
705 return Err(EdgeErrors::LengthMismatch {
706 name: "group",
707 expected: n_samples,
708 got: g.len(),
709 });
710 }
711 g
712 }
713 None => &ones,
714 };
715
716 let derived: Vec<f64>;
717 let lib_size = match lib_size {
718 Some(l) => {
719 if l.len() != n_samples {
720 return Err(EdgeErrors::LengthMismatch {
721 name: "lib_size",
722 expected: n_samples,
723 got: l.len(),
724 });
725 }
726 l
727 }
728 None => {
729 derived = column_sums(counts, n_samples);
730 &derived
731 }
732 };
733 if let Some(bad) = lib_size.iter().find(|v| !v.is_finite() || **v <= 0.0) {
734 return Err(EdgeErrors::InvalidArgument(format!(
735 "library sizes must be positive and finite, found {bad}"
736 )));
737 }
738
739 let common_lib_size = (lib_size.iter().map(|l| l.ln()).sum::<f64>() / n_samples as f64).exp();
740
741 let mut input_mean = vec![0.0_f64; n_genes * n_samples];
744 let mut output_mean = vec![0.0_f64; n_genes * n_samples];
745 for columns in group_columns(group) {
746 let sub = select_columns(counts, n_genes, n_samples, &columns);
747 let offsets: Vec<f64> = columns.iter().map(|&j| lib_size[j].ln()).collect();
748 let beta = mglm_one_group(
749 &sub,
750 n_genes,
751 columns.len(),
752 dispersion,
753 &Recycled::by_sample(offsets),
754 None,
755 None,
756 None,
757 )?;
758 for (gene, b) in beta.iter().enumerate() {
759 let lambda = b.exp();
760 for &j in &columns {
761 input_mean[gene * n_samples + j] = lambda * lib_size[j];
762 output_mean[gene * n_samples + j] = lambda * common_lib_size;
763 }
764 }
765 }
766
767 let mut pseudo = vec![0.0_f64; n_genes * n_samples];
768 let outcome: Result<(), EdgeErrors> = pseudo
769 .par_chunks_mut(n_samples)
770 .enumerate()
771 .try_for_each(|(gene, row)| {
772 let y = &counts[gene * n_samples..(gene + 1) * n_samples];
773 let im = &input_mean[gene * n_samples..(gene + 1) * n_samples];
774 let om = &output_mean[gene * n_samples..(gene + 1) * n_samples];
775 let d = per_gene[gene];
776 for (j, out) in row.iter_mut().enumerate() {
777 let count = y[j].to_f64().unwrap_or(f64::NAN);
778 *out = q2q_one(count, im[j], om[j], d)?.max(0.0);
779 }
780 Ok(())
781 });
782 outcome?;
783
784 Ok(pseudo)
785}
786
787fn log_sum_exp(values: &[f64]) -> f64 {
804 let mut max = f64::NEG_INFINITY;
805 for v in values {
806 if *v > max {
807 max = *v;
808 }
809 }
810 if max == f64::NEG_INFINITY {
811 return f64::NEG_INFINITY;
812 }
813 let sum: f64 = values.iter().map(|v| (v - max).exp()).sum();
814 max + sum.ln()
815}
816
817#[inline]
830fn binom_ln_pmf(k: f64, n: f64, ln_p: f64, ln_q: f64) -> f64 {
831 ln_gamma(n + 1.0) - ln_gamma(k + 1.0) - ln_gamma(n - k + 1.0) + k * ln_p + (n - k) * ln_q
832}
833
834fn binom_test_one(y1: f64, y2: f64, p: f64) -> Result<f64, EdgeErrors> {
866 let size = y1 + y2;
867 if size <= 0.0 {
868 return Ok(1.0);
869 }
870 if p == 0.5 {
871 let k = y1.min(y2);
873 let cdf = beta_cdf(0.5, size - k, k + 1.0)?;
874 return Ok((2.0 * cdf).min(1.0));
875 }
876
877 let n = size as usize;
878 let ln_p = p.ln();
879 let ln_q = (-p).ln_1p();
880 let masses: Vec<f64> = (0..=n)
881 .map(|k| binom_ln_pmf(k as f64, size, ln_p, ln_q).exp())
882 .collect();
883
884 let threshold = masses[y1 as usize] * BINOM_TIE_TOL;
885 let mut kept: Vec<f64> = masses.into_iter().filter(|m| *m <= threshold).collect();
886 kept.sort_by(f64::total_cmp);
889 Ok(kept.iter().sum::<f64>().min(1.0))
890}
891
892fn beta_approx_one(
915 raw1: f64,
916 raw2: f64,
917 n1: usize,
918 n2: usize,
919 dispersion: f64,
920) -> Result<f64, EdgeErrors> {
921 let total = raw1 + raw2;
922 if total <= 0.0 {
923 return Ok(1.0);
924 }
925 let mu = total / (n1 + n2) as f64;
926 let alpha1 = n1 as f64 * mu / (1.0 + dispersion * mu);
927 let alpha2 = (n2 as f64 / n1 as f64) * alpha1;
928 let median = beta_ppf(0.5, alpha1, alpha2)?;
929
930 let left = (raw1 + 0.5) / total;
931 let right = (raw1 - 0.5) / total;
932 if left < median {
933 Ok((2.0 * beta_cdf(left, alpha1, alpha2)?).min(1.0))
934 } else if right > median {
935 Ok((2.0 * beta_sf(right, alpha1, alpha2)?).min(1.0))
936 } else {
937 Ok(1.0)
938 }
939}
940
941fn double_tail_one(
964 y1: &[f64],
965 y2: &[f64],
966 dispersion: f64,
967 big_count: f64,
968 buf: &mut Vec<f64>,
969) -> Result<f64, EdgeErrors> {
970 let (n1, n2) = (y1.len(), y2.len());
971 let raw1: f64 = y1.iter().sum();
972 let raw2: f64 = y2.iter().sum();
973 let s1 = raw1.round_ties_even();
975 let s2 = raw2.round_ties_even();
976
977 if dispersion <= 0.0 {
978 return binom_test_one(s1, s2, n1 as f64 / (n1 + n2) as f64);
979 }
980 if s1 > big_count && s2 > big_count {
981 return beta_approx_one(raw1, raw2, n1, n2, dispersion);
982 }
983
984 let total = s1 + s2;
985 if total == 0.0 {
986 return Ok(1.0);
987 }
988 let n_total = (n1 + n2) as f64;
989 let mu = total / n_total;
990 let mu1 = n1 as f64 * mu;
991 let mu2 = n2 as f64 * mu;
992 let size1 = n1 as f64 / dispersion;
993 let size2 = n2 as f64 / dispersion;
994 let size_total = n_total / dispersion;
995 let prob1 = size1 / (size1 + mu1);
998 let prob2 = size2 / (size2 + mu2);
999 let prob_total = size_total / (size_total + total);
1000
1001 let (lo, hi) = if s1 < mu1 {
1002 (0.0, s1)
1003 } else if s1 > mu1 {
1004 (s1, total)
1005 } else {
1006 return Ok(1.0);
1007 };
1008
1009 buf.clear();
1010 let mut x = lo;
1011 while x <= hi {
1012 buf.push(nbinom_ln_pmf(x, size1, prob1)? + nbinom_ln_pmf(total - x, size2, prob2)?);
1013 x += 1.0;
1014 }
1015 let ln_bottom = nbinom_ln_pmf(total, size_total, prob_total)?;
1016 Ok((LN_2 + log_sum_exp(buf) - ln_bottom).min(0.0).exp())
1017}
1018
1019fn deviance_one(
1044 y1: &[f64],
1045 y2: &[f64],
1046 dispersion: f64,
1047 buf: &mut Vec<f64>,
1048) -> Result<f64, EdgeErrors> {
1049 let (n1, n2) = (y1.len(), y2.len());
1050 let s1: f64 = y1.iter().sum::<f64>().round_ties_even();
1051 let s2: f64 = y2.iter().sum::<f64>().round_ties_even();
1052
1053 if dispersion <= 0.0 {
1054 return binom_test_one(s1, s2, n1 as f64 / (n1 + n2) as f64);
1055 }
1056 let total = s1 + s2;
1057 if total == 0.0 {
1058 return Ok(1.0);
1059 }
1060
1061 let n_total = (n1 + n2) as f64;
1062 let mu = total / n_total;
1063 let mu1 = n1 as f64 * mu;
1064 let mu2 = n2 as f64 * mu;
1065 let size1 = n1 as f64 / dispersion;
1066 let size2 = n2 as f64 / dispersion;
1067 let phi1 = 1.0 / size1;
1068 let phi2 = 1.0 / size2;
1069 let prob = size1 / (size1 + mu1);
1070
1071 let observed = unit_nb_deviance(s1, mu1, phi1) + unit_nb_deviance(s2, mu2, phi2);
1072 buf.clear();
1073
1074 let mut j = 0.0_f64;
1076 while j <= total {
1077 if observed > unit_nb_deviance(j, mu1, phi1) + unit_nb_deviance(total - j, mu2, phi2) {
1078 break;
1079 }
1080 buf.push(nbinom_ln_pmf(j, size1, prob)? + nbinom_ln_pmf(total - j, size2, prob)?);
1081 j += 1.0;
1082 }
1083 let mut k = 0.0_f64;
1085 while k <= total - j {
1086 if observed > unit_nb_deviance(k, mu2, phi2) + unit_nb_deviance(total - k, mu1, phi1) {
1087 break;
1088 }
1089 buf.push(nbinom_ln_pmf(k, size2, prob)? + nbinom_ln_pmf(total - k, size1, prob)?);
1090 k += 1.0;
1091 }
1092
1093 let size_total = size1 + size2;
1094 let ln_bottom = nbinom_ln_pmf(total, size_total, size_total / (size_total + mu1 + mu2))?;
1095 Ok((log_sum_exp(buf) - ln_bottom).min(0.0).exp())
1096}
1097
1098fn small_p_one(
1123 y1: &[f64],
1124 y2: &[f64],
1125 dispersion: f64,
1126 buf: &mut Vec<f64>,
1127) -> Result<f64, EdgeErrors> {
1128 let (n1, n2) = (y1.len(), y2.len());
1129 let s1: f64 = y1.iter().sum::<f64>().round_ties_even();
1130 let s2: f64 = y2.iter().sum::<f64>().round_ties_even();
1131
1132 if dispersion <= 0.0 {
1133 return binom_test_one(s1, s2, n1 as f64 / (n1 + n2) as f64);
1134 }
1135 let total = s1 + s2;
1136 if total == 0.0 {
1137 return Ok(1.0);
1138 }
1139
1140 let n_total = (n1 + n2) as f64;
1141 let mu = total / n_total;
1142 let size1 = n1 as f64 / dispersion;
1143 let size2 = n2 as f64 / dispersion;
1144 let prob = size1 / (size1 + n1 as f64 * mu);
1145
1146 let ln_observed = nbinom_ln_pmf(s1, size1, prob)? + nbinom_ln_pmf(s2, size2, prob)?;
1147 buf.clear();
1148 let mut x = 0.0_f64;
1149 while x <= total {
1150 let term = nbinom_ln_pmf(x, size1, prob)? + nbinom_ln_pmf(total - x, size2, prob)?;
1151 if term <= ln_observed {
1152 buf.push(term);
1153 }
1154 x += 1.0;
1155 }
1156
1157 let size_total = size1 + size2;
1158 let ln_bottom = nbinom_ln_pmf(total, size_total, size_total / (size_total + total))?;
1159 Ok((log_sum_exp(buf) - ln_bottom).min(0.0).exp())
1160}
1161
1162#[allow(clippy::too_many_arguments)]
1184fn exact_pvalues(
1185 y1: &[f64],
1186 y2: &[f64],
1187 n_genes: usize,
1188 n1: usize,
1189 n2: usize,
1190 dispersion: &[f64],
1191 region: RejectionRegion,
1192 big_count: f64,
1193) -> Result<Vec<f64>, EdgeErrors> {
1194 let region = if n1 == n2 {
1195 RejectionRegion::DoubleTail
1196 } else {
1197 region
1198 };
1199
1200 let mut out = vec![1.0_f64; n_genes];
1201 let outcome: Result<(), EdgeErrors> =
1202 out.par_iter_mut()
1203 .enumerate()
1204 .try_for_each_init(Vec::<f64>::new, |buf, (gene, p)| {
1205 let row1 = &y1[gene * n1..(gene + 1) * n1];
1206 let row2 = &y2[gene * n2..(gene + 1) * n2];
1207 let d = dispersion[gene];
1208 *p = match region {
1209 RejectionRegion::DoubleTail => double_tail_one(row1, row2, d, big_count, buf)?,
1210 RejectionRegion::Deviance => deviance_one(row1, row2, d, buf)?,
1211 RejectionRegion::SmallP => small_p_one(row1, row2, d, buf)?,
1212 };
1213 Ok(())
1214 });
1215 outcome?;
1216 Ok(out)
1217}
1218
1219fn augmented_abundance(
1238 counts: &[f64],
1239 n_genes: usize,
1240 lib: &[f64],
1241 mean_lib: f64,
1242 prior_count: f64,
1243 dispersion: &Recycled<f64>,
1244) -> Result<Vec<f64>, EdgeErrors> {
1245 let n_group = lib.len();
1246 let prior: Vec<f64> = lib.iter().map(|l| prior_count * l / mean_lib).collect();
1247 let offset: Vec<f64> = lib
1248 .iter()
1249 .zip(prior.iter())
1250 .map(|(l, p)| (l + 2.0 * p).ln())
1251 .collect();
1252
1253 let mut augmented = vec![0.0_f64; n_genes * n_group];
1254 augmented
1255 .par_chunks_mut(n_group)
1256 .enumerate()
1257 .for_each(|(gene, row)| {
1258 let src = &counts[gene * n_group..(gene + 1) * n_group];
1259 for (j, v) in row.iter_mut().enumerate() {
1260 *v = src[j] + prior[j];
1261 }
1262 });
1263
1264 mglm_one_group(
1265 &augmented,
1266 n_genes,
1267 n_group,
1268 dispersion,
1269 &Recycled::by_sample(offset),
1270 None,
1271 None,
1272 None,
1273 )
1274}
1275
1276fn interleave(y1: &[f64], y2: &[f64], n_genes: usize, n1: usize, n2: usize) -> Vec<f64> {
1290 let mut out = vec![0.0_f64; n_genes * (n1 + n2)];
1291 out.par_chunks_mut(n1 + n2)
1292 .enumerate()
1293 .for_each(|(gene, row)| {
1294 row[..n1].copy_from_slice(&y1[gene * n1..(gene + 1) * n1]);
1295 row[n1..].copy_from_slice(&y2[gene * n2..(gene + 1) * n2]);
1296 });
1297 out
1298}
1299
1300pub fn exact_test<T: EdgeFloat>(
1342 y: &DgeList<T>,
1343 pair: (usize, usize),
1344 dispersion: Option<&Recycled<f64>>,
1345 params: Option<ExactTestParams>,
1346) -> Result<ExactTestResult, EdgeErrors> {
1347 let params = params.unwrap_or_default();
1348 if !params.prior_count.is_finite() || params.prior_count < 0.0 {
1349 return Err(EdgeErrors::InvalidArgument(format!(
1350 "prior_count must be non-negative and finite, got {}",
1351 params.prior_count
1352 )));
1353 }
1354 if pair.0 == pair.1 {
1355 return Err(EdgeErrors::InvalidArgument(format!(
1356 "pair must name two different groups; got ({}, {})",
1357 pair.0, pair.1
1358 )));
1359 }
1360
1361 let group = y.group.as_ref().ok_or_else(|| {
1362 EdgeErrors::InvalidArgument("the exact test needs a group per sample".to_string())
1363 })?;
1364
1365 let owned;
1366 let dispersion = match dispersion {
1367 Some(d) => d,
1368 None => {
1369 owned = y
1370 .dispersion()
1371 .ok_or(EdgeErrors::InvalidDispersion(f64::NAN))?
1372 .0;
1373 &owned
1374 }
1375 };
1376 let per_gene = per_gene_dispersion(dispersion, y.n_genes)?;
1377
1378 let columns1: Vec<usize> = (0..y.n_samples).filter(|&j| group[j] == pair.0).collect();
1379 let columns2: Vec<usize> = (0..y.n_samples).filter(|&j| group[j] == pair.1).collect();
1380 if columns1.is_empty() || columns2.is_empty() {
1381 return Err(EdgeErrors::InvalidArgument(format!(
1382 "both groups of the pair need at least one sample; got {} and {}",
1383 columns1.len(),
1384 columns2.len()
1385 )));
1386 }
1387 let (n1, n2) = (columns1.len(), columns2.len());
1388
1389 let effective = y.norm_lib_sizes();
1390 if let Some(bad) = effective.iter().find(|v| !v.is_finite() || **v <= 0.0) {
1391 return Err(EdgeErrors::InvalidArgument(format!(
1392 "library sizes must be positive and finite, found {bad}"
1393 )));
1394 }
1395 let lib1: Vec<f64> = columns1.iter().map(|&j| effective[j]).collect();
1396 let lib2: Vec<f64> = columns2.iter().map(|&j| effective[j]).collect();
1397
1398 let n_pair = n1 + n2;
1399 let mean_lib = (lib1.iter().sum::<f64>() + lib2.iter().sum::<f64>()) / n_pair as f64;
1400 let ln_sum: f64 = lib1.iter().chain(lib2.iter()).map(|l| l.ln()).sum::<f64>();
1401 let lib_size_average = (ln_sum / n_pair as f64).exp();
1402
1403 let y1 = select_columns(&y.counts, y.n_genes, y.n_samples, &columns1);
1404 let y2 = select_columns(&y.counts, y.n_genes, y.n_samples, &columns2);
1405
1406 let abundance1 = augmented_abundance(
1408 &y1,
1409 y.n_genes,
1410 &lib1,
1411 mean_lib,
1412 params.prior_count,
1413 dispersion,
1414 )?;
1415 let abundance2 = augmented_abundance(
1416 &y2,
1417 y.n_genes,
1418 &lib2,
1419 mean_lib,
1420 params.prior_count,
1421 dispersion,
1422 )?;
1423 let log_fc: Vec<f64> = abundance1
1424 .iter()
1425 .zip(abundance2.iter())
1426 .map(|(a1, a2)| (a2 - a1) / LN_2)
1427 .collect();
1428
1429 let pooled = interleave(&y1, &y2, y.n_genes, n1, n2);
1432 let pooled_offset: Vec<f64> = lib1.iter().chain(lib2.iter()).map(|l| l.ln()).collect();
1433 let abundance = mglm_one_group(
1434 &pooled,
1435 y.n_genes,
1436 n_pair,
1437 dispersion,
1438 &Recycled::by_sample(pooled_offset),
1439 None,
1440 None,
1441 None,
1442 )?;
1443
1444 let mut eq1 = vec![0.0_f64; y.n_genes * n1];
1445 let mut eq2 = vec![0.0_f64; y.n_genes * n2];
1446 let outcome: Result<(), EdgeErrors> = eq1
1447 .par_chunks_mut(n1)
1448 .zip(eq2.par_chunks_mut(n2))
1449 .enumerate()
1450 .try_for_each(|(gene, (row1, row2))| {
1451 let rate = abundance[gene].exp();
1452 let d = per_gene[gene];
1453 let output_mean = rate * lib_size_average;
1454 for (j, out) in row1.iter_mut().enumerate() {
1455 *out = q2q_one(y1[gene * n1 + j], rate * lib1[j], output_mean, d)?;
1456 }
1457 for (j, out) in row2.iter_mut().enumerate() {
1458 *out = q2q_one(y2[gene * n2 + j], rate * lib2[j], output_mean, d)?;
1459 }
1460 Ok(())
1461 });
1462 outcome?;
1463
1464 let p_value = exact_pvalues(
1465 &eq1,
1466 &eq2,
1467 y.n_genes,
1468 n1,
1469 n2,
1470 &per_gene,
1471 params.rejection_region,
1472 params.big_count,
1473 )?;
1474
1475 let log_cpm = match &y.ave_log_cpm {
1476 Some(v) => {
1477 if v.len() != y.n_genes {
1478 return Err(EdgeErrors::LengthMismatch {
1479 name: "ave_log_cpm",
1480 expected: y.n_genes,
1481 got: v.len(),
1482 });
1483 }
1484 v.clone()
1485 }
1486 None => {
1487 let alc_dispersion = y.common_dispersion.map(Recycled::scalar);
1490 ave_log_cpm(
1491 &y.counts,
1492 y.n_genes,
1493 y.n_samples,
1494 Some(&effective),
1495 None,
1496 AVE_LOG_CPM_PRIOR,
1497 alc_dispersion.as_ref(),
1498 )?
1499 }
1500 };
1501
1502 Ok(ExactTestResult {
1503 log_fc,
1504 log_cpm,
1505 p_value,
1506 })
1507}
1508
1509#[cfg(test)]
1514mod tests {
1515 #![allow(clippy::excessive_precision)]
1518
1519 use super::*;
1520 use approx::assert_relative_eq;
1521
1522 const TOL: f64 = 1e-9;
1531
1532 #[rustfmt::skip]
1543 const COUNTS: [f64; 36] = [
1544 10.0, 12.0, 11.0, 40.0, 44.0, 38.0,
1545 50.0, 48.0, 52.0, 49.0, 51.0, 50.0,
1546 2.0, 0.0, 5.0, 1.0, 3.0, 0.0,
1547 0.0, 0.0, 0.0, 7.0, 9.0, 8.0,
1548 1200.0, 1100.0, 1300.0, 2400.0, 2500.0, 2300.0,
1549 5.0, 7.0, 6.0, 5.0, 6.0, 7.0,
1550 ];
1551
1552 const LIB: [f64; 6] = [1e6, 1.2e6, 0.9e6, 1.1e6, 1e6, 1.3e6];
1553 const NF: [f64; 6] = [0.95, 1.05, 1.0, 1.1, 0.9, 1.0];
1554 const DISP: [f64; 6] = [0.05, 0.1, 0.2, 0.15, 0.08, 0.12];
1555
1556 fn fixture(group: Vec<usize>) -> DgeList<f64> {
1567 let mut y = DgeList::new(COUNTS.to_vec(), 6, 6, Some(group)).unwrap();
1568 y.lib_size = LIB.to_vec();
1569 y.norm_factors = NF.to_vec();
1570 y
1571 }
1572
1573 fn assert_close(got: &[f64], want: &[f64]) {
1580 assert_eq!(got.len(), want.len());
1581 for (g, w) in got.iter().zip(want.iter()) {
1582 assert_relative_eq!(g, w, epsilon = 1e-12, max_relative = TOL);
1583 }
1584 }
1585
1586 const Q2Q_X: [f64; 8] = [0.0, 1.0, 5.0, 20.0, 100.0, 900.0, 3.0, 0.5];
1594 const Q2Q_IM: [f64; 8] = [2.0, 2.0, 10.0, 10.0, 50.0, 500.0, 3.0, 1e-16];
1595 const Q2Q_OM: [f64; 8] = [4.0, 4.0, 20.0, 20.0, 25.0, 800.0, 3.0, 1e-16];
1596
1597 #[test]
1599 fn test_q2q_nbinom_matches_edger_at_dispersion_one_tenth() {
1600 let want = [
1601 0.47247476834805369,
1602 2.48762930404220217,
1603 11.26359605754503690,
1604 37.16149924300670193,
1605 52.18843930258167774,
1606 1437.48127962309013128,
1607 3.00000000000000000,
1608 0.50000000000000011,
1609 ];
1610 let got = q2q_nbinom(&Q2Q_X, &Q2Q_IM, &Q2Q_OM, 0.1).unwrap();
1611 assert_close(&got, &want);
1612 }
1613
1614 #[test]
1616 fn test_q2q_nbinom_matches_edger_at_zero_dispersion() {
1617 let want = [
1618 0.58578643762690552,
1619 2.59345105979179325,
1620 12.74306177448739064,
1621 33.76355118458810267,
1622 61.97302934441376010,
1623 1295.83915428995442198,
1624 3.00000000000000000,
1625 0.50000000000000011,
1626 ];
1627 let got = q2q_nbinom(&Q2Q_X, &Q2Q_IM, &Q2Q_OM, 0.0).unwrap();
1628 assert_close(&got, &want);
1629 }
1630
1631 #[test]
1633 fn test_q2q_nbinom_matches_edger_at_a_large_dispersion() {
1634 let want = [
1635 0.12917130661302939,
1636 2.17053597295496514,
1637 10.19528650723413499,
1638 39.77185077921592438,
1639 50.12063954091983220,
1640 1439.89519676942063597,
1641 3.00000000000000044,
1642 0.49999999999999989,
1643 ];
1644 let got = q2q_nbinom(&Q2Q_X, &Q2Q_IM, &Q2Q_OM, 1.5).unwrap();
1645 assert_close(&got, &want);
1646 }
1647
1648 #[test]
1655 fn test_q2q_nbinom_survives_the_far_upper_tail() {
1656 let x = [6000.0, 6000.0, 2000.0];
1657 let im = [1000.0, 200.0, 40.0];
1658 let om = [1500.0, 400.0, 55.0];
1659
1660 let want = [8186.4210997511691, 8404.0259444343410, 2227.2071162950324];
1661 assert_close(&q2q_nbinom(&x, &im, &om, 0.001).unwrap(), &want);
1662
1663 let want = [8968.9264289783969, 11625.9467408884921, 2572.2470886452957];
1665 assert_close(&q2q_nbinom(&x, &im, &om, 0.05).unwrap(), &want);
1666 }
1667
1668 #[test]
1671 fn test_q2q_nbinom_matches_edger_in_the_far_lower_tail() {
1672 let x = [0.0, 0.0, 1.0];
1673 let im = [1000.0, 200.0, 40.0];
1674 let om = [1500.0, 400.0, 55.0];
1675
1676 let want = [65.3468031185425104, 47.2474768348053402, 5.9130723310523443];
1677 assert_close(&q2q_nbinom(&x, &im, &om, 0.001).unwrap(), &want);
1678
1679 let want = [2.4549984035979393, 4.5983158163212074, 2.8240360871062333];
1680 assert_close(&q2q_nbinom(&x, &im, &om, 0.05).unwrap(), &want);
1681 }
1682
1683 #[test]
1687 fn test_q2q_nbinom_is_the_identity_when_the_means_agree() {
1688 let x = [0.0, 3.0, 17.0, 900.0];
1689 let means = [5.0_f64; 4];
1690 let got = q2q_nbinom(&x, &means, &means, 0.1).unwrap();
1691 for (g, w) in got.iter().zip(x.iter()) {
1694 assert_relative_eq!(g, w, epsilon = 1e-9, max_relative = TOL);
1695 }
1696 }
1697
1698 #[test]
1700 fn test_q2q_nbinom_nudges_a_vanishing_mean() {
1701 let got = q2q_nbinom(&[0.5], &[1e-16], &[1e-16], 0.1).unwrap();
1702 assert_relative_eq!(got[0], 0.5, epsilon = 1e-12);
1703 }
1704
1705 #[test]
1706 fn test_q2q_nbinom_rejects_bad_input() {
1707 assert!(matches!(
1708 q2q_nbinom(&[1.0], &[1.0, 2.0], &[1.0], 0.1),
1709 Err(EdgeErrors::LengthMismatch { .. })
1710 ));
1711 assert!(matches!(
1712 q2q_nbinom(&[1.0], &[1.0], &[1.0, 2.0], 0.1),
1713 Err(EdgeErrors::LengthMismatch { .. })
1714 ));
1715 assert!(matches!(
1716 q2q_nbinom(&[1.0], &[1.0], &[1.0], -0.1),
1717 Err(EdgeErrors::InvalidDispersion(_))
1718 ));
1719 assert!(matches!(
1720 q2q_nbinom(&[1.0], &[1.0], &[1.0], f64::NAN),
1721 Err(EdgeErrors::InvalidDispersion(_))
1722 ));
1723 assert!(matches!(
1724 q2q_nbinom(&[-1.0], &[1.0], &[1.0], 0.1),
1725 Err(EdgeErrors::InvalidArgument(_))
1726 ));
1727 assert!(matches!(
1728 q2q_nbinom(&[1.0], &[-1.0], &[1.0], 0.1),
1729 Err(EdgeErrors::InvalidArgument(_))
1730 ));
1731 assert!(matches!(
1732 q2q_nbinom(&[1.0], &[1.0], &[-1.0], 0.1),
1733 Err(EdgeErrors::InvalidArgument(_))
1734 ));
1735 }
1736
1737 #[test]
1740 fn test_ln_reg_gamma_upper_agrees_with_the_natural_scale() {
1741 for &(a, x) in &[
1742 (0.5_f64, 0.1_f64),
1743 (1.0, 0.5),
1744 (2.0, 3.0),
1745 (5.0, 5.0),
1746 (5.0, 40.0),
1747 (14.3, 285.0),
1748 (100.0, 120.0),
1749 (500.0, 600.0),
1750 ] {
1751 let direct = chisq_sf(2.0 * x, 2.0 * a).unwrap();
1752 let logged = ln_reg_gamma_upper(a, x).unwrap();
1753 assert_relative_eq!(logged.exp(), direct, max_relative = 1e-11);
1754 }
1755 }
1756
1757 #[test]
1760 fn test_ln_reg_gamma_upper_inv_round_trips() {
1761 for &a in &[0.5_f64, 1.0, 3.0, 20.0, 500.0] {
1762 for &ln_q in &[-1e-3_f64, -0.7, -5.0, -50.0, -400.0, -1600.0] {
1763 let x = ln_reg_gamma_upper_inv(ln_q, a).unwrap();
1764 assert_relative_eq!(ln_reg_gamma_upper(a, x).unwrap(), ln_q, max_relative = 1e-9);
1765 }
1766 }
1767 }
1768
1769 #[rustfmt::skip]
1777 const EQ_COUNTS: [f64; 18] = [
1778 10.0, 12.0, 11.0, 40.0, 44.0, 38.0,
1779 50.0, 48.0, 52.0, 49.0, 51.0, 50.0,
1780 2.0, 0.0, 5.0, 1.0, 3.0, 0.0,
1781 ];
1782
1783 #[test]
1786 fn test_equalize_lib_sizes_matches_edger_with_a_tagwise_dispersion() {
1787 let want = [
1788 10.77396968053937343,
1789 10.71756939170353817,
1790 13.06148638116767380,
1791 39.09381908827660368,
1792 47.20616010342799029,
1793 31.18877655466048182,
1794 53.75710417620476989,
1795 42.91780303131315577,
1796 61.99137485252995816,
1797 47.89006600316915296,
1798 54.79292199365470140,
1799 41.23952895466592850,
1800 2.16482265343957536,
1801 0.00000000000000000,
1802 5.76615714775634647,
1803 0.97296861050989225,
1804 3.17215790436234890,
1805 0.00000000000000000,
1806 ];
1807 let dispersion = Recycled::by_gene(vec![0.05, 0.1, 0.2]);
1808 let got = equalize_lib_sizes(
1809 &EQ_COUNTS,
1810 3,
1811 6,
1812 Some(&[1, 1, 1, 2, 2, 2]),
1813 &dispersion,
1814 Some(&LIB),
1815 )
1816 .unwrap();
1817 assert_close(&got, &want);
1818
1819 let common = (LIB.iter().map(|l| l.ln()).sum::<f64>() / 6.0).exp();
1821 assert_relative_eq!(common, 1075127.4874964589, max_relative = 1e-12);
1822 }
1823
1824 #[test]
1826 fn test_equalize_lib_sizes_treats_a_missing_group_as_one_group() {
1827 let want = [
1828 10.96008842033811348,
1829 10.44646960127512614,
1830 13.59712013549795273,
1831 39.16595939215174837,
1832 46.92801915579765648,
1833 31.65690822951201966,
1834 53.72547236839000107,
1835 42.89019621190085019,
1836 61.80956593706329016,
1837 47.88264153989446470,
1838 54.78906141484367254,
1839 41.11794764355339282,
1840 2.14673596145325218,
1841 0.00000000000000000,
1842 5.61776154772958769,
1843 0.96792622133193928,
1844 3.18430182459055366,
1845 0.00000000000000000,
1846 ];
1847 let dispersion = Recycled::scalar(0.05);
1848 let got = equalize_lib_sizes(&EQ_COUNTS, 3, 6, None, &dispersion, Some(&LIB)).unwrap();
1849 assert_close(&got, &want);
1850 }
1851
1852 #[test]
1856 fn test_equalize_lib_sizes_falls_back_to_the_column_sums() {
1857 let want = [
1858 12.37402438212434674,
1859 15.12339607660574714,
1860 12.37382629390799593,
1861 33.87607295258574425,
1862 34.23385182961500561,
1863 32.88971374206387566,
1864 61.46657328525719777,
1865 60.98805703840496051,
1866 58.33745565015052392,
1867 41.49462344661865387,
1868 39.58747634774626079,
1869 43.34659093904294025,
1870 2.50600579756254671,
1871 0.14125440085394836,
1872 5.46619301702612859,
1873 0.81569267802383960,
1874 2.47919721221450740,
1875 0.00000000000000000,
1876 ];
1877 let dispersion = Recycled::scalar(0.05);
1878 let got = equalize_lib_sizes(
1879 &EQ_COUNTS,
1880 3,
1881 6,
1882 Some(&[1, 1, 1, 2, 2, 2]),
1883 &dispersion,
1884 None,
1885 )
1886 .unwrap();
1887 assert_close(&got, &want);
1888 }
1889
1890 #[test]
1891 fn test_equalize_lib_sizes_rejects_bad_input() {
1892 let d = Recycled::scalar(0.05);
1893 assert!(matches!(
1894 equalize_lib_sizes::<f64>(&[], 0, 6, None, &d, None),
1895 Err(EdgeErrors::EmptyCounts { .. })
1896 ));
1897 assert!(matches!(
1898 equalize_lib_sizes(&EQ_COUNTS[..12], 3, 6, None, &d, None),
1899 Err(EdgeErrors::LengthMismatch { name: "counts", .. })
1900 ));
1901 assert!(matches!(
1902 equalize_lib_sizes(&EQ_COUNTS, 3, 6, Some(&[1, 1, 2]), &d, None),
1903 Err(EdgeErrors::LengthMismatch { name: "group", .. })
1904 ));
1905 assert!(matches!(
1906 equalize_lib_sizes(&EQ_COUNTS, 3, 6, None, &d, Some(&[1e6, 1e6])),
1907 Err(EdgeErrors::LengthMismatch {
1908 name: "lib_size",
1909 ..
1910 })
1911 ));
1912 assert!(matches!(
1913 equalize_lib_sizes(
1914 &EQ_COUNTS,
1915 3,
1916 6,
1917 None,
1918 &d,
1919 Some(&[1e6, 0.0, 1e6, 1e6, 1e6, 1e6])
1920 ),
1921 Err(EdgeErrors::InvalidArgument(_))
1922 ));
1923 assert!(matches!(
1924 equalize_lib_sizes(&EQ_COUNTS, 3, 6, None, &Recycled::scalar(-0.1), None),
1925 Err(EdgeErrors::InvalidDispersion(_))
1926 ));
1927 assert!(matches!(
1928 equalize_lib_sizes(
1929 &EQ_COUNTS,
1930 3,
1931 6,
1932 None,
1933 &Recycled::by_gene(vec![0.1, 0.2]),
1934 None
1935 ),
1936 Err(EdgeErrors::LengthMismatch {
1937 name: "by_gene",
1938 ..
1939 })
1940 ));
1941 assert!(matches!(
1942 equalize_lib_sizes(
1943 &EQ_COUNTS,
1944 3,
1945 6,
1946 None,
1947 &Recycled::by_sample(vec![0.1; 6]),
1948 None
1949 ),
1950 Err(EdgeErrors::InvalidArgument(_))
1951 ));
1952 }
1953
1954 #[rustfmt::skip]
1966 const K_Y1: [f64; 12] = [
1967 10.0, 12.0, 50.0, 48.0, 2.0, 0.0, 0.0, 0.0, 300.0, 290.0, 5.0, 7.0,
1968 ];
1969 #[rustfmt::skip]
1970 const K_Y2: [f64; 24] = [
1971 40.0, 44.0, 38.0, 41.0,
1972 49.0, 51.0, 50.0, 52.0,
1973 1.0, 3.0, 0.0, 2.0,
1974 7.0, 9.0, 8.0, 6.0,
1975 100.0, 110.0, 105.0, 99.0,
1976 5.0, 6.0, 7.0, 5.0,
1977 ];
1978
1979 #[test]
1981 fn test_double_tail_matches_edger_on_unequal_groups() {
1982 let want = [
1983 1.0877946590376498e-05,
1984 9.6474841698552705e-01,
1985 9.6034839393473215e-01,
1986 3.2619499340000606e-04,
1987 1.8490293273658116e-05,
1988 1.0000000000000000e+00,
1989 ];
1990 let got = exact_pvalues(
1991 &K_Y1,
1992 &K_Y2,
1993 6,
1994 2,
1995 4,
1996 &DISP,
1997 RejectionRegion::DoubleTail,
1998 BIG_COUNT_DEFAULT,
1999 )
2000 .unwrap();
2001 assert_close(&got, &want);
2002 }
2003
2004 #[test]
2006 fn test_deviance_matches_edger_on_unequal_groups() {
2007 let want = [
2008 8.1344278851021571e-06,
2009 9.2093279234031145e-01,
2010 7.5779954527582549e-01,
2011 1.8059270195564577e-04,
2012 2.0166440897726341e-05,
2013 1.0000000000000000e+00,
2014 ];
2015 let got = exact_pvalues(
2016 &K_Y1,
2017 &K_Y2,
2018 6,
2019 2,
2020 4,
2021 &DISP,
2022 RejectionRegion::Deviance,
2023 BIG_COUNT_DEFAULT,
2024 )
2025 .unwrap();
2026 assert_close(&got, &want);
2027 }
2028
2029 #[test]
2035 fn test_small_p_matches_edger_called_one_gene_at_a_time() {
2036 let want = [
2037 1.1812148275602017e-05,
2038 9.9999999999999922e-01,
2039 1.0000000000000000e+00,
2040 3.7544809480160240e-04,
2041 1.4354611746487754e-05,
2042 8.9101578715497332e-01,
2043 ];
2044 let got = exact_pvalues(
2045 &K_Y1,
2046 &K_Y2,
2047 6,
2048 2,
2049 4,
2050 &DISP,
2051 RejectionRegion::SmallP,
2052 BIG_COUNT_DEFAULT,
2053 )
2054 .unwrap();
2055 assert_close(&got, &want);
2056 }
2057
2058 #[test]
2062 fn test_kernels_match_edger_at_a_common_dispersion() {
2063 let common = [0.1_f64; 6];
2064 let want_dt = [
2065 0.00043066236852219725,
2066 0.96474841698552704639,
2067 0.94916961181158332472,
2068 0.00014095556701738610,
2069 0.00011337582143935078,
2070 1.00000000000000000000,
2071 ];
2072 let got = exact_pvalues(
2073 &K_Y1,
2074 &K_Y2,
2075 6,
2076 2,
2077 4,
2078 &common,
2079 RejectionRegion::DoubleTail,
2080 BIG_COUNT_DEFAULT,
2081 )
2082 .unwrap();
2083 assert_close(&got, &want_dt);
2084
2085 let want_dev = [
2086 0.00033859092769447587,
2087 0.92093279234031144576,
2088 0.74358178328280177816,
2089 0.00007467786708842068,
2090 0.00012974328406972557,
2091 0.99999999999999977796,
2092 ];
2093 let got = exact_pvalues(
2094 &K_Y1,
2095 &K_Y2,
2096 6,
2097 2,
2098 4,
2099 &common,
2100 RejectionRegion::Deviance,
2101 BIG_COUNT_DEFAULT,
2102 )
2103 .unwrap();
2104 assert_close(&got, &want_dev);
2105 }
2106
2107 #[test]
2115 fn test_regions_coincide_when_the_groups_are_the_same_size() {
2116 #[rustfmt::skip]
2117 let z1 = [10.0, 12.0, 11.0, 2.0, 0.0, 5.0, 300.0, 290.0, 310.0];
2118 #[rustfmt::skip]
2119 let z2 = [40.0, 44.0, 38.0, 1.0, 3.0, 0.0, 100.0, 110.0, 105.0];
2120 let d = [0.05, 0.2, 0.08];
2121 let want = [
2122 8.2785897767219603e-07,
2123 6.1008651331231956e-01,
2124 1.8992725372229388e-05,
2125 ];
2126 for region in [
2127 RejectionRegion::DoubleTail,
2128 RejectionRegion::Deviance,
2129 RejectionRegion::SmallP,
2130 ] {
2131 let got = exact_pvalues(&z1, &z2, 3, 3, 3, &d, region, BIG_COUNT_DEFAULT).unwrap();
2132 assert_close(&got, &want);
2133 }
2134 }
2135
2136 #[test]
2143 fn test_beta_approximation_matches_edger() {
2144 let want = [
2145 3.1654679012061791e-03,
2146 2.7141288810736955e-110,
2147 1.0000000000000000e+00,
2148 ];
2149 let got = [
2150 beta_approx_one(3600.0, 7200.0, 3, 3, 0.08).unwrap(),
2151 beta_approx_one(9.0, 2880.0, 3, 3, 0.05).unwrap(),
2152 beta_approx_one(2880.0, 2880.0, 3, 3, 0.1).unwrap(),
2153 ];
2154 assert_close(&got, &want);
2155 }
2156
2157 #[test]
2160 fn test_big_count_threshold_switches_paths() {
2161 let y1 = [1200.0, 1100.0, 1300.0];
2162 let y2 = [2400.0, 2500.0, 2300.0];
2163 let mut buf = Vec::new();
2164 let exact = double_tail_one(&y1, &y2, 0.08, 1e12, &mut buf).unwrap();
2165 let approx = double_tail_one(&y1, &y2, 0.08, BIG_COUNT_DEFAULT, &mut buf).unwrap();
2166 assert_relative_eq!(approx, 3.1654679012061791e-03, max_relative = TOL);
2167 assert_relative_eq!(exact, approx, max_relative = 1e-2);
2169 assert!(exact != approx);
2170 }
2171
2172 #[test]
2182 fn test_zero_dispersion_uses_the_binomial_test() {
2183 let want = [
2184 2.3227510676278390e-11,
2185 8.5428783017505106e-01,
2186 1.0000000000000000e+00,
2187 6.6897430840626251e-06,
2188 8.1441784505498174e-61,
2189 1.0000000000000000e+00,
2190 ];
2191 let zero = [0.0_f64; 6];
2192 let got = exact_pvalues(
2193 &K_Y1,
2194 &K_Y2,
2195 6,
2196 2,
2197 4,
2198 &zero,
2199 RejectionRegion::DoubleTail,
2200 BIG_COUNT_DEFAULT,
2201 )
2202 .unwrap();
2203 assert_close(&got, &want);
2204 }
2205
2206 #[test]
2209 fn test_binom_test_matches_edger() {
2210 let y1 = [0.0, 3.0, 10.0, 50.0, 300.0];
2211 let y2 = [0.0, 5.0, 10.0, 60.0, 290.0];
2212
2213 let want_half = [
2214 1.00000000000000000,
2215 0.72656250000000033,
2216 1.00000000000000000,
2217 0.39092745669950468,
2218 0.71102572604547820,
2219 ];
2220 for (i, w) in want_half.iter().enumerate() {
2221 assert_relative_eq!(
2222 binom_test_one(y1[i], y2[i], 0.5).unwrap(),
2223 w,
2224 max_relative = TOL
2225 );
2226 }
2227
2228 let want_third = [
2233 1.0000000000000000e+00,
2234 1.0000000000000000e+00,
2235 1.5234223511142742e-01,
2236 8.4044576876146256e-03,
2237 2.2012099642833954e-18,
2238 ];
2239 for (i, w) in want_third.iter().enumerate() {
2240 assert_relative_eq!(
2241 binom_test_one(y1[i], y2[i], 1.0 / 3.0).unwrap(),
2242 w,
2243 max_relative = TOL
2244 );
2245 }
2246
2247 let want_quarter = [
2249 1.0000000000000000000,
2250 0.0035057067871093776,
2251 0.0010801099946617424,
2252 ];
2253 let a = [2.0, 7.0, 40.0];
2254 let b = [6.0, 3.0, 60.0];
2255 for (i, w) in want_quarter.iter().enumerate() {
2256 assert_relative_eq!(
2257 binom_test_one(a[i], b[i], 0.25).unwrap(),
2258 w,
2259 max_relative = TOL
2260 );
2261 }
2262 }
2263
2264 const A_LOG_CPM: [f64; 6] = [
2270 4.6828570480490503,
2271 5.6094058355452656,
2272 1.8369026364109835,
2273 2.4631896748603697,
2274 10.7199768972231873,
2275 2.8848509178259678,
2276 ];
2277
2278 #[test]
2285 fn test_exact_test_matches_edger_on_equal_groups() {
2286 let want_fc = [
2287 1.76837964949810367,
2288 -0.13190914581416660,
2289 -0.89031602850510549,
2290 5.98706174338887909,
2291 0.86708062515865070,
2292 -0.12249104113230144,
2293 ];
2294 let want_p = [
2295 3.4349237412116053e-06,
2296 7.6299000470704081e-01,
2297 4.6743523769487572e-01,
2298 1.4972774802797630e-05,
2299 1.0337909917591625e-02,
2300 8.9951524706410912e-01,
2301 ];
2302
2303 let y = fixture(vec![1, 1, 1, 2, 2, 2]);
2304 let dispersion = Recycled::by_gene(DISP.to_vec());
2305 for region in [
2306 RejectionRegion::DoubleTail,
2307 RejectionRegion::Deviance,
2308 RejectionRegion::SmallP,
2309 ] {
2310 let params = ExactTestParams::new(region, BIG_COUNT_DEFAULT, PRIOR_COUNT_DEFAULT);
2311 let got = exact_test(&y, (1, 2), Some(&dispersion), Some(params)).unwrap();
2312 assert_close(&got.log_fc, &want_fc);
2313 assert_close(&got.log_cpm, &A_LOG_CPM);
2314 assert_close(&got.p_value, &want_p);
2315 }
2316 }
2317
2318 #[test]
2321 fn test_exact_test_matches_edger_on_unequal_groups() {
2322 let want_fc = [
2323 1.617329610273079199,
2324 0.089998354795996716,
2325 1.144582075236848073,
2326 5.616562904007275314,
2327 0.900674912831392449,
2328 0.052897540463604717,
2329 ];
2330 let y = fixture(vec![1, 1, 2, 2, 2, 2]);
2331 let dispersion = Recycled::by_gene(DISP.to_vec());
2332
2333 let want_dt = [
2335 0.00017528190903009128,
2336 0.87366769045812053829,
2337 0.45299967984928735110,
2338 0.00158610472572541451,
2339 0.01631959757549012249,
2340 1.00000000000000000000,
2341 ];
2342 let got = exact_test(&y, (1, 2), Some(&dispersion), None).unwrap();
2343 assert_close(&got.log_fc, &want_fc);
2344 assert_close(&got.log_cpm, &A_LOG_CPM);
2345 assert_close(&got.p_value, &want_dt);
2346
2347 let want_dev = [
2349 0.00014592570324165402,
2350 0.84420783731055693000,
2351 0.32771177133536538717,
2352 0.00089863870338400599,
2353 0.01523232012423183027,
2354 1.00000000000000000000,
2355 ];
2356 let params = ExactTestParams::new(
2357 RejectionRegion::Deviance,
2358 BIG_COUNT_DEFAULT,
2359 PRIOR_COUNT_DEFAULT,
2360 );
2361 let got = exact_test(&y, (1, 2), Some(&dispersion), Some(params)).unwrap();
2362 assert_close(&got.p_value, &want_dev);
2363 }
2364
2365 #[test]
2369 fn test_exact_test_small_p_region_runs_on_unequal_groups() {
2370 let y = fixture(vec![1, 1, 2, 2, 2, 2]);
2371 let dispersion = Recycled::by_gene(DISP.to_vec());
2372 let params = ExactTestParams::new(
2373 RejectionRegion::SmallP,
2374 BIG_COUNT_DEFAULT,
2375 PRIOR_COUNT_DEFAULT,
2376 );
2377 let got = exact_test(&y, (1, 2), Some(&dispersion), Some(params)).unwrap();
2378 assert!(got.p_value.iter().all(|p| (0.0..=1.0).contains(p)));
2379 let double = exact_test(&y, (1, 2), Some(&dispersion), None).unwrap();
2380 assert!(got.p_value[0] != double.p_value[0]);
2381 }
2382
2383 #[test]
2387 fn test_exact_test_honours_a_lowered_big_count() {
2388 let want = [
2389 3.4349237412116053e-06,
2390 7.6605971775209569e-01,
2391 4.6743523769487572e-01,
2392 1.4972774802797630e-05,
2393 1.0337909917591625e-02,
2394 8.9951524706410912e-01,
2395 ];
2396 let y = fixture(vec![1, 1, 1, 2, 2, 2]);
2397 let dispersion = Recycled::by_gene(DISP.to_vec());
2398 let params = ExactTestParams::new(RejectionRegion::DoubleTail, 100.0, PRIOR_COUNT_DEFAULT);
2399 let got = exact_test(&y, (1, 2), Some(&dispersion), Some(params)).unwrap();
2400 assert_close(&got.p_value, &want);
2401 }
2402
2403 #[test]
2407 fn test_exact_test_honours_the_prior_count() {
2408 let want = [
2409 1.73637993337830987,
2410 -0.13094130138685259,
2411 -0.74393133794193356,
2412 4.05433589435307962,
2413 0.86689211695718937,
2414 -0.11516957613998995,
2415 ];
2416 let y = fixture(vec![1, 1, 1, 2, 2, 2]);
2417 let dispersion = Recycled::by_gene(DISP.to_vec());
2418 let params = ExactTestParams::new(RejectionRegion::DoubleTail, BIG_COUNT_DEFAULT, 0.5);
2419 let got = exact_test(&y, (1, 2), Some(&dispersion), Some(params)).unwrap();
2420 assert_close(&got.log_fc, &want);
2421 }
2422
2423 #[test]
2425 fn test_exact_test_pair_order_flips_the_fold_change() {
2426 let y = fixture(vec![1, 1, 1, 2, 2, 2]);
2427 let dispersion = Recycled::by_gene(DISP.to_vec());
2428 let forward = exact_test(&y, (1, 2), Some(&dispersion), None).unwrap();
2429 let reverse = exact_test(&y, (2, 1), Some(&dispersion), None).unwrap();
2430 for (f, r) in forward.log_fc.iter().zip(reverse.log_fc.iter()) {
2431 assert_relative_eq!(*f, -r, max_relative = 1e-10);
2432 }
2433 assert_close(&forward.p_value, &reverse.p_value);
2434 }
2435
2436 #[test]
2438 fn test_exact_test_falls_back_to_the_container_dispersion() {
2439 let mut y = fixture(vec![1, 1, 1, 2, 2, 2]);
2440 y.tagwise_dispersion = Some(DISP.to_vec());
2441 let got = exact_test(&y, (1, 2), None, None).unwrap();
2442 let dispersion = Recycled::by_gene(DISP.to_vec());
2443 let want = exact_test(&y, (1, 2), Some(&dispersion), None).unwrap();
2444 assert_close(&got.p_value, &want.p_value);
2445 }
2446
2447 #[test]
2449 fn test_exact_test_is_generic_over_the_count_type() {
2450 let counts32: Vec<f32> = COUNTS.iter().map(|v| *v as f32).collect();
2451 let mut y32 = DgeList::new(counts32, 6, 6, Some(vec![1, 1, 1, 2, 2, 2])).unwrap();
2452 y32.lib_size = LIB.to_vec();
2453 y32.norm_factors = NF.to_vec();
2454
2455 let dispersion = Recycled::by_gene(DISP.to_vec());
2456 let got = exact_test(&y32, (1, 2), Some(&dispersion), None).unwrap();
2457 let want = exact_test(
2458 &fixture(vec![1, 1, 1, 2, 2, 2]),
2459 (1, 2),
2460 Some(&dispersion),
2461 None,
2462 )
2463 .unwrap();
2464 for (g, w) in got.p_value.iter().zip(want.p_value.iter()) {
2465 assert_relative_eq!(g, w, epsilon = 1e-6, max_relative = 1e-5);
2466 }
2467 }
2468
2469 #[test]
2470 fn test_exact_test_rejects_bad_input() {
2471 let dispersion = Recycled::by_gene(DISP.to_vec());
2472
2473 let mut ungrouped = fixture(vec![1, 1, 1, 2, 2, 2]);
2475 ungrouped.group = None;
2476 assert!(matches!(
2477 exact_test(&ungrouped, (1, 2), Some(&dispersion), None),
2478 Err(EdgeErrors::InvalidArgument(_))
2479 ));
2480
2481 let y = fixture(vec![1, 1, 1, 2, 2, 2]);
2482 assert!(matches!(
2484 exact_test(&y, (1, 1), Some(&dispersion), None),
2485 Err(EdgeErrors::InvalidArgument(_))
2486 ));
2487 assert!(matches!(
2489 exact_test(&y, (1, 7), Some(&dispersion), None),
2490 Err(EdgeErrors::InvalidArgument(_))
2491 ));
2492 assert!(matches!(
2494 exact_test(&y, (1, 2), None, None),
2495 Err(EdgeErrors::InvalidDispersion(_))
2496 ));
2497 assert!(matches!(
2499 exact_test(&y, (1, 2), Some(&Recycled::scalar(-0.1)), None),
2500 Err(EdgeErrors::InvalidDispersion(_))
2501 ));
2502 assert!(matches!(
2504 exact_test(&y, (1, 2), Some(&Recycled::by_gene(vec![0.1, 0.2])), None),
2505 Err(EdgeErrors::LengthMismatch { .. })
2506 ));
2507 let params = ExactTestParams::new(RejectionRegion::DoubleTail, BIG_COUNT_DEFAULT, -1.0);
2509 assert!(matches!(
2510 exact_test(&y, (1, 2), Some(&dispersion), Some(params)),
2511 Err(EdgeErrors::InvalidArgument(_))
2512 ));
2513 let mut zero_lib = fixture(vec![1, 1, 1, 2, 2, 2]);
2515 zero_lib.lib_size[0] = 0.0;
2516 assert!(matches!(
2517 exact_test(&zero_lib, (1, 2), Some(&dispersion), None),
2518 Err(EdgeErrors::InvalidArgument(_))
2519 ));
2520 let mut bad_alc = fixture(vec![1, 1, 1, 2, 2, 2]);
2522 bad_alc.ave_log_cpm = Some(vec![1.0, 2.0]);
2523 assert!(matches!(
2524 exact_test(&bad_alc, (1, 2), Some(&dispersion), None),
2525 Err(EdgeErrors::LengthMismatch {
2526 name: "ave_log_cpm",
2527 ..
2528 })
2529 ));
2530 }
2531
2532 #[test]
2533 fn test_log_sum_exp_handles_an_empty_and_an_all_infinite_input() {
2534 assert_eq!(log_sum_exp(&[]), f64::NEG_INFINITY);
2535 assert_eq!(
2536 log_sum_exp(&[f64::NEG_INFINITY, f64::NEG_INFINITY]),
2537 f64::NEG_INFINITY
2538 );
2539 assert_relative_eq!(log_sum_exp(&[0.0, 0.0]), LN_2, max_relative = 1e-15);
2540 }
2541}