use rayon::prelude::*;
use crate::limma::lowess::lowess;
use crate::numeric::stats::quantile_type7;
use crate::prelude::*;
use crate::ql::chebyshev::{compute_weight, unit_nb_deviance};
use crate::utils::design::hat_diagonal;
const THRESHOLD_ZERO: f64 = 1e-4;
const MIN_DF: f64 = 1e-8;
const PRIOR_SPAN: f64 = 0.5;
const PRIOR_ITERATIONS: usize = 3;
const PRIOR_QUANTILE: f64 = 0.9;
const PRIOR_FLOOR: f64 = 1.0;
#[derive(Clone, Debug)]
pub struct AdjustedDeviance {
pub deviance: Vec<f64>,
pub df: Vec<f64>,
pub s2: Vec<f64>,
}
#[allow(clippy::too_many_arguments)]
pub fn compute_adjust_vec<T: EdgeFloat>(
counts: &[T],
n_genes: usize,
n_samples: usize,
fitted: &[f64],
design: &[f64],
n_coef: usize,
dispersion: &Recycled<f64>,
prior: &[f64],
weights: Option<&Recycled<f64>>,
) -> Result<AdjustedDeviance, EdgeErrors> {
validate(
counts.len(),
n_genes,
n_samples,
fitted.len(),
design.len(),
n_coef,
)?;
dispersion.validate(n_genes, n_samples)?;
if let Some(w) = weights {
w.validate(n_genes, n_samples)?;
}
validate_prior(prior, n_genes)?;
let per_gene: Vec<(f64, f64, f64)> = (0..n_genes)
.into_par_iter()
.map_init(
|| Scratch::new(n_samples, n_coef),
|scratch, gene| {
let start = gene * n_samples;
for (slot, value) in scratch.y.iter_mut().zip(&counts[start..start + n_samples]) {
*slot = value.to_f64().unwrap_or(0.0);
}
adjust_one_gene(
&scratch.y,
&fitted[start..start + n_samples],
design,
n_coef,
dispersion.row(gene, n_samples),
weights.map(|w| w.row(gene, n_samples)),
if prior.len() == 1 {
prior[0]
} else {
prior[gene]
},
&mut scratch.weighted_design,
)
},
)
.collect();
let mut out = AdjustedDeviance {
deviance: Vec::with_capacity(n_genes),
df: Vec::with_capacity(n_genes),
s2: Vec::with_capacity(n_genes),
};
for (deviance, df, s2) in per_gene {
out.deviance.push(deviance);
out.df.push(df);
out.s2.push(s2);
}
Ok(out)
}
pub fn compute_prior(ave_log_cpm: &[f64], s2: &[f64], df: &[f64]) -> Result<Vec<f64>, EdgeErrors> {
let n = ave_log_cpm.len();
if s2.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "s2",
expected: n,
got: s2.len(),
});
}
if df.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "df",
expected: n,
got: df.len(),
});
}
let mut abundance = Vec::with_capacity(n);
let mut root_s2 = Vec::with_capacity(n);
for gene in 0..n {
if df[gene] > MIN_DF {
abundance.push(ave_log_cpm[gene]);
root_s2.push(s2[gene].max(0.0).sqrt().sqrt());
}
}
let trend = match root_s2.len() {
0 => return Ok(vec![PRIOR_FLOOR]),
1 => root_s2,
_ => {
lowess(&abundance, &root_s2, PRIOR_SPAN, PRIOR_ITERATIONS)?
}
};
let p = quantile_type7(&trend, PRIOR_QUANTILE)?.max(PRIOR_FLOOR);
Ok(vec![p * p * p * p])
}
#[allow(clippy::too_many_arguments)]
pub fn update_prior<T: EdgeFloat>(
counts: &[T],
n_genes: usize,
n_samples: usize,
fitted: &[f64],
design: &[f64],
n_coef: usize,
dispersion: &Recycled<f64>,
weights: Option<&Recycled<f64>>,
ave_log_cpm: &[f64],
) -> Result<Vec<f64>, EdgeErrors> {
if ave_log_cpm.len() != n_genes {
return Err(EdgeErrors::LengthMismatch {
name: "ave_log_cpm",
expected: n_genes,
got: ave_log_cpm.len(),
});
}
let mut prior = vec![PRIOR_FLOOR];
for _ in 0..2 {
let adjusted = compute_adjust_vec(
counts, n_genes, n_samples, fitted, design, n_coef, dispersion, &prior, weights,
)?;
prior = compute_prior(ave_log_cpm, &adjusted.s2, &adjusted.df)?;
}
Ok(prior)
}
struct Scratch {
y: Vec<f64>,
weighted_design: Vec<f64>,
}
impl Scratch {
fn new(n_samples: usize, n_coef: usize) -> Self {
Self {
y: vec![0.0; n_samples],
weighted_design: vec![0.0; n_samples * n_coef],
}
}
}
fn validate(
n_counts: usize,
n_genes: usize,
n_samples: usize,
n_fitted: usize,
n_design: usize,
n_coef: usize,
) -> Result<(), EdgeErrors> {
if n_genes == 0 || n_samples == 0 {
return Err(EdgeErrors::EmptyCounts { n_genes, n_samples });
}
if n_coef == 0 {
return Err(EdgeErrors::MustBePositive("n_coef".to_string()));
}
if n_counts != n_genes * n_samples {
return Err(EdgeErrors::LengthMismatch {
name: "counts",
expected: n_genes * n_samples,
got: n_counts,
});
}
if n_fitted != n_genes * n_samples {
return Err(EdgeErrors::LengthMismatch {
name: "fitted",
expected: n_genes * n_samples,
got: n_fitted,
});
}
if n_design != n_samples * n_coef {
return Err(EdgeErrors::LengthMismatch {
name: "design",
expected: n_samples * n_coef,
got: n_design,
});
}
if n_samples < n_coef {
return Err(EdgeErrors::DesignNotFullRank {
n_cols: n_coef,
rank: n_samples,
});
}
Ok(())
}
fn validate_prior(prior: &[f64], n_genes: usize) -> Result<(), EdgeErrors> {
if prior.len() != 1 && prior.len() != n_genes {
return Err(EdgeErrors::LengthMismatch {
name: "prior",
expected: n_genes,
got: prior.len(),
});
}
if let Some(&bad) = prior.iter().find(|p| !p.is_finite() || **p <= 0.0) {
return Err(EdgeErrors::InvalidArgument(format!(
"prior must be finite and positive; got {bad}."
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn adjust_one_gene(
y: &[f64],
mu: &[f64],
design: &[f64],
n_coef: usize,
dispersion: RecycledRow<'_, f64>,
weights: Option<RecycledRow<'_, f64>>,
prior: f64,
weighted_design: &mut [f64],
) -> (f64, f64, f64) {
let n_samples = y.len();
for (sample, &mu_j) in mu.iter().enumerate() {
let phi = dispersion.get(sample);
let w = weights.map_or(1.0, |w| w.get(sample));
let working = (mu_j / (1.0 + (mu_j * phi * w / prior))).sqrt();
let row = &design[sample * n_coef..(sample + 1) * n_coef];
let out = &mut weighted_design[sample * n_coef..(sample + 1) * n_coef];
for (slot, &x) in out.iter_mut().zip(row) {
*slot = x * working;
}
}
let hat = hat_diagonal(weighted_design, n_samples, n_coef)
.expect("weighted design shape was validated on entry");
let mut deviance = 0.0;
let mut df = 0.0;
for sample in 0..n_samples {
let phi = dispersion.get(sample);
let w = weights.map_or(1.0, |w| w.get(sample));
let (alpha, kappa) = compute_weight(mu[sample], phi, prior / w);
let mut unit = unit_nb_deviance(y[sample], mu[sample], phi * w / prior);
let mut complement = 1.0 - hat[sample];
if complement < THRESHOLD_ZERO {
unit = 0.0;
complement = 0.0;
}
deviance += (unit * alpha) * w;
df += complement * kappa;
}
let s2 = if df < THRESHOLD_ZERO {
0.0
} else {
deviance / df
};
(deviance, df, s2)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const TOL: f64 = 1e-12;
fn counts() -> Vec<f64> {
vec![
10.0, 12.0, 11.0, 40.0, 44.0, 38.0, 50.0, 48.0, 52.0, 49.0, 51.0, 50.0, 2.0, 0.0, 5.0, 1.0, 3.0, 0.0, 0.0, 0.0, 0.0, 20.0, 22.0, 18.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000.0, 1100.0, 900.0, 3000.0, 3200.0, 2800.0,
]
}
fn fitted() -> Vec<f64> {
vec![
11.0, 11.5, 10.75, 40.5, 41.25, 39.5, 50.0, 49.5, 50.25, 49.75, 50.5, 50.0, 2.5, 2.25, 2.75, 1.5, 1.25, 1.75, 0.0, 0.0, 0.0, 20.0, 20.0, 20.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000.0, 1050.0, 950.0, 3000.0, 3100.0, 2900.0,
]
}
fn two_group() -> Vec<f64> {
vec![
1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
]
}
fn continuous() -> Vec<f64> {
vec![
1.0, -1.5, 1.0, -0.5, 1.0, 0.5, 1.0, 1.5, 1.0, 2.5, 1.0, 3.5,
]
}
fn check(
design: &[f64],
dispersion: Recycled<f64>,
prior: &[f64],
weights: Option<&Recycled<f64>>,
deviance: &[f64],
df: &[f64],
s2: &[f64],
) {
let got = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
design,
2,
&dispersion,
prior,
weights,
)
.unwrap();
for gene in 0..6 {
assert_relative_eq!(got.deviance[gene], deviance[gene], max_relative = TOL);
assert_relative_eq!(got.df[gene], df[gene], max_relative = TOL);
assert_relative_eq!(got.s2[gene], s2[gene], max_relative = TOL);
}
}
#[test]
fn test_two_group_matches_edger() {
check(
&two_group(),
Recycled::scalar(0.125),
&[1.0],
None,
&[
0.087_678_574_155_993_14,
0.016_535_319_784_658_36,
11.401_625_661_199_109,
0.111_548_751_889_321_45,
0.0,
0.056_972_440_150_977_24,
],
&[
3.969_634_764_628_127,
3.999_429_434_054_224,
5.340_746_228_772_16,
1.992_229_288_606_397_7,
0.0,
4.001_942_003_921_18,
],
&[
0.022_087_315_169_965_472,
0.004_134_419_685_934_175,
2.134_837_562_544_204,
0.055_991_924_487_442_87,
0.0,
0.014_236_198_349_489_956,
],
);
}
#[test]
fn test_prior_above_one_matches_edger() {
check(
&two_group(),
Recycled::scalar(0.125),
&[2.5],
None,
&[
0.148_404_728_650_759_82,
0.034_023_627_382_698_46,
23.944_790_215_814_272,
0.187_876_878_253_122_5,
0.0,
0.141_051_250_719_609_08,
],
&[
3.944_608_502_302_763_5,
3.984_625_332_836_51,
9.508_568_001_899_926,
1.949_129_177_816_912_6,
0.0,
4.001_937_312_693_502,
],
&[
0.037_622_169_237_866_03,
0.008_538_726_866_567_972,
2.518_233_051_604_596_5,
0.096_390_162_535_841_08,
0.0,
0.035_245_742_173_975_886,
],
);
}
#[test]
fn test_per_gene_dispersion_matches_edger() {
check(
&two_group(),
Recycled::by_gene(vec![0.05, 0.125, 0.5, 0.25, 0.0, 2.0]),
&[1.5],
None,
&[
0.185_285_564_462_572_72,
0.023_166_881_640_206_673,
14.989_239_815_832_821,
0.086_845_649_266_648_48,
0.0,
0.005_175_385_846_847_329_5,
],
&[
3.954_646_135_693_23,
3.996_255_259_784_583_3,
8.051_884_164_459_716,
1.968_403_109_458_039_6,
0.0,
4.722_405_574_548_103_5,
],
&[
0.046_852_628_049_385_024,
0.005_797_147_613_001_947,
1.861_581_650_912_709_4,
0.044_119_849_663_598_47,
0.0,
0.001_095_921_509_736_607_7,
],
);
}
#[test]
fn test_weights_match_edger() {
let weights = Recycled::full(
vec![
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.5, 2.0, 2.0, 1.0, 1.0, 1.0, 0.25, 1.0, 1.0, 4.0, 1.0, 2.0, 2.0, 2.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.5, 1.0, 1.0,
],
6,
6,
)
.unwrap();
check(
&two_group(),
Recycled::scalar(0.125),
&[1.25],
Some(&weights),
&[
0.101_079_410_203_347_93,
0.019_878_961_611_844_97,
12.984_719_805_109_156,
0.093_938_439_126_561_25,
0.0,
0.071_201_367_293_380_21,
],
&[
3.954_058_331_932_422_7,
3.995_405_261_291_514,
6.298_973_859_851_691,
1.949_129_177_816_912_6,
0.0,
4.001_941_748_985_114,
],
&[
0.025_563_459_544_095_425,
0.004_975_455_632_608_117,
2.061_402_395_693_523_5,
0.048_195_081_267_920_54,
0.0,
0.017_791_705_067_030_712,
],
);
}
#[test]
fn test_continuous_design_matches_edger() {
check(
&continuous(),
Recycled::scalar(0.125),
&[1.0],
None,
&[
0.087_678_574_155_993_14,
0.016_535_319_784_658_36,
11.401_625_661_199_109,
0.111_548_751_889_321_45,
0.0,
0.056_972_440_150_977_24,
],
&[
3.968_981_768_078_912_3,
3.999_429_410_347_023_6,
5.390_514_507_550_15,
0.996_114_644_303_199,
0.0,
4.001_941_999_230_005,
],
&[
0.022_090_949_084_513_38,
0.004_134_419_710_441_55,
2.115_127_534_714_836,
0.111_983_848_974_885_72,
0.0,
0.014_236_198_366_177_98,
],
);
}
#[test]
fn test_single_gene_matches_edger() {
let out = compute_adjust_vec(
&counts()[6..12],
1,
6,
&fitted()[6..12],
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0],
None,
)
.unwrap();
assert_relative_eq!(
out.deviance[0],
0.016_535_319_784_658_36,
max_relative = TOL
);
assert_relative_eq!(out.df[0], 3.999_429_434_054_224, max_relative = TOL);
assert_relative_eq!(out.s2[0], 0.004_134_419_685_934_175, max_relative = TOL);
}
#[test]
fn test_structural_zeros_collapse_the_degrees_of_freedom() {
let out = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0],
None,
)
.unwrap();
assert_eq!(out.deviance[4], 0.0);
assert_eq!(out.df[4], 0.0);
assert_eq!(out.s2[4], 0.0);
assert!(out.df[3] > 1.9 && out.df[3] < 2.1);
assert!(out.df[0] > 3.9);
}
#[test]
fn test_recycled_dispersion_forms_agree() {
let run = |dispersion: Recycled<f64>| {
compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&dispersion,
&[1.0],
None,
)
.unwrap()
};
let scalar = run(Recycled::scalar(0.125));
let per_gene = run(Recycled::by_gene(vec![0.125; 6]));
let by_sample = run(Recycled::by_sample(vec![0.125; 6]));
let full = run(Recycled::full(vec![0.125; 36], 6, 6).unwrap());
assert_eq!(scalar.deviance, per_gene.deviance);
assert_eq!(scalar.deviance, by_sample.deviance);
assert_eq!(scalar.deviance, full.deviance);
assert_eq!(scalar.df, per_gene.df);
assert_eq!(scalar.df, full.df);
}
#[test]
fn test_per_gene_prior_matches_scalar_prior() {
let run = |prior: &[f64]| {
compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
prior,
None,
)
.unwrap()
};
let scalar = run(&[2.5]);
let vector = run(&[2.5; 6]);
assert_eq!(scalar.deviance, vector.deviance);
assert_eq!(scalar.df, vector.df);
}
#[test]
fn test_f32_counts_agree_with_f64() {
let wide = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0],
None,
)
.unwrap();
let narrow: Vec<f32> = counts().iter().map(|&v| v as f32).collect();
let got = compute_adjust_vec(
&narrow,
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0],
None,
)
.unwrap();
assert_eq!(wide.deviance, got.deviance);
assert_eq!(wide.df, got.df);
}
const PRIOR_TOL: f64 = 1e-12;
fn prior_fixture() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let ave: Vec<f64> = (0..25).map(|i| i as f64 / 4.0 - 2.0).collect();
let s2: Vec<f64> = (0..25)
.map(|i| ((i * 7 % 13) + 1) as f64 / 8.0 + i as f64 / 16.0)
.collect();
(ave, s2, vec![3.0; 25])
}
#[test]
fn test_compute_prior_matches_edger() {
let (ave, s2, df) = prior_fixture();
let got = compute_prior(&ave, &s2, &df).unwrap();
assert_eq!(got.len(), 1);
assert_relative_eq!(got[0], 2.423_348_828_030_909, max_relative = PRIOR_TOL);
}
#[test]
fn test_compute_prior_drops_low_df_genes() {
let (ave, s2, mut df) = prior_fixture();
df[0] = 0.0;
df[24] = MIN_DF;
let filtered = compute_prior(&ave, &s2, &df).unwrap();
let direct = compute_prior(&ave[1..24], &s2[1..24], &[3.0; 23]).unwrap();
assert_eq!(filtered, direct);
assert_relative_eq!(
filtered[0],
2.238_549_259_016_040_5,
max_relative = PRIOR_TOL
);
}
#[test]
fn test_compute_prior_degenerate_gene_counts() {
let none = compute_prior(&[1.0, 2.0], &[4.0, 9.0], &[0.0, 0.0]).unwrap();
assert_eq!(none, vec![1.0]);
let one = compute_prior(&[1.0, 2.0], &[16.0, 9.0], &[3.0, 0.0]).unwrap();
assert_relative_eq!(one[0], 16.0, max_relative = 1e-12);
}
#[test]
fn test_compute_prior_floors_at_one() {
let ave: Vec<f64> = (0..10).map(|i| i as f64).collect();
let got = compute_prior(&ave, &[0.0625; 10], &[2.0; 10]).unwrap();
assert_eq!(got, vec![1.0]);
}
#[allow(clippy::type_complexity)]
fn prior_counts() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let counts = vec![
0.0, 11.0, 1.0, 2.0, 2.0, 2.0, 1.0, 5.0, 5.0, 8.0, 3.0, 3.0, 1.0, 3.0, 5.0, 7.0, 3.0, 3.0, 7.0, 6.0, 0.0, 6.0, 3.0, 6.0, 3.0, 9.0, 1.0, 4.0, 8.0, 0.0, 18.0, 11.0, 9.0, 36.0, 11.0, 5.0, 19.0, 9.0, 7.0, 1.0, 34.0, 17.0, 20.0, 3.0, 7.0, 22.0, 3.0, 4.0, 19.0, 14.0, 40.0, 64.0, 37.0, 26.0, 12.0, 20.0, 27.0, 9.0, 28.0, 54.0, 41.0, 8.0, 2.0, 4.0, 22.0, 68.0, 32.0, 38.0, 43.0, 22.0, 13.0, 46.0, 46.0, 92.0, 8.0, 53.0, 49.0, 66.0, 13.0, 7.0, 88.0, 28.0, 156.0, 51.0, 294.0, 31.0, 19.0, 17.0, 158.0, 20.0, 22.0, 73.0, 14.0, 74.0, 160.0, 42.0, 76.0, 154.0, 67.0, 123.0, 28.0, 265.0, 196.0, 111.0, 29.0, 150.0, 18.0, 96.0, 7.0, 133.0, 102.0, 174.0, 310.0, 73.0, 133.0, 492.0, 362.0, 14.0, 71.0, 58.0, 250.0, 551.0, 40.0, 46.0, 54.0, 56.0, 45.0, 288.0, 209.0, 376.0, 182.0, 152.0, 458.0, 157.0, 192.0, 524.0, 480.0, 574.0, 172.0, 271.0, 340.0, 874.0, 456.0, 210.0,
];
let group_means: [(f64, f64); 24] = [
(4.0, 2.0),
(3.75, 4.75),
(3.0, 4.25),
(4.25, 5.0),
(4.25, 4.0),
(12.75, 17.25),
(11.75, 17.25),
(10.0, 9.75),
(24.25, 42.25),
(19.75, 30.25),
(17.0, 31.25),
(37.75, 27.0),
(48.75, 56.0),
(36.0, 78.25),
(114.75, 65.0),
(36.25, 92.0),
(99.0, 138.75),
(112.0, 88.0),
(80.75, 185.75),
(329.0, 47.75),
(280.25, 52.0),
(180.75, 236.75),
(269.0, 526.0),
(261.0, 513.25),
];
let mut fitted = Vec::with_capacity(144);
for (first, second) in group_means {
fitted.extend_from_slice(&[first, first, first, second, second, second]);
}
let ave_log_cpm = vec![
11.171875, 11.484375, 11.375, 11.59375, 11.46875, 12.921875, 12.9375, 12.4375,
14.015625, 13.6875, 13.65625, 14.046875, 14.59375, 14.828125, 15.453125, 14.84375,
15.8125, 15.546875, 15.890625, 16.515625, 16.203125, 16.546875, 17.53125, 17.421875,
];
(counts, fitted, ave_log_cpm)
}
#[test]
fn test_prior_after_one_round_matches_edger() {
let (counts, fitted, ave) = prior_counts();
let adjusted = compute_adjust_vec(
&counts,
24,
6,
&fitted,
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0],
None,
)
.unwrap();
let prior = compute_prior(&ave, &adjusted.s2, &adjusted.df).unwrap();
assert_relative_eq!(prior[0], 5.250_556_991_969_081, max_relative = PRIOR_TOL);
}
#[test]
fn test_update_prior_matches_edger() {
let (counts, fitted, ave) = prior_counts();
let prior = update_prior(
&counts,
24,
6,
&fitted,
&two_group(),
2,
&Recycled::scalar(0.125),
None,
&ave,
)
.unwrap();
assert_eq!(prior.len(), 1);
assert_relative_eq!(prior[0], 18.590_485_410_598_912, max_relative = PRIOR_TOL);
}
#[test]
fn test_update_prior_is_exactly_two_rounds() {
let (counts, fitted, ave) = prior_counts();
let run = |prior: &[f64]| {
let adjusted = compute_adjust_vec(
&counts,
24,
6,
&fitted,
&two_group(),
2,
&Recycled::scalar(0.125),
prior,
None,
)
.unwrap();
compute_prior(&ave, &adjusted.s2, &adjusted.df).unwrap()
};
let first = run(&[1.0]);
let second = run(&first);
let got = update_prior(
&counts,
24,
6,
&fitted,
&two_group(),
2,
&Recycled::scalar(0.125),
None,
&ave,
)
.unwrap();
assert_eq!(got, second);
assert!(first[0] != second[0]);
}
#[test]
fn test_near_poisson_dispersion_matches_edger() {
check(
&two_group(),
Recycled::scalar(2.0_f64.powi(-20)),
&[1.0],
None,
&[
0.360_893_151_979_798_77,
0.122_110_248_576_896_45,
12.329_306_966_107_474,
0.396_987_455_932_764_3,
0.0,
11.686_733_868_487_11,
],
&[
3.991_092_303_143_825_7,
3.999_478_990_707_440_4,
4.976_310_706_206_089,
1.997_955_076_334_263_3,
0.0,
4.000_035_625_925_281,
],
&[
0.090_424_656_852_841_87,
0.030_531_538_948_101_14,
2.477_599_911_663_728_6,
0.198_696_887_950_621_38,
0.0,
2.921_657_445_434_316,
],
);
}
#[test]
fn test_rejects_bad_shapes() {
let run = |n_genes: usize,
n_samples: usize,
counts: &[f64],
fitted: &[f64],
design: &[f64],
n_coef: usize| {
compute_adjust_vec(
counts,
n_genes,
n_samples,
fitted,
design,
n_coef,
&Recycled::scalar(0.125),
&[1.0],
None,
)
.unwrap_err()
};
assert!(matches!(
run(0, 6, &[], &[], &two_group(), 2),
EdgeErrors::EmptyCounts { .. }
));
assert!(matches!(
run(6, 0, &[], &[], &[], 2),
EdgeErrors::EmptyCounts { .. }
));
assert!(matches!(
run(6, 6, &counts(), &fitted(), &[], 0),
EdgeErrors::MustBePositive(_)
));
assert!(matches!(
run(5, 6, &counts(), &fitted(), &two_group(), 2),
EdgeErrors::LengthMismatch { name: "counts", .. }
));
assert!(matches!(
run(6, 6, &counts(), &fitted()[..30], &two_group(), 2),
EdgeErrors::LengthMismatch { name: "fitted", .. }
));
assert!(matches!(
run(6, 6, &counts(), &fitted(), &two_group(), 3),
EdgeErrors::LengthMismatch { name: "design", .. }
));
assert!(matches!(
run(
1,
2,
&[1.0, 2.0],
&[1.0, 2.0],
&[1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
3
),
EdgeErrors::DesignNotFullRank { .. }
));
}
#[test]
fn test_rejects_mismatched_dispersion() {
let err = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::by_gene(vec![0.125; 3]),
&[1.0],
None,
)
.unwrap_err();
assert!(matches!(
err,
EdgeErrors::LengthMismatch {
name: "by_gene",
..
}
));
}
#[test]
fn test_rejects_mismatched_weights() {
let weights = Recycled::by_sample(vec![1.0; 3]);
let err = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0],
Some(&weights),
)
.unwrap_err();
assert!(matches!(
err,
EdgeErrors::LengthMismatch {
name: "by_sample",
..
}
));
}
#[test]
fn test_rejects_wrong_prior_length() {
let err = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
&[1.0, 2.0],
None,
)
.unwrap_err();
assert!(matches!(
err,
EdgeErrors::LengthMismatch { name: "prior", .. }
));
}
#[test]
fn test_rejects_non_positive_prior() {
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let err = compute_adjust_vec(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
&[bad],
None,
)
.unwrap_err();
assert!(matches!(err, EdgeErrors::InvalidArgument(_)));
}
}
#[test]
fn test_compute_prior_rejects_mismatched_lengths() {
let err = compute_prior(&[1.0, 2.0], &[1.0], &[1.0, 1.0]).unwrap_err();
assert!(matches!(err, EdgeErrors::LengthMismatch { name: "s2", .. }));
let err = compute_prior(&[1.0, 2.0], &[1.0, 1.0], &[1.0]).unwrap_err();
assert!(matches!(err, EdgeErrors::LengthMismatch { name: "df", .. }));
}
#[test]
fn test_update_prior_rejects_mismatched_ave_log_cpm() {
let err = update_prior(
&counts(),
6,
6,
&fitted(),
&two_group(),
2,
&Recycled::scalar(0.125),
None,
&[0.0, 1.0],
)
.unwrap_err();
assert!(matches!(
err,
EdgeErrors::LengthMismatch {
name: "ave_log_cpm",
..
}
));
}
#[test]
fn test_zero_dispersion_matches_edger() {
let counts: Vec<f64> = vec![
8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 4.0, 4.0, 8.0, 8.0, 16.0, 16.0, 64.0, 64.0, 64.0, 128.0, 128.0, 128.0, ];
let fitted: Vec<f64> = vec![
18.666_666_666_666_668,
18.666_666_666_666_668,
18.666_666_666_666_668,
149.333_333_333_333_4,
149.333_333_333_333_4,
149.333_333_333_333_4,
5.333_333_333_333_334,
5.333_333_333_333_334,
5.333_333_333_333_334,
13.333_333_333_333_334,
13.333_333_333_333_334,
13.333_333_333_333_334,
64.000_000_000_000_03,
64.000_000_000_000_03,
64.000_000_000_000_03,
128.000_000_000_000_09,
128.000_000_000_000_09,
128.000_000_000_000_09,
];
let design = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
let out = compute_adjust_vec(
&counts,
3,
6,
&fitted,
&design,
2,
&Recycled::scalar(0.0),
&[15.741_184_694_250_476],
None,
)
.unwrap();
let expected_deviance = [147.927_266_159_262_62, 11.904_649_073_836_47, 0.0];
let expected_df = [
5.343_595_873_000_883,
9.158_203_896_871_512,
3.905_730_864_074_366_4,
];
for (got, want) in out.deviance.iter().zip(expected_deviance.iter()) {
assert_relative_eq!(got, want, epsilon = 1e-12, max_relative = 1e-12);
}
for (got, want) in out.df.iter().zip(expected_df.iter()) {
assert_relative_eq!(got, want, max_relative = 1e-12);
}
}
}