use faer::MatRef;
use faer::linalg::solvers::SolveLstsq;
use rayon::prelude::*;
use crate::errors::EdgeErrors;
use crate::limma::lowess::lowess;
use crate::limma::lowess::{LowessParams, weighted_lowess};
use crate::numeric::dist::{beta_cdf, beta_ppf, beta_sf, chisq_sf, f_ppf, f_sf, gamma_ppf};
use crate::numeric::gamma::{ln_gamma, logmdigamma, trigamma, trigamma_inverse};
use crate::numeric::interpolate::{interp_linear_extrap, natural_spline_basis};
use crate::numeric::optimise::{OPTIMIZE_TOL, brent_fmin, brentq};
use crate::numeric::stats::{median, p_adjust_bh, quantile_type7, rank_average, trimmed_mean};
use crate::utils::design::{LIMMA_LOWESS_DEFAULTS, choose_lowess_span};
const MIN_GENES: usize = 3;
const DF1_TOL: f64 = 1e-15;
const VAR_TOL: f64 = -1e-15;
const ZERO_VAR_OFFSET: f64 = 1e-5;
const ROBUST_DF1_TOL: f64 = 1e-6;
const ROBUST_VAR_FLOOR: f64 = 1e-12;
const DF1_TIE_TOL: f64 = 1e-14;
const QUAD_NODES: usize = 128;
const QUAD_NEWTON_MAX_ITER: usize = 100;
const ROBUST_LOWESS_SPAN: f64 = 0.4;
const ROBUST_LOWESS_STEPS: usize = 3;
const ROOT_XTOL: f64 = 1e-15;
const ROOT_RTOL: f64 = 8.0 * f64::EPSILON;
const ROOT_MAX_ITER: usize = 200;
const RANK_TOL: f64 = 1e-7;
const PARALLEL_THRESHOLD: usize = 4096;
const LN_HALF: f64 = -std::f64::consts::LN_2;
const UNEQUAL_DF1_TOL: f64 = 0.01;
const UNEQUAL_ZERO_VAR_OFFSET: f64 = 1e-12;
const UNEQUAL_LOWESS_SMALL_N: usize = 500;
const LOESS_MIN_WEIGHT: f64 = 1e-8;
const LOESS_MAX_WEIGHT: f64 = 100.0;
const EQUAL_WEIGHT_TOL: f64 = 1e-15;
const UNEQUAL_PAR_LOWER: f64 = 0.5;
const UNEQUAL_PAR_UPPER: f64 = 0.9998;
const ROBUST_FDR_CUTOFF: f64 = 0.3;
const LEFT_TAIL_SWITCH: f64 = 0.001;
const WLS_RANK_TOL: f64 = 1e-14;
#[derive(Clone, Copy, Debug)]
pub struct SqueezeVarParams {
pub robust: bool,
pub winsor_tail_p: (f64, f64),
pub span: Option<f64>,
pub legacy: Option<bool>,
}
impl Default for SqueezeVarParams {
fn default() -> Self {
Self {
robust: false,
winsor_tail_p: (0.05, 0.1),
span: None,
legacy: None,
}
}
}
#[derive(Clone, Debug)]
pub struct SqueezeVarResult {
pub var_post: Vec<f64>,
pub var_prior: Vec<f64>,
pub df_prior: Vec<f64>,
}
pub fn squeeze_var(
var: &[f64],
df: &[f64],
covariate: Option<&[f64]>,
params: Option<SqueezeVarParams>,
) -> Result<SqueezeVarResult, EdgeErrors> {
let params = params.unwrap_or_default();
let n = var.len();
if n == 0 {
return Err(EdgeErrors::InvalidArgument(
"squeeze_var was given an empty variance vector.".to_string(),
));
}
if df.len() != 1 && df.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "df",
expected: n,
got: df.len(),
});
}
if let Some(cov) = covariate
&& cov.len() != n
{
return Err(EdgeErrors::LengthMismatch {
name: "covariate",
expected: n,
got: cov.len(),
});
}
check_winsor_tail_p(params.winsor_tail_p)?;
if let Some(s) = params.span
&& !(s > 0.0 && s <= 1.0)
{
return Err(EdgeErrors::InvalidArgument(format!(
"squeeze_var span must lie in (0, 1]; got {s}."
)));
}
for (i, &d) in df.iter().enumerate() {
if !d.is_finite() || d < 0.0 {
return Err(EdgeErrors::InvalidArgument(format!(
"squeeze_var needs finite non-negative degrees of freedom; df[{i}] is {d}."
)));
}
}
if let Some(cov) = covariate
&& let Some(i) = cov.iter().position(|v| v.is_nan())
{
return Err(EdgeErrors::InvalidArgument(format!(
"squeeze_var covariate must not contain NaN; covariate[{i}] is NaN."
)));
}
let mut var = var.to_vec();
if df.len() > 1 {
for (v, &d) in var.iter_mut().zip(df) {
if d == 0.0 {
*v = 0.0;
}
}
}
for (i, &v) in var.iter().enumerate() {
if !v.is_finite() || v < 0.0 {
return Err(EdgeErrors::InvalidArgument(format!(
"squeeze_var needs finite non-negative variances; var[{i}] is {v}."
)));
}
}
if n < MIN_GENES {
return Ok(SqueezeVarResult {
var_post: var.clone(),
var_prior: var,
df_prior: vec![0.0],
});
}
let legacy = if params.span.is_some() {
false
} else {
params.legacy.unwrap_or_else(|| all_positive_df_equal(df))
};
let (var_prior, df_prior) = if !legacy {
let fit = fit_f_dist_unequal_df1(&var, df, covariate, params.span, params.robust, None)?;
let df_prior = fit.df2_shrunk.unwrap_or_else(|| vec![fit.df2]);
if df_prior.iter().any(|v| v.is_nan()) {
return Err(EdgeErrors::InvalidArgument(
"squeeze_var could not estimate the prior degrees of freedom: fewer than two \
genes carry a positive variance and a positive weight."
.to_string(),
));
}
(fit.scale, df_prior)
} else if params.robust {
let (scale, df2_shrunk) =
robust_fit(&var, df, covariate, params.winsor_tail_p, params.span)?;
(recycle(&scale, n), df2_shrunk)
} else if let Some(cov) = covariate {
let (scale, df2) = fit_f_dist_trend(&var, df, cov, params.span)?;
(recycle(&scale, n), vec![df2])
} else {
let (scale, df2) = fit_f_dist(&var, df)?;
(vec![scale], vec![df2])
};
let var_post = posterior_var(&var, df, &var_prior, &df_prior);
Ok(SqueezeVarResult {
var_post,
var_prior,
df_prior,
})
}
pub fn fit_f_dist(x: &[f64], df1: &[f64]) -> Result<(f64, f64), EdgeErrors> {
let n = check_fit_lengths("fit_f_dist", x, df1)?;
if n == 1 {
return Ok((x[0], 0.0));
}
let ok = informative_mask(x, df1, DF1_TOL);
let nok = ok.iter().filter(|&&k| k).count();
if nok == 1 {
let i = ok.iter().position(|&k| k).unwrap_or(0);
return Ok((x[i], 0.0));
}
if nok == 0 {
return Err(EdgeErrors::InvalidArgument(
"fit_f_dist found no gene with a finite variance and positive degrees of freedom."
.to_string(),
));
}
let x_ok: Vec<f64> = subset(x, &ok);
let df_ok: Vec<f64> = subset_recycled(df1, &ok);
let x_ok = offset_from_zero(&x_ok);
let e: Vec<f64> = x_ok
.iter()
.zip(&df_ok)
.map(|(&v, &d)| v.ln() + logmdigamma(0.5 * d))
.collect();
let emean = mean(&e);
let evar = e.iter().map(|&v| (v - emean) * (v - emean)).sum::<f64>() / (nok - 1) as f64
- mean_trigamma(&df_ok);
if evar > 0.0 {
let df2 = 2.0 * trigamma_inverse(evar);
Ok(((emean - logmdigamma(0.5 * df2)).exp(), df2))
} else {
Ok((mean(&x_ok), f64::INFINITY))
}
}
pub fn fit_f_dist_trend(
var: &[f64],
df1: &[f64],
covariate: &[f64],
span: Option<f64>,
) -> Result<(Vec<f64>, f64), EdgeErrors> {
let n = check_fit_lengths("fit_f_dist_trend", var, df1)?;
if covariate.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "covariate",
expected: n,
got: covariate.len(),
});
}
if let Some(s) = span
&& !(s > 0.0 && s <= 1.0)
{
return Err(EdgeErrors::InvalidArgument(format!(
"fit_f_dist_trend span must lie in (0, 1]; got {s}."
)));
}
if n == 1 {
return Ok((vec![var[0]], 0.0));
}
let covariate = finitise_covariate(covariate)?;
let ok = informative_mask(var, df1, DF1_TOL);
let nok = ok.iter().filter(|&&k| k).count();
if nok == 1 {
let i = ok.iter().position(|&k| k).unwrap_or(0);
return Ok((vec![var[i]], 0.0));
}
if nok == 0 {
return Err(EdgeErrors::InvalidArgument(
"fit_f_dist_trend found no gene with a finite variance and positive degrees of freedom."
.to_string(),
));
}
let cov_ok: Vec<f64> = subset(&covariate, &ok);
let spline_df = spline_columns(nok, &cov_ok);
if spline_df < MIN_SPLINE_DF {
let x_ok: Vec<f64> = subset(var, &ok);
let df_ok: Vec<f64> = subset_recycled(df1, &ok);
let (scale, df2) = fit_f_dist(&x_ok, &df_ok)?;
return Ok((vec![scale; n], df2));
}
let x_ok = offset_from_zero(&subset(var, &ok));
let df_ok: Vec<f64> = subset_recycled(df1, &ok);
let e: Vec<f64> = x_ok
.iter()
.zip(&df_ok)
.map(|(&v, &d)| v.ln() + logmdigamma(0.5 * d))
.collect();
let (basis, ncol) = natural_spline_basis(&cov_ok, spline_df)?;
let (fitted_ok, rss, rank) = spline_fit(&basis, ncol, &e)?;
let emean = extend_trend(&covariate, &ok, &cov_ok, &fitted_ok)?;
let evar = if nok > rank {
rss / (nok - rank) as f64
} else {
0.0
} - mean_trigamma(&df_ok);
if evar > 0.0 {
let df2 = 2.0 * trigamma_inverse(evar);
let shift = logmdigamma(0.5 * df2);
Ok((emean.iter().map(|&m| (m - shift).exp()).collect(), df2))
} else {
Ok((emean.iter().map(|&m| m.exp()).collect(), f64::INFINITY))
}
}
pub fn fit_f_dist_robustly(
x: &[f64],
df1: &[f64],
covariate: Option<&[f64]>,
winsor_tail_p: (f64, f64),
) -> Result<(Vec<f64>, Vec<f64>), EdgeErrors> {
robust_fit(x, df1, covariate, winsor_tail_p, None)
}
#[derive(Clone, Debug)]
pub struct UnequalDf1Fit {
pub scale: Vec<f64>,
pub df2: f64,
pub df2_outlier: Option<f64>,
pub df2_shrunk: Option<Vec<f64>>,
}
pub fn fit_f_dist_unequal_df1(
x: &[f64],
df1: &[f64],
covariate: Option<&[f64]>,
span: Option<f64>,
robust: bool,
prior_weights: Option<&[f64]>,
) -> Result<UnequalDf1Fit, EdgeErrors> {
let n = check_fit_lengths("fit_f_dist_unequal_df1", x, df1)?;
if let Some(cov) = covariate {
if cov.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "covariate",
expected: n,
got: cov.len(),
});
}
if let Some(i) = cov.iter().position(|v| v.is_nan()) {
return Err(EdgeErrors::InvalidArgument(format!(
"fit_f_dist_unequal_df1 covariate must not contain NaN; covariate[{i}] is NaN."
)));
}
}
if let Some(pw) = prior_weights {
if pw.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "prior_weights",
expected: n,
got: pw.len(),
});
}
if let Some(i) = pw.iter().position(|v| v.is_nan() || *v < 0.0) {
return Err(EdgeErrors::InvalidArgument(format!(
"fit_f_dist_unequal_df1 prior weights must be non-negative and not NaN; \
prior_weights[{i}] is {}.",
pw[i]
)));
}
}
if let Some(d) = df1.iter().find(|v| v.is_nan()) {
return Err(EdgeErrors::InvalidArgument(format!(
"fit_f_dist_unequal_df1 degrees of freedom must not be NaN; found {d}."
)));
}
unequal_df1_inner(
x,
&recycle(df1, n),
covariate,
span,
robust,
prior_weights.map(|p| p.to_vec()),
)
}
fn posterior_var(var: &[f64], df: &[f64], var_prior: &[f64], df_prior: &[f64]) -> Vec<f64> {
let n = var.len();
let largest = df_prior.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
if largest.is_finite() {
return (0..n)
.map(|i| {
let d = at(df, i);
let dp = at(df_prior, i);
(d * var[i] + dp * at(var_prior, i)) / (d + dp)
})
.collect();
}
let mut out: Vec<f64> = (0..n).map(|i| at(var_prior, i)).collect();
let smallest = df_prior.iter().fold(f64::INFINITY, |a, &b| a.min(b));
if smallest > 1e100 {
return out;
}
for i in 0..n {
let dp = at(df_prior, i);
if dp.is_finite() {
let d = at(df, i);
out[i] = (d * var[i] + dp * out[i]) / (d + dp);
}
}
out
}
#[derive(Clone, Debug)]
struct RobustFit {
scale: Vec<f64>,
df2: f64,
df2_shrunk: Vec<f64>,
}
fn robust_fit(
x: &[f64],
df1: &[f64],
covariate: Option<&[f64]>,
winsor_tail_p: (f64, f64),
span: Option<f64>,
) -> Result<(Vec<f64>, Vec<f64>), EdgeErrors> {
check_winsor_tail_p(winsor_tail_p)?;
let fit = robust_fit_inner(x, df1, covariate, winsor_tail_p, span)?;
Ok((fit.scale, fit.df2_shrunk))
}
fn robust_fit_inner(
x: &[f64],
df1: &[f64],
covariate: Option<&[f64]>,
winsor_tail_p: (f64, f64),
span: Option<f64>,
) -> Result<RobustFit, EdgeErrors> {
let n = check_fit_lengths("fit_f_dist_robustly", x, df1)?;
if n < 2 {
return Err(EdgeErrors::InvalidArgument(
"fit_f_dist_robustly needs at least two genes.".to_string(),
));
}
if let Some(cov) = covariate {
if cov.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "covariate",
expected: n,
got: cov.len(),
});
}
if let Some(i) = cov.iter().position(|v| !v.is_finite()) {
return Err(EdgeErrors::InvalidArgument(format!(
"fit_f_dist_robustly needs a finite covariate; covariate[{i}] is {}.",
cov[i]
)));
}
}
if n == 2 {
return Ok(match covariate {
Some(cov) => {
let (scale, df2) = fit_f_dist_trend(x, df1, cov, span)?;
RobustFit {
scale,
df2,
df2_shrunk: vec![df2; n],
}
}
None => {
let (scale, df2) = fit_f_dist(x, df1)?;
RobustFit {
scale: vec![scale],
df2,
df2_shrunk: vec![df2; n],
}
}
});
}
let ok: Vec<bool> = (0..n)
.map(|i| {
let d = at(df1, i);
!x[i].is_nan() && d.is_finite() && d > ROBUST_DF1_TOL
})
.collect();
if ok.iter().any(|&k| !k) {
return robust_fit_dropping(x, df1, covariate, winsor_tail_p, span, &ok);
}
let mut x = x.to_vec();
let m = median(&x);
if m <= 0.0 {
return Err(EdgeErrors::InvalidArgument(
"fit_f_dist_robustly needs a positive median variance; more than half are <= 0."
.to_string(),
));
}
let floor = m * ROBUST_VAR_FLOOR;
for v in &mut x {
if *v < floor {
*v = floor;
}
}
let non_robust = match covariate {
Some(cov) => {
let (scale, df2) = fit_f_dist_trend(&x, df1, cov, span)?;
(recycle(&scale, n), df2)
}
None => {
let (scale, df2) = fit_f_dist(&x, df1)?;
(vec![scale], df2)
}
};
let (nr_scale, nr_df2) = non_robust;
if winsor_tail_p.0 < 1.0 / n as f64 && winsor_tail_p.1 < 1.0 / n as f64 {
return Ok(RobustFit {
scale: nr_scale,
df2: nr_df2,
df2_shrunk: vec![nr_df2; n],
});
}
let df1_val = unify_df1(&mut x, df1, &nr_scale, nr_df2)?;
let z: Vec<f64> = x.iter().map(|v| v.ln()).collect();
let ztrend: Vec<f64> = match covariate {
Some(cov) => lowess(
cov,
&z,
span.unwrap_or(ROBUST_LOWESS_SPAN),
ROBUST_LOWESS_STEPS,
)?,
None => vec![trimmed_mean(&z, winsor_tail_p.1)?],
};
let zresid: Vec<f64> = z
.iter()
.enumerate()
.map(|(i, &v)| v - at(&ztrend, i))
.collect();
let lo = quantile_type7(&zresid, winsor_tail_p.0)?;
let hi = quantile_type7(&zresid, 1.0 - winsor_tail_p.1)?;
let zwins: Vec<f64> = zresid.iter().map(|&v| v.clamp(lo, hi)).collect();
let zwmean = mean(&zwins);
let zwvar = zwins
.iter()
.map(|&v| (v - zwmean) * (v - zwmean))
.sum::<f64>()
/ (n - 1) as f64;
let quad = gauss_legendre_unit(QUAD_NODES);
let mom_inf = winsorized_moments(df1_val, f64::INFINITY, winsor_tail_p, &quad)?;
let funval_inf = (zwvar / mom_inf.1).ln();
if funval_inf <= 0.0 {
return robust_infinite_df2(&z, &ztrend, zwmean, mom_inf.0, df1_val);
}
if nr_df2.is_infinite() {
return Ok(RobustFit {
scale: nr_scale,
df2: nr_df2,
df2_shrunk: vec![nr_df2; n],
});
}
let mut objective = |par: f64| -> f64 {
let d2 = link_inv(par);
match winsorized_moments(df1_val, d2, winsor_tail_p, &quad) {
Ok((_, v)) => (zwvar / v).ln(),
Err(_) => funval_inf,
}
};
let rbx = link_fun(nr_df2);
let funval_low = objective(rbx);
let df2 = if funval_low >= 0.0 {
nr_df2
} else {
link_inv(brentq(
&mut objective,
rbx,
1.0,
ROOT_XTOL,
ROOT_RTOL,
ROOT_MAX_ITER,
)?)
};
let (mom_mean, _) = winsorized_moments(df1_val, df2, winsor_tail_p, &quad)?;
let corrected: Vec<f64> = (0..n).map(|i| at(&ztrend, i) + zwmean - mom_mean).collect();
let scale: Vec<f64> = if covariate.is_some() {
corrected.iter().map(|&v| v.exp()).collect()
} else {
vec![corrected[0].exp()]
};
let fstat: Vec<f64> = (0..n).map(|i| (z[i] - corrected[i]).exp()).collect();
let log_tail_p = map_maybe_par(&fstat, |f| Ok(f_sf(f, df1_val, df2)?.ln()))?;
let df2_shrunk = shrink_df2(&log_tail_p, &fstat, df1_val, df2)?;
Ok(RobustFit {
scale,
df2,
df2_shrunk,
})
}
fn robust_fit_dropping(
x: &[f64],
df1: &[f64],
covariate: Option<&[f64]>,
winsor_tail_p: (f64, f64),
span: Option<f64>,
ok: &[bool],
) -> Result<RobustFit, EdgeErrors> {
let n = x.len();
let x_ok = subset(x, ok);
let df_ok = subset_recycled(df1, ok);
let cov_ok = covariate.map(|c| subset(c, ok));
let fit = robust_fit_inner(&x_ok, &df_ok, cov_ok.as_deref(), winsor_tail_p, span)?;
let mut df2_shrunk = vec![fit.df2; n];
let mut k = 0;
for i in 0..n {
if ok[i] {
df2_shrunk[i] = fit.df2_shrunk[k];
k += 1;
}
}
let scale = match (covariate, cov_ok) {
(Some(cov), Some(cov_ok)) => {
let log_scale: Vec<f64> = fit.scale.iter().map(|v| v.ln()).collect();
let (knots, values) = collapse_ties(&cov_ok, &recycle(&log_scale, cov_ok.len()));
let missing: Vec<f64> = (0..n).filter(|&i| !ok[i]).map(|i| cov[i]).collect();
let filled = interp_linear_extrap(&missing, &knots, &values)?;
let mut scale = vec![0.0; n];
let (mut k, mut j) = (0, 0);
for i in 0..n {
if ok[i] {
scale[i] = at(&fit.scale, k);
k += 1;
} else {
scale[i] = filled[j].exp();
j += 1;
}
}
scale
}
_ => fit.scale,
};
Ok(RobustFit {
scale,
df2: fit.df2,
df2_shrunk,
})
}
fn robust_infinite_df2(
z: &[f64],
ztrend: &[f64],
zwmean: f64,
mom_mean: f64,
df1_val: f64,
) -> Result<RobustFit, EdgeErrors> {
let n = z.len();
let corrected: Vec<f64> = (0..n).map(|i| at(ztrend, i) + zwmean - mom_mean).collect();
let scale: Vec<f64> = if ztrend.len() > 1 {
corrected.iter().map(|&v| v.exp()).collect()
} else {
vec![corrected[0].exp()]
};
let fstat: Vec<f64> = (0..n).map(|i| (z[i] - corrected[i]).exp()).collect();
let tail_p = map_maybe_par(&fstat, |f| chisq_sf(f * df1_val, df1_val))?;
let ranks = rank_average(&fstat);
let pooled = n as f64 * df1_val;
let mut df2_shrunk = vec![f64::INFINITY; n];
let mut any = false;
for i in 0..n {
let empirical = (n as f64 - ranks[i] + 0.5) / n as f64;
let prob = (tail_p[i] / empirical).min(1.0);
if prob < 1.0 {
df2_shrunk[i] = prob * pooled;
any = true;
}
}
if any {
let order = stable_order(&tail_p);
cummax_in_order(&mut df2_shrunk, &order);
}
Ok(RobustFit {
scale,
df2: f64::INFINITY,
df2_shrunk,
})
}
fn shrink_df2(
log_tail_p: &[f64],
fstat: &[f64],
df1_val: f64,
df2: f64,
) -> Result<Vec<f64>, EdgeErrors> {
let n = fstat.len();
let ranks = rank_average(fstat);
let log_prob_not: Vec<f64> = (0..n)
.map(|i| (log_tail_p[i] - ((n as f64 - ranks[i] + 0.5).ln() - (n as f64).ln())).min(0.0))
.collect();
if !log_prob_not.iter().any(|&v| v < 0.0) {
return Ok(vec![df2; n]);
}
let min_log_tail = log_tail_p.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let mut df2_shrunk: Vec<f64> = if min_log_tail == f64::NEG_INFINITY {
log_prob_not.iter().map(|&v| v.exp() * df2).collect()
} else {
let mut df2_outlier = LN_HALF / min_log_tail * df2;
let fmax = fstat.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let new_log_tail = f_sf(fmax, df1_val, df2_outlier)?.ln();
df2_outlier *= LN_HALF / new_log_tail;
log_prob_not
.iter()
.map(|&v| v.exp() * df2 - v.exp_m1() * df2_outlier)
.collect()
};
let order = stable_order(log_tail_p);
flatten_then_cummax(&mut df2_shrunk, &order);
Ok(df2_shrunk)
}
fn unify_df1(x: &mut [f64], df1: &[f64], scale: &[f64], df2: f64) -> Result<f64, EdgeErrors> {
if df1.len() == 1 {
return Ok(df1[0]);
}
let df1max = df1.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let low: Vec<usize> = (0..x.len())
.filter(|&i| df1[i] < df1max - DF1_TIE_TOL)
.collect();
if low.is_empty() {
return Ok(df1[0]);
}
if !df2.is_finite() {
return Ok(df1max);
}
for &i in &low {
let s = at(scale, i);
let f = x[i] / s;
let upper = f_sf(f, df1[i], df2)?;
let lower = beta_cdf(df1[i] * f / (df1[i] * f + df2), 0.5 * df1[i], 0.5 * df2)?;
let mapped = if upper < lower {
f_isf(upper, df1max, df2)?
} else {
f_ppf(lower, df1max, df2)?
};
x[i] = mapped * s;
}
Ok(df1max)
}
fn unequal_df1_inner(
x: &[f64],
df1: &[f64],
covariate: Option<&[f64]>,
span: Option<f64>,
robust: bool,
prior_weights: Option<Vec<f64>>,
) -> Result<UnequalDf1Fit, EdgeErrors> {
let n = x.len();
let mut x = x.to_vec();
let mut df1 = df1.to_vec();
let mut prior_weights = prior_weights;
let mut covariate = covariate;
let mut robust = robust;
if x.iter().any(|v| v.is_nan()) {
let mask: Vec<bool> = x.iter().map(|v| v.is_nan()).collect();
zero_out_weights(&mut prior_weights, &mask);
for (v, &m) in x.iter_mut().zip(&mask) {
if m {
*v = 0.0;
}
}
}
if df1.iter().any(|&d| d < UNEQUAL_DF1_TOL) {
let mask: Vec<bool> = df1.iter().map(|&d| d < UNEQUAL_DF1_TOL).collect();
zero_out_weights(&mut prior_weights, &mask);
for (d, &m) in df1.iter_mut().zip(&mask) {
if m {
*d = 1.0;
}
}
}
let weighted = prior_weights.is_some();
let informative: Vec<bool> = (0..n)
.map(|i| x[i] > 0.0 && prior_weights.as_ref().is_none_or(|p| p[i] != 0.0))
.collect();
let n_informative = informative.iter().filter(|&&k| k).count();
if n_informative < 2 {
return Ok(UnequalDf1Fit {
scale: vec![f64::NAN],
df2: f64::NAN,
df2_outlier: None,
df2_shrunk: None,
});
}
if n_informative == 2 {
covariate = None;
robust = false;
prior_weights = None;
}
let floor = UNEQUAL_ZERO_VAR_OFFSET * median(&subset(&x, &informative));
let xpos: Vec<f64> = x.iter().map(|&v| v.max(floor)).collect();
let d1: Vec<f64> = df1.iter().map(|&d| 0.5 * d).collect();
let e: Vec<f64> = xpos
.iter()
.zip(&d1)
.map(|(&v, &h)| v.ln() + logmdigamma(h))
.collect();
let w: Vec<f64> = match (weighted, prior_weights.as_ref()) {
(true, None) => Vec::new(),
(true, Some(p)) => d1
.iter()
.zip(p)
.map(|(&h, &q)| 1.0 / trigamma(h) * q)
.collect(),
(false, _) => d1.iter().map(|&h| 1.0 / trigamma(h)).collect(),
};
let emean: Vec<f64> = match covariate {
None => {
let num: f64 = w.iter().zip(&e).map(|(&a, &b)| a * b).sum();
let den: f64 = w.iter().sum();
vec![num / den]
}
Some(cov) => {
let span = span.unwrap_or_else(|| {
let (_, min_span, power) = LIMMA_LOWESS_DEFAULTS;
choose_lowess_span(n, UNEQUAL_LOWESS_SMALL_N, min_span, power)
});
loess_fit_weighted(&e, cov, &w, span)?
}
};
let d1x: Vec<f64> = d1.iter().zip(&xpos).map(|(&h, &v)| h * v).collect();
let objective = |par: f64| -> f64 {
let d2 = par / (1.0 - par);
let lmd2 = logmdigamma(d2);
let lg_d2 = ln_gamma(d2);
let term = |g: usize| -> f64 {
let d2s20 = d2 * (at(&emean, g) - lmd2).exp();
-(d1[g] + d2) * (d1x[g] / d2s20).ln_1p() - d1[g] * d2s20.ln() + ln_gamma(d1[g] + d2)
- lg_d2
};
let total: f64 = match (weighted, prior_weights.as_ref()) {
(true, None) => 0.0,
(true, Some(p)) => (0..n).map(|g| p[g] * term(g)).sum(),
(false, _) => (0..n).map(term).sum(),
};
-2.0 * total
};
let par = brent_fmin(
UNEQUAL_PAR_LOWER,
UNEQUAL_PAR_UPPER,
objective,
OPTIMIZE_TOL,
);
let d2 = par / (1.0 - par);
let shift = logmdigamma(d2);
let s20: Vec<f64> = emean.iter().map(|&m| (m - shift).exp()).collect();
let mut df2 = 2.0 * d2;
if !robust {
return Ok(UnequalDf1Fit {
scale: s20,
df2,
df2_outlier: None,
df2_shrunk: None,
});
}
let f_stat: Vec<f64> = (0..n).map(|g| x[g] / at(&s20, g)).collect();
let right_p: Vec<f64> = if n >= PARALLEL_THRESHOLD {
(0..n)
.into_par_iter()
.map(|g| f_sf(f_stat[g], df1[g], df2))
.collect::<Result<_, _>>()?
} else {
(0..n)
.map(|g| f_sf(f_stat[g], df1[g], df2))
.collect::<Result<_, _>>()?
};
let mut left_p: Vec<f64> = right_p.iter().map(|&p| 1.0 - p).collect();
if left_p.iter().copied().fold(f64::INFINITY, f64::min) < LEFT_TAIL_SWITCH {
for g in 0..n {
if left_p[g] < LEFT_TAIL_SWITCH {
left_p[g] = f_cdf(f_stat[g], df1[g], df2)?;
}
}
}
let two_sided: Vec<f64> = (0..n).map(|g| 2.0 * left_p[g].min(right_p[g])).collect();
let mut fdr = p_adjust_bh(&two_sided);
for v in &mut fdr {
if *v > ROBUST_FDR_CUTOFF {
*v = 1.0;
}
}
if fdr.iter().copied().fold(f64::INFINITY, f64::min) == 1.0 {
return Ok(UnequalDf1Fit {
scale: s20,
df2,
df2_outlier: None,
df2_shrunk: None,
});
}
let outpw = unequal_df1_inner(&x, &df1, covariate, None, false, Some(fdr))?;
let scale = outpw.scale;
df2 = outpw.df2;
let ranks = rank_average(&f_stat);
let n_f = n as f64;
let prob_not_outlier: Vec<f64> = (0..n)
.map(|g| (right_p[g] / ((n_f - ranks[g] + 0.5) / n_f)).min(1.0))
.collect();
if prob_not_outlier
.iter()
.copied()
.fold(f64::INFINITY, f64::min)
== 1.0
{
return Ok(UnequalDf1Fit {
scale,
df2,
df2_outlier: None,
df2_shrunk: None,
});
}
let imin = which_min(&right_p);
let min_right_p = right_p[imin];
let (df2_outlier, mut df2_shrunk) = if min_right_p == 0.0 {
(
0.0,
prob_not_outlier
.iter()
.map(|&p| p * df2)
.collect::<Vec<_>>(),
)
} else {
let first = LN_HALF / min_right_p.ln() * df2;
let new_log_right_p = f_sf(f_stat[imin], df1[imin], first)?.ln();
let outlier = LN_HALF / new_log_right_p * first;
(
outlier,
prob_not_outlier
.iter()
.map(|&p| p * df2 + (1.0 - p) * outlier)
.collect(),
)
};
let order = stable_order(&right_p);
flatten_then_cummax(&mut df2_shrunk, &order);
Ok(UnequalDf1Fit {
scale,
df2,
df2_outlier: Some(df2_outlier),
df2_shrunk: Some(df2_shrunk),
})
}
fn zero_out_weights(prior_weights: &mut Option<Vec<f64>>, mask: &[bool]) {
match prior_weights {
Some(p) => {
for (v, &m) in p.iter_mut().zip(mask) {
if m {
*v = 0.0;
}
}
}
None => {
*prior_weights = Some(
mask.iter()
.map(|&m| if m { 0.0 } else { 1.0 })
.collect::<Vec<f64>>(),
);
}
}
}
fn which_min(v: &[f64]) -> usize {
let mut best = 0;
for i in 1..v.len() {
if v[i] < v[best] {
best = i;
}
}
best
}
fn all_positive_df_equal(df: &[f64]) -> bool {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &d in df.iter().filter(|&&d| d > 0.0) {
lo = lo.min(d);
hi = hi.max(d);
}
lo == hi
}
fn f_cdf(x: f64, df1: f64, df2: f64) -> Result<f64, EdgeErrors> {
if x <= 0.0 {
return Ok(0.0);
}
if x.is_infinite() {
return Ok(1.0);
}
if df1 * x > df2 {
beta_sf(df2 / (df2 + df1 * x), 0.5 * df2, 0.5 * df1)
} else {
beta_cdf(df1 * x / (df2 + df1 * x), 0.5 * df1, 0.5 * df2)
}
}
fn winsorized_moments(
df1: f64,
df2: f64,
winsor_tail_p: (f64, f64),
quad: &(Vec<f64>, Vec<f64>),
) -> Result<(f64, f64), EdgeErrors> {
let fq = (
f_quantile(winsor_tail_p.0, df1, df2)?,
f_quantile(1.0 - winsor_tail_p.1, df1, df2)?,
);
let zq = (fq.0.ln(), fq.1.ln());
let q = (link_fun(fq.0), link_fun(fq.1));
let width = q.1 - q.0;
let (nodes, weights) = quad;
let mut z_nodes = Vec::with_capacity(nodes.len());
let mut density = Vec::with_capacity(nodes.len());
for &node in nodes {
let u = q.0 + width * node;
let f = link_inv(u);
z_nodes.push(f.ln());
density.push(f_density(f, df1, df2) / ((1.0 - u) * (1.0 - u)));
}
let mut m = 0.0;
for i in 0..nodes.len() {
m += weights[i] * density[i] * z_nodes[i];
}
m = width * m + zq.0 * winsor_tail_p.0 + zq.1 * winsor_tail_p.1;
let mut v = 0.0;
for i in 0..nodes.len() {
let d = z_nodes[i] - m;
v += weights[i] * density[i] * d * d;
}
v = width * v
+ (zq.0 - m) * (zq.0 - m) * winsor_tail_p.0
+ (zq.1 - m) * (zq.1 - m) * winsor_tail_p.1;
Ok((m, v))
}
fn gauss_legendre_unit(n: usize) -> (Vec<f64>, Vec<f64>) {
let mut nodes = vec![0.0; n];
let mut weights = vec![0.0; n];
let half = n.div_ceil(2);
for i in 0..half {
let mut z = (std::f64::consts::PI * (i as f64 + 0.75) / (n as f64 + 0.5)).cos();
let mut pp = 0.0;
for _ in 0..QUAD_NEWTON_MAX_ITER {
let (mut p1, mut p2) = (1.0_f64, 0.0_f64);
for j in 0..n {
let p3 = p2;
p2 = p1;
p1 = ((2 * j + 1) as f64 * z * p2 - j as f64 * p3) / (j + 1) as f64;
}
pp = n as f64 * (z * p1 - p2) / (z * z - 1.0);
let z1 = z;
z = z1 - p1 / pp;
if (z - z1).abs() <= f64::EPSILON {
break;
}
}
nodes[i] = 0.5 * (1.0 - z);
nodes[n - 1 - i] = 0.5 * (1.0 + z);
weights[i] = 1.0 / ((1.0 - z * z) * pp * pp);
weights[n - 1 - i] = weights[i];
}
(nodes, weights)
}
#[inline]
fn link_fun(d: f64) -> f64 {
if d.is_infinite() { 1.0 } else { d / (1.0 + d) }
}
#[inline]
fn link_inv(p: f64) -> f64 {
p / (1.0 - p)
}
fn f_quantile(p: f64, df1: f64, df2: f64) -> Result<f64, EdgeErrors> {
if df2.is_infinite() {
Ok(gamma_ppf(p, 0.5 * df1, 2.0)? / df1)
} else {
f_ppf(p, df1, df2)
}
}
fn f_density(x: f64, df1: f64, df2: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if df2.is_infinite() {
let y = df1 * x;
let k = 0.5 * df1;
return df1
* ((k - 1.0) * y.ln() - 0.5 * y - ln_gamma(k) - k * std::f64::consts::LN_2).exp();
}
let (a, b) = (0.5 * df1, 0.5 * df2);
let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
(a * (df1 * x).ln() + b * df2.ln() - (a + b) * (df1 * x + df2).ln() - x.ln() - ln_beta).exp()
}
fn f_isf(p: f64, df1: f64, df2: f64) -> Result<f64, EdgeErrors> {
let w = beta_ppf(p, 0.5 * df2, 0.5 * df1)?;
if w <= 0.0 {
return Ok(f64::INFINITY);
}
Ok(df2 * (1.0 - w) / (df1 * w))
}
const MIN_SPLINE_DF: usize = 2;
fn spline_columns(nok: usize, covariate: &[f64]) -> usize {
let mut sorted = covariate.to_vec();
sorted.sort_by(f64::total_cmp);
sorted.dedup();
let requested = 1 + usize::from(nok >= 3) + usize::from(nok >= 6) + usize::from(nok >= 30);
requested.min(sorted.len())
}
fn spline_fit(basis: &[f64], ncol: usize, e: &[f64]) -> Result<(Vec<f64>, f64, usize), EdgeErrors> {
let nok = e.len();
let a = MatRef::from_row_major_slice(basis, nok, ncol);
let qr = a.qr();
let r = qr.thin_R();
let pivot = r[(0, 0)].abs();
let rank = (0..ncol.min(nok))
.filter(|&i| r[(i, i)].abs() > RANK_TOL * pivot)
.count();
let rhs = MatRef::from_column_major_slice(e, nok, 1);
let coef = qr.solve_lstsq(rhs);
if (0..ncol).any(|j| !coef[(j, 0)].is_finite()) {
return Err(EdgeErrors::SolveFailed(
"the natural spline basis for the variance trend is rank deficient.".to_string(),
));
}
let fitted: Vec<f64> = basis
.chunks_exact(ncol)
.map(|row| row.iter().enumerate().map(|(j, &b)| b * coef[(j, 0)]).sum())
.collect();
let rss = e
.iter()
.zip(&fitted)
.map(|(&y, &f)| (y - f) * (y - f))
.sum();
Ok((fitted, rss, rank))
}
fn extend_trend(
covariate: &[f64],
ok: &[bool],
cov_ok: &[f64],
fitted_ok: &[f64],
) -> Result<Vec<f64>, EdgeErrors> {
let n = covariate.len();
if ok.iter().all(|&k| k) {
return Ok(fitted_ok.to_vec());
}
let (knots, values) = collapse_ties(cov_ok, fitted_ok);
if knots.len() < 2 {
return Ok(vec![values[0]; n]);
}
let mut out = vec![0.0; n];
let mut k = 0;
for i in 0..n {
if ok[i] {
out[i] = fitted_ok[k];
k += 1;
} else {
out[i] = interp_linear_extend(covariate[i], &knots, &values);
}
}
Ok(out)
}
fn interp_linear_extend(x: f64, xp: &[f64], fp: &[f64]) -> f64 {
let last = xp.len() - 1;
let (lo, hi) = if x <= xp[0] {
(0, 1)
} else if x >= xp[last] {
(last - 1, last)
} else {
let hi = xp.partition_point(|&knot| knot <= x);
(hi - 1, hi)
};
let t = (x - xp[lo]) / (xp[hi] - xp[lo]);
fp[lo] + t * (fp[hi] - fp[lo])
}
fn loess_fit_weighted(
e: &[f64],
covariate: &[f64],
w: &[f64],
span: f64,
) -> Result<Vec<f64>, EdgeErrors> {
let n = e.len();
let q75 = quantile_type7(w, 0.75)?;
let obs: Vec<usize> = (0..n)
.filter(|&i| e[i].is_finite() && covariate[i].is_finite())
.collect();
let nobs = obs.len();
let mut fitted = vec![f64::NAN; n];
if nobs == 0 {
return Ok(fitted);
}
let xobs: Vec<f64> = obs.iter().map(|&i| covariate[i]).collect();
let yobs: Vec<f64> = obs.iter().map(|&i| e[i]).collect();
let scatter = |fitted: &mut [f64], values: &[f64]| {
for (&i, &v) in obs.iter().zip(values) {
fitted[i] = v;
}
};
if span < 1.0 / nobs as f64 || nobs < 2 {
scatter(&mut fitted, &yobs);
return Ok(fitted);
}
let wobs: Vec<f64> = obs
.iter()
.map(|&i| {
let v = w[i] / q75;
let v = if v.is_nan() { 0.0 } else { v };
v.clamp(LOESS_MIN_WEIGHT, LOESS_MAX_WEIGHT)
})
.collect();
let lo = wobs.iter().copied().fold(f64::INFINITY, f64::min);
let hi = wobs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if hi - lo < EQUAL_WEIGHT_TOL {
let ys = lowess(&xobs, &yobs, span, 0)?;
scatter(&mut fitted, &ys);
return Ok(fitted);
}
if (nobs as f64) < 4.0 + 1.0 / span {
let ys = weighted_line_fitted(&xobs, &yobs, &wobs);
scatter(&mut fitted, &ys);
return Ok(fitted);
}
let fit = weighted_lowess(
&xobs,
&yobs,
Some(&wobs),
Some(LowessParams::new(span, 1, 200, None)),
)?;
scatter(&mut fitted, &fit.fitted);
Ok(fitted)
}
fn weighted_line_fitted(x: &[f64], y: &[f64], w: &[f64]) -> Vec<f64> {
let sw: f64 = w.iter().sum();
let swx: f64 = w.iter().zip(x).map(|(&a, &b)| a * b).sum();
let swy: f64 = w.iter().zip(y).map(|(&a, &b)| a * b).sum();
let swxx: f64 = w.iter().zip(x).map(|(&a, &b)| a * b * b).sum();
let swxy: f64 = (0..x.len()).map(|i| w[i] * x[i] * y[i]).sum();
let det = sw * swxx - swx * swx;
if det.abs() <= WLS_RANK_TOL * (sw * swxx).abs() {
return vec![swy / sw; x.len()];
}
let slope = (sw * swxy - swx * swy) / det;
let intercept = (swy - slope * swx) / sw;
x.iter().map(|&v| intercept + slope * v).collect()
}
fn check_fit_lengths(name: &'static str, x: &[f64], df1: &[f64]) -> Result<usize, EdgeErrors> {
let n = x.len();
if n == 0 {
return Err(EdgeErrors::InvalidArgument(format!(
"{name} was given an empty variance vector."
)));
}
if df1.len() != 1 && df1.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "df1",
expected: n,
got: df1.len(),
});
}
Ok(n)
}
fn check_winsor_tail_p(winsor_tail_p: (f64, f64)) -> Result<(), EdgeErrors> {
for p in [winsor_tail_p.0, winsor_tail_p.1] {
if !(0.0..0.5).contains(&p) {
return Err(EdgeErrors::InvalidArgument(format!(
"winsor_tail_p entries must lie in [0, 0.5); got {p}."
)));
}
}
Ok(())
}
fn informative_mask(x: &[f64], df1: &[f64], df_tol: f64) -> Vec<bool> {
(0..x.len())
.map(|i| {
let d = at(df1, i);
d.is_finite() && d > df_tol && x[i].is_finite() && x[i] > VAR_TOL
})
.collect()
}
fn offset_from_zero(x: &[f64]) -> Vec<f64> {
let mut out: Vec<f64> = x.iter().map(|&v| v.max(0.0)).collect();
let m = median(&out);
let m = if m == 0.0 { 1.0 } else { m };
let floor = ZERO_VAR_OFFSET * m;
for v in &mut out {
*v = v.max(floor);
}
out
}
fn finitise_covariate(covariate: &[f64]) -> Result<Vec<f64>, EdgeErrors> {
if let Some(i) = covariate.iter().position(|v| v.is_nan()) {
return Err(EdgeErrors::InvalidArgument(format!(
"fit_f_dist_trend covariate must not contain NaN; covariate[{i}] is NaN."
)));
}
if covariate.iter().all(|v| v.is_finite()) {
return Ok(covariate.to_vec());
}
let finite: Vec<f64> = covariate
.iter()
.copied()
.filter(|v| v.is_finite())
.collect();
if finite.is_empty() {
return Ok(covariate.iter().map(|v| v.signum()).collect());
}
let lo = finite.iter().fold(f64::INFINITY, |a, &b| a.min(b)) - 1.0;
let hi = finite.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)) + 1.0;
Ok(covariate
.iter()
.map(|&v| {
if v == f64::NEG_INFINITY {
lo
} else if v == f64::INFINITY {
hi
} else {
v
}
})
.collect())
}
#[inline]
fn at(v: &[f64], i: usize) -> f64 {
if v.len() == 1 { v[0] } else { v[i] }
}
fn recycle(v: &[f64], n: usize) -> Vec<f64> {
if v.len() == 1 {
vec![v[0]; n]
} else {
v.to_vec()
}
}
fn subset(v: &[f64], ok: &[bool]) -> Vec<f64> {
v.iter()
.zip(ok)
.filter(|(_, k)| **k)
.map(|(&x, _)| x)
.collect()
}
fn subset_recycled(v: &[f64], ok: &[bool]) -> Vec<f64> {
if v.len() == 1 {
vec![v[0]; ok.iter().filter(|&&k| k).count()]
} else {
subset(v, ok)
}
}
fn mean(x: &[f64]) -> f64 {
x.iter().sum::<f64>() / x.len() as f64
}
fn mean_trigamma(df: &[f64]) -> f64 {
df.iter().map(|&d| trigamma(0.5 * d)).sum::<f64>() / df.len() as f64
}
fn stable_order(x: &[f64]) -> Vec<usize> {
let mut order: Vec<usize> = (0..x.len()).collect();
order.sort_by(|&a, &b| x[a].total_cmp(&x[b]));
order
}
fn cummax_in_order(v: &mut [f64], order: &[usize]) {
let mut running = f64::NEG_INFINITY;
for &i in order {
running = running.max(v[i]);
v[i] = running;
}
}
fn flatten_then_cummax(v: &mut [f64], order: &[usize]) {
let mut running = 0.0;
let (mut best, mut imin) = (f64::INFINITY, 0);
for (k, &idx) in order.iter().enumerate() {
running += v[idx];
let avg = running / (k + 1) as f64;
if avg < best {
best = avg;
imin = k;
}
}
for &idx in &order[..=imin] {
v[idx] = best;
}
cummax_in_order(v, order);
}
fn collapse_ties(x: &[f64], y: &[f64]) -> (Vec<f64>, Vec<f64>) {
let order = stable_order(x);
let mut knots = Vec::with_capacity(x.len());
let mut values = Vec::with_capacity(x.len());
let mut k = 0;
while k < order.len() {
let xi = x[order[k]];
let mut sum = 0.0;
let mut count = 0;
while k < order.len() && x[order[k]] == xi {
sum += y[order[k]];
count += 1;
k += 1;
}
knots.push(xi);
values.push(sum / count as f64);
}
(knots, values)
}
fn map_maybe_par<F>(x: &[f64], f: F) -> Result<Vec<f64>, EdgeErrors>
where
F: Fn(f64) -> Result<f64, EdgeErrors> + Sync + Send,
{
if x.len() >= PARALLEL_THRESHOLD {
x.par_iter().map(|&v| f(v)).collect()
} else {
x.iter().map(|&v| f(v)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn fixture_a() -> Vec<f64> {
(1..=24)
.map(|i| (((i * 37) % 97) + 1) as f64 / 64.0)
.collect()
}
fn covariate_a() -> Vec<f64> {
(1..=24).map(|i| ((i * 53) % 101) as f64 / 8.0).collect()
}
fn fixture_b() -> Vec<f64> {
let mut x: Vec<f64> = (1..=40)
.map(|i| (((i * 37) % 251) + 1) as f64 / 64.0)
.collect();
x[6] = 40.0;
x[22] = 60.0;
x[30] = 25.0;
x
}
fn covariate_b() -> Vec<f64> {
(1..=40).map(|i| ((i * 29) % 97) as f64 / 8.0).collect()
}
fn fixture_d() -> Vec<f64> {
(1..=60)
.map(|j| 1.0 + ((((j * 17) % 7) as f64) - 3.0) / 4096.0)
.collect()
}
fn assert_slices_close(got: &[f64], want: &[f64], tol: f64) {
assert_eq!(got.len(), want.len(), "length mismatch");
for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
if w.is_infinite() {
assert_eq!(g, w, "element {i}: got {g}, want {w}");
} else {
assert_relative_eq!(g, w, max_relative = tol, epsilon = 1e-300);
}
}
}
const A_FIT_SCALE: f64 = 0.640_837_559_492_241_4;
const A_FIT_DF2: f64 = 8.773_643_845_753_776;
#[test]
fn test_fit_f_dist_matches_limma() {
let (scale, df2) = fit_f_dist(&fixture_a(), &[4.0]).unwrap();
assert_relative_eq!(scale, A_FIT_SCALE, max_relative = 1e-12);
assert_relative_eq!(df2, A_FIT_DF2, max_relative = 1e-12);
let (scale2, df2b) = fit_f_dist(&fixture_a(), &[4.0; 24]).unwrap();
assert_relative_eq!(scale2, A_FIT_SCALE, max_relative = 1e-12);
assert_relative_eq!(df2b, A_FIT_DF2, max_relative = 1e-12);
}
const A_FIT_UNEQUAL_SCALE: f64 = 0.642_853_466_657_464_3;
const A_FIT_UNEQUAL_DF2: f64 = 8.830_849_273_819_364;
#[test]
fn test_fit_f_dist_matches_limma_with_unequal_df1() {
let df: Vec<f64> = (0..24).map(|i| [2.0, 4.0, 7.0, 11.0][i % 4]).collect();
let (scale, df2) = fit_f_dist(&fixture_a(), &df).unwrap();
assert_relative_eq!(scale, A_FIT_UNEQUAL_SCALE, max_relative = 1e-12);
assert_relative_eq!(df2, A_FIT_UNEQUAL_DF2, max_relative = 1e-12);
}
const A_VAR_POST: [f64; 24] = [
0.626_092_335_635_731_7,
0.807_129_166_466_796_7,
0.513_555_927_281_286_1,
0.694_592_758_112_351,
0.875_629_588_943_415_9,
0.582_056_349_757_905_2,
0.763_093_180_588_970_1,
0.46951994140345943,
0.650_556_772_234_524_3,
0.831_593_603_065_589_3,
0.538_020_363_880_078_6,
0.719_057_194_711_143_5,
0.900_094_025_542_208_4,
0.606_520_786_356_697_7,
0.787_557_617_187_762_6,
0.493_984_378_002_252,
0.675_021_208_833_317,
0.856_058_039_664_381_9,
0.562_484_800_478_871_2,
0.743_521_631_309_936_1,
0.449_948_392_124_425_4,
0.630_985_222_955_490_3,
0.812_022_053_786_555_2,
0.518_448_814_601_044_5,
];
#[test]
fn test_squeeze_var_untrended_matches_limma() {
let out = squeeze_var(&fixture_a(), &[4.0; 24], None, None).unwrap();
assert_eq!(out.var_prior.len(), 1);
assert_eq!(out.df_prior.len(), 1);
assert_relative_eq!(out.var_prior[0], A_FIT_SCALE, max_relative = 1e-12);
assert_relative_eq!(out.df_prior[0], A_FIT_DF2, max_relative = 1e-12);
assert_slices_close(&out.var_post, &A_VAR_POST, 1e-12);
}
const A_VAR_POST_UNEQUAL: [f64; 24] = [
0.633_786_132_154_682_8,
0.807_775_217_993_745_6,
0.462_234_649_742_128_9,
0.736_954_926_509_288_6,
0.780_935_257_741_026,
0.583_705_872_415_329_6,
0.814_592_562_038_404_4,
0.338_270_538_824_601_3,
0.648_212_517_016_089,
0.832_130_581_643_573_5,
0.496_779_543_104_508_9,
0.780_290_186_040_232_9,
0.795_361_642_602_432_3,
0.608_061_236_065_157_4,
0.849_137_455_400_784_3,
0.381_605_798_355_545_6,
0.662_638_901_877_495_2,
0.856_485_945_293_401_3,
0.531_324_436_466_888_9,
0.823_625_445_571_177_2,
0.529_916_161_152_558_1,
0.632_416_599_714_985_2,
0.883_682_348_763_164_3,
0.42494105788648984,
];
#[test]
fn test_squeeze_var_with_unequal_df_matches_limma_legacy() {
let df: Vec<f64> = (0..24).map(|i| [2.0, 4.0, 7.0, 11.0][i % 4]).collect();
let out = squeeze_var(
&fixture_a(),
&df,
None,
Some(SqueezeVarParams {
legacy: Some(true),
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(out.var_prior[0], A_FIT_UNEQUAL_SCALE, max_relative = 1e-12);
assert_relative_eq!(out.df_prior[0], A_FIT_UNEQUAL_DF2, max_relative = 1e-12);
assert_slices_close(&out.var_post, &A_VAR_POST_UNEQUAL, 1e-12);
}
const A_TREND_DF_PRIOR: f64 = 8.048_268_025_548_438;
const A_TREND_VAR_PRIOR: [f64; 24] = [
0.723_110_005_839_652_5,
0.4143418913007032,
0.742_711_177_713_071_7,
0.445_533_797_163_167_8,
0.757_534_747_932_870_6,
0.47844567134565685,
0.767_947_074_917_522_7,
0.512_694_459_358_742_5,
0.774_435_547_851_372_9,
0.547_775_357_103_530_8,
0.777_577_435_996_847_5,
0.583_053_065_165_493,
0.778_010_618_468_01,
0.617_759_245_685_393_9,
0.776_408_046_642_088_2,
0.650_998_304_607_132_7,
0.773_457_194_450_546_4,
0.681_763_500_708_267_1,
0.769_845_216_183_354_9,
0.708_964_967_024_256_9,
0.396_570_490_459_764_1,
0.731_541_265_734_613_1,
0.42659244020765413,
0.749_196_478_552_777_4,
];
const A_TREND_VAR_POST: [f64; 24] = [
0.680_162_752_154_610_3,
0.665_841_312_493_174_1,
0.573_944_620_848_585_9,
0.567_365_815_527_526_2,
0.967_719_398_780_496_7,
0.470_039_260_966_351_2,
0.855_363_100_033_876_8,
0.37360576761637165,
0.740_385_658_644_428_8,
0.780_912_482_342_576_5,
0.623_172_691_676_536,
0.685_166_309_718_917_1,
1.0073346607510243,
0.589_038_355_511_984_6,
0.886_952_384_683_656_7,
0.491_930_361_035_111_1,
0.765_669_454_536_127_1,
0.896_354_178_113_888_8,
0.643_944_890_799_087,
0.795_213_059_256_225,
0.275_284_845_191_880_1,
0.690_982_318_846_792,
0.679_212_172_165_421_3,
0.583_464_282_856_523_7,
];
#[test]
fn test_squeeze_var_trended_matches_limma() {
let cov = covariate_a();
let out = squeeze_var(&fixture_a(), &[4.0; 24], Some(&cov), None).unwrap();
assert_eq!(out.var_prior.len(), 24);
assert_eq!(out.df_prior.len(), 1);
assert_relative_eq!(out.df_prior[0], A_TREND_DF_PRIOR, max_relative = 1e-12);
assert_slices_close(&out.var_prior, &A_TREND_VAR_PRIOR, 1e-12);
assert_slices_close(&out.var_post, &A_TREND_VAR_POST, 1e-12);
}
#[test]
fn test_fit_f_dist_trend_agrees_with_squeeze_var() {
let cov = covariate_a();
let (scale, df2) = fit_f_dist_trend(&fixture_a(), &[4.0], &cov, None).unwrap();
assert_relative_eq!(df2, A_TREND_DF_PRIOR, max_relative = 1e-12);
assert_slices_close(&scale, &A_TREND_VAR_PRIOR, 1e-12);
}
const B_ROBUST_SCALE: f64 = 2.249456506635014;
const B_ROBUST_DF2: f64 = 40.76894452916315;
const B_ROBUST_SHRUNK_OUTLIERS: [f64; 3] =
[0.37277999082103946, 0.37276287480907766, 0.3749930112639601];
const B_ROBUST_VAR_POST: [f64; 40] = [
2.1015230206841475,
2.15317713101265,
2.204831241341153,
2.2564853516696552,
2.3081394619981577,
2.35979357232666,
36.781761879974304,
2.1126914769713916,
2.164345587299894,
2.2159996976283964,
2.267653807956899,
2.3193079182854013,
2.3709620286139037,
2.072205822930133,
2.123859933258635,
2.1755140435871376,
2.22716815391564,
2.2788222642441425,
2.330476374572645,
2.3821304849011478,
2.0833742792173764,
2.1350283895458793,
55.07696638699776,
2.238336610202884,
2.2899907205313865,
2.341644830859889,
2.3932989411883914,
2.0945427355046204,
2.146196845833123,
2.1978509561616253,
23.049986642149197,
2.30115917681863,
2.3528132871471326,
2.0540570814633616,
2.105711191791864,
2.1573653021203665,
2.209019412448869,
2.2606735227773713,
2.312327633105874,
2.3639817434343766,
];
const B_ROBUST_SCALE_STOCK: f64 = 2.2494565078913564;
const B_ROBUST_DF2_STOCK: f64 = 40.768945564031306;
#[test]
fn test_robust_fit_matches_stock_limma_to_its_own_root_tolerance() {
let (scale, shrunk) = fit_f_dist_robustly(&fixture_b(), &[4.0], None, (0.05, 0.1)).unwrap();
assert_relative_eq!(scale[0], B_ROBUST_SCALE_STOCK, max_relative = 1e-7);
assert_relative_eq!(shrunk[0], B_ROBUST_DF2_STOCK, max_relative = 1e-7);
let ours = (shrunk[0] - B_ROBUST_DF2).abs();
let stock = (B_ROBUST_DF2_STOCK - B_ROBUST_DF2).abs();
assert!(ours < stock, "ours {ours:e} should beat limma's {stock:e}");
}
#[test]
fn test_fit_f_dist_robustly_matches_limma() {
let (scale, shrunk) = fit_f_dist_robustly(&fixture_b(), &[4.0], None, (0.05, 0.1)).unwrap();
assert_eq!(scale.len(), 1);
assert_relative_eq!(scale[0], B_ROBUST_SCALE, max_relative = 1e-12);
for (k, &i) in [6_usize, 22, 30].iter().enumerate() {
assert_relative_eq!(shrunk[i], B_ROBUST_SHRUNK_OUTLIERS[k], max_relative = 1e-12);
}
for (i, &v) in shrunk.iter().enumerate() {
if ![6, 22, 30].contains(&i) {
assert_relative_eq!(v, B_ROBUST_DF2, max_relative = 1e-12);
}
}
}
#[test]
fn test_squeeze_var_robust_matches_limma() {
let out = squeeze_var(
&fixture_b(),
&vec![4.0; 40],
None,
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
assert_eq!(out.var_prior.len(), 40);
assert_eq!(out.df_prior.len(), 40);
for &v in &out.var_prior {
assert_relative_eq!(v, B_ROBUST_SCALE, max_relative = 1e-12);
}
assert_slices_close(&out.var_post, &B_ROBUST_VAR_POST, 1e-12);
}
const B_ROBUST_UNEQUAL_SCALE: f64 = 1.8234463961467966;
const B_ROBUST_UNEQUAL_DF2: f64 = 6.128948796846419;
const B_ROBUST_UNEQUAL_SHRUNK: [f64; 3] =
[0.38502316453516383, 0.378098698877172, 0.4906283453912008];
#[test]
fn test_fit_f_dist_robustly_with_unequal_df1_matches_limma() {
let df: Vec<f64> = (0..40).map(|i| [3.0, 5.0, 8.0, 12.0][i % 4]).collect();
let (scale, shrunk) = fit_f_dist_robustly(&fixture_b(), &df, None, (0.05, 0.1)).unwrap();
assert_relative_eq!(scale[0], B_ROBUST_UNEQUAL_SCALE, max_relative = 1e-12);
for (k, &i) in [6_usize, 22, 30].iter().enumerate() {
assert_relative_eq!(shrunk[i], B_ROBUST_UNEQUAL_SHRUNK[k], max_relative = 1e-12);
}
for (i, &v) in shrunk.iter().enumerate() {
if ![6, 22, 30].contains(&i) {
assert_relative_eq!(v, B_ROBUST_UNEQUAL_DF2, max_relative = 1e-12);
}
}
}
const B_ROBUST_UNEQUAL_VAR_POST: [f64; 40] = [
1.4193375255050005,
1.5307092257091761,
1.7818600631773178,
2.1575056576132927,
2.179282635767482,
2.56966629264046,
38.24701051014726,
1.0922205042149082,
1.650401917138863,
1.8466083338977421,
2.179978853250044,
2.622921501331034,
2.4103470274013445,
1.123550375155024,
1.2687291781946928,
1.5576363479326492,
1.881466308772726,
2.162507442086308,
2.5780976433227707,
3.0883373450487746,
1.3525855901441068,
1.4394494833435902,
57.37452612897447,
2.02305219165039,
2.1125307004065883,
2.4784065502748747,
2.9762164333954972,
0.9577670382520053,
1.5836499817779695,
1.7553485915321565,
23.66074998410438,
2.4884680353681308,
2.343595092040451,
1.0322906327894383,
1.1537170832847938,
1.423182881969746,
1.8147143734118323,
2.0712476997207228,
2.463085548412872,
2.953883879085872,
];
#[test]
fn test_squeeze_var_robust_unequal_df_matches_limma() {
let df: Vec<f64> = (0..40).map(|i| [3.0, 5.0, 8.0, 12.0][i % 4]).collect();
let out = squeeze_var(
&fixture_b(),
&df,
None,
Some(SqueezeVarParams {
robust: true,
legacy: Some(true),
..Default::default()
}),
)
.unwrap();
assert_slices_close(&out.var_post, &B_ROBUST_UNEQUAL_VAR_POST, 1e-12);
}
const B_ROBUST_TREND_VAR_PRIOR: [f64; 40] = [
1.465_770_201_514_433,
2.358_668_761_366_851,
3.339_447_111_256_858,
1.8174232291408439,
1.9734580517452869,
3.0165766069245397,
2.8946582725605468,
1.5611743355219518,
2.5311378090478764,
3.4492900227288428,
1.4817076508386355,
2.3319515209800286,
3.3190827874202276,
1.903968165960005,
1.9370106534545692,
2.9673210210542877,
3.0348075460931447,
1.5176429150692603,
2.503943464329236,
3.4441373069445405,
1.4977238662889754,
2.300227928362065,
3.298_061_598_485_803,
1.9936741757277605,
1.9002124768758293,
2.914_072_802_964_981,
3.182_043_611_244_255,
1.4812391846190578,
2.4831810620408024,
3.4371897720428874,
1.5133318524086241,
2.2621453665329994,
3.2773255485123842,
2.087_594_717_837_246,
1.8645313934402432,
2.860_942_861_174_906,
3.3366770288668715,
1.4533273348912759,
2.4663072510486352,
3.4283856470227216,
];
const B_ROBUST_TREND_SHRUNK: [f64; 3] = [
7.286_571_062_696_712e-8,
7.552_642_835_491_027e-11,
6.50314614594886e-10,
];
#[test]
fn test_squeeze_var_robust_trended_matches_limma() {
let cov = covariate_b();
let out = squeeze_var(
&fixture_b(),
&vec![4.0; 40],
Some(&cov),
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
assert_slices_close(&out.var_prior, &B_ROBUST_TREND_VAR_PRIOR, 1e-12);
for (k, &i) in [6_usize, 22, 30].iter().enumerate() {
assert_relative_eq!(
out.df_prior[i],
B_ROBUST_TREND_SHRUNK[k],
max_relative = 1e-12
);
}
for (i, &v) in out.df_prior.iter().enumerate() {
if ![6, 22, 30].contains(&i) {
assert!(v.is_infinite(), "gene {i} should have an infinite prior");
}
}
for (i, (&post, &prior)) in out.var_post.iter().zip(&out.var_prior).enumerate() {
if ![6, 22, 30].contains(&i) {
assert_eq!(post, prior, "gene {i}");
}
}
}
const B_ROBUST_WIDE_SCALE: f64 = 2.3500199318927755;
const B_ROBUST_WIDE_DF2: f64 = 25.050701533836126;
const B_ROBUST_WIDE_OUTLIER: f64 = 0.3596862065996353;
#[test]
fn test_squeeze_var_robust_honours_winsor_tail_p() {
let out = squeeze_var(
&fixture_b(),
&vec![4.0; 40],
None,
Some(SqueezeVarParams {
robust: true,
winsor_tail_p: (0.2, 0.25),
span: None,
legacy: None,
}),
)
.unwrap();
assert_relative_eq!(out.var_prior[0], B_ROBUST_WIDE_SCALE, max_relative = 1e-12);
assert_relative_eq!(out.df_prior[0], B_ROBUST_WIDE_DF2, max_relative = 1e-12);
assert_relative_eq!(out.df_prior[6], B_ROBUST_WIDE_OUTLIER, max_relative = 1e-12);
}
const B_TINY_WINSOR_SCALE: f64 = 1.8682476400861205;
const B_TINY_WINSOR_DF2: f64 = 3.305496787977293;
#[test]
fn test_squeeze_var_robust_falls_back_when_nothing_would_be_winsorised() {
let out = squeeze_var(
&fixture_b(),
&vec![4.0; 40],
None,
Some(SqueezeVarParams {
robust: true,
winsor_tail_p: (0.005, 0.005),
span: None,
legacy: None,
}),
)
.unwrap();
assert_relative_eq!(out.var_prior[0], B_TINY_WINSOR_SCALE, max_relative = 1e-12);
for &v in &out.df_prior {
assert_relative_eq!(v, B_TINY_WINSOR_DF2, max_relative = 1e-12);
}
}
const B_ZERODF_DF_PRIOR: f64 = 3.001_027_369_685_683;
const B_ZERODF_VAR_PRIOR: f64 = 1.8360659952892042;
const B_ZERODF_VAR_POST: [f64; 40] = [
1.1262753147565674,
1.456_583_979_169_605,
1.8360659952892042,
2.117_201_307_995_68,
2.4475099724087177,
2.7778186368217557,
23.640_828_062_045_24,
1.1976934043593863,
1.5280020687724238,
1.8583107331854616,
2.188_619_397_598_499,
2.5189280620115366,
2.8492367264245746,
0.938_802_829_549_167_7,
1.2691114939622052,
1.5994201583752428,
1.8360659952892042,
2.260037487201318,
2.5903461516143556,
2.9206548160273935,
1.0102209191519866,
1.3405295835650242,
35.067_722_398_496_27,
2.0011469123910994,
2.331455576804137,
2.6617642412171745,
2.9920729056302124,
1.0816390087548056,
1.8360659952892042,
1.7422563375808808,
15.070657309706968,
2.402_873_666_406_956,
2.733_182_330_819_994,
0.822_748_433_944_587,
1.1530570983576245,
1.483365762770662,
1.8136744271836998,
2.1439830915967373,
2.474_291_756_009_775,
2.8046004204228128,
];
#[test]
fn test_squeeze_var_with_zero_df_matches_limma() {
let mut df = vec![4.0; 40];
for i in [2, 16, 28] {
df[i] = 0.0;
}
let out = squeeze_var(&fixture_b(), &df, None, None).unwrap();
assert_relative_eq!(out.df_prior[0], B_ZERODF_DF_PRIOR, max_relative = 1e-12);
assert_relative_eq!(out.var_prior[0], B_ZERODF_VAR_PRIOR, max_relative = 1e-12);
assert_slices_close(&out.var_post, &B_ZERODF_VAR_POST, 1e-12);
for i in [2, 16, 28] {
assert_relative_eq!(out.var_post[i], B_ZERODF_VAR_PRIOR, max_relative = 1e-12);
}
}
const B_ROBUST_ZERODF_SCALE: f64 = 2.2031641721288566;
const B_ROBUST_ZERODF_DF2: f64 = 17.940854604905585;
const B_ROBUST_ZERODF_OUTLIERS: [f64; 3] =
[0.34184209142922345, 0.34036007756885583, 0.3636949709480071];
#[test]
fn test_squeeze_var_robust_with_zero_df_matches_limma() {
let mut df = vec![4.0; 40];
for i in [2, 16, 28] {
df[i] = 0.0;
}
let out = squeeze_var(
&fixture_b(),
&df,
None,
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(
out.var_prior[0],
B_ROBUST_ZERODF_SCALE,
max_relative = 1e-12
);
for (k, &i) in [6_usize, 22, 30].iter().enumerate() {
assert_relative_eq!(
out.df_prior[i],
B_ROBUST_ZERODF_OUTLIERS[k],
max_relative = 1e-12
);
}
for i in [2, 16, 28] {
assert_relative_eq!(out.df_prior[i], B_ROBUST_ZERODF_DF2, max_relative = 1e-12);
}
}
const B_ZEROVAR_DF_PRIOR: f64 = 0.800_079_544_363_124_5;
const B_ZEROVAR_VAR_PRIOR: f64 = 0.276_423_301_654_354_7;
const B_ZEROVAR_VAR_POST_AT_ZERO: f64 = 0.04607436755890474;
#[test]
fn test_squeeze_var_with_zero_variance_matches_limma() {
let mut x = fixture_b();
x[4] = 0.0;
x[18] = 0.0;
let out = squeeze_var(&x, &vec![4.0; 40], None, None).unwrap();
assert_relative_eq!(out.df_prior[0], B_ZEROVAR_DF_PRIOR, max_relative = 1e-12);
assert_relative_eq!(out.var_prior[0], B_ZEROVAR_VAR_PRIOR, max_relative = 1e-12);
for i in [4, 18] {
assert_relative_eq!(
out.var_post[i],
B_ZEROVAR_VAR_POST_AT_ZERO,
max_relative = 1e-12
);
}
}
const B_ZEROVAR_ROBUST_SCALE: f64 = 0.9409970501549559;
const B_ZEROVAR_ROBUST_DF2: f64 = 1.517197722874929;
const B_ZEROVAR_ROBUST_AT_ZERO: f64 = 1.7402266327836557;
#[test]
fn test_squeeze_var_robust_with_zero_variance_matches_limma() {
let mut x = fixture_b();
x[4] = 0.0;
x[18] = 0.0;
let out = squeeze_var(
&x,
&vec![4.0; 40],
None,
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(
out.var_prior[0],
B_ZEROVAR_ROBUST_SCALE,
max_relative = 1e-12
);
assert_relative_eq!(out.df_prior[0], B_ZEROVAR_ROBUST_DF2, max_relative = 1e-12);
assert_relative_eq!(
out.df_prior[4],
B_ZEROVAR_ROBUST_AT_ZERO,
max_relative = 1e-12
);
}
#[test]
fn test_constant_variances_give_an_infinite_prior() {
let out = squeeze_var(&[0.5; 20], &[4.0; 20], None, None).unwrap();
assert!(out.df_prior[0].is_infinite());
assert_eq!(out.var_prior[0], 0.5);
for &v in &out.var_post {
assert_eq!(v, 0.5);
}
}
const D_ROBUST_SCALE: f64 = 1.1942105542067107;
#[test]
fn test_robust_fit_takes_the_infinite_branch_on_flat_variances() {
let x = fixture_d();
let (scale, shrunk) = fit_f_dist_robustly(&x, &[6.0], None, (0.05, 0.1)).unwrap();
assert_relative_eq!(scale[0], D_ROBUST_SCALE, max_relative = 1e-12);
for &v in &shrunk {
assert!(v.is_infinite());
}
let out = squeeze_var(
&x,
&vec![6.0; 60],
None,
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
for &v in &out.var_post {
assert_eq!(v, out.var_prior[0]);
}
}
#[test]
fn test_fewer_than_three_genes_returns_the_input_unmoderated() {
let out = squeeze_var(&[0.5, 2.0], &[4.0], None, None).unwrap();
assert_eq!(out.var_post, vec![0.5, 2.0]);
assert_eq!(out.var_prior, vec![0.5, 2.0]);
assert_eq!(out.df_prior, vec![0.0]);
}
#[test]
fn test_fit_f_dist_with_a_single_usable_gene_gives_zero_prior_df() {
let (scale, df2) = fit_f_dist(&[0.25], &[4.0]).unwrap();
assert_eq!(scale, 0.25);
assert_eq!(df2, 0.0);
let (scale, df2) = fit_f_dist(&[0.25, 1.0, 2.0], &[4.0, 0.0, 0.0]).unwrap();
assert_eq!(scale, 0.25);
assert_eq!(df2, 0.0);
}
#[test]
fn test_empty_input_is_an_error() {
assert!(matches!(
squeeze_var(&[], &[4.0], None, None),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
fit_f_dist(&[], &[4.0]),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
fit_f_dist_trend(&[], &[4.0], &[], None),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
fit_f_dist_robustly(&[], &[4.0], None, (0.05, 0.1)),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_mismatched_df_length_is_an_error() {
assert!(matches!(
squeeze_var(&[1.0, 2.0, 3.0], &[4.0, 4.0], None, None),
Err(EdgeErrors::LengthMismatch { name: "df", .. })
));
assert!(matches!(
fit_f_dist(&[1.0, 2.0, 3.0], &[4.0, 4.0]),
Err(EdgeErrors::LengthMismatch { name: "df1", .. })
));
}
#[test]
fn test_mismatched_covariate_length_is_an_error() {
assert!(matches!(
squeeze_var(&[1.0, 2.0, 3.0], &[4.0], Some(&[1.0, 2.0]), None),
Err(EdgeErrors::LengthMismatch {
name: "covariate",
..
})
));
assert!(matches!(
fit_f_dist_trend(&[1.0, 2.0, 3.0], &[4.0], &[1.0, 2.0], None),
Err(EdgeErrors::LengthMismatch {
name: "covariate",
..
})
));
assert!(matches!(
fit_f_dist_robustly(&[1.0, 2.0, 3.0], &[4.0], Some(&[1.0, 2.0]), (0.05, 0.1)),
Err(EdgeErrors::LengthMismatch {
name: "covariate",
..
})
));
}
#[test]
fn test_negative_or_non_finite_variance_is_an_error() {
for bad in [-1.0, f64::NAN, f64::INFINITY] {
let x = [1.0, bad, 3.0, 4.0];
assert!(
matches!(
squeeze_var(&x, &[4.0], None, None),
Err(EdgeErrors::InvalidArgument(_))
),
"variance {bad} should have been rejected"
);
}
}
#[test]
fn test_negative_or_non_finite_df_is_an_error() {
for bad in [-1.0, f64::NAN, f64::INFINITY] {
let df = [4.0, bad, 4.0, 4.0];
assert!(
matches!(
squeeze_var(&[1.0, 2.0, 3.0, 4.0], &df, None, None),
Err(EdgeErrors::InvalidArgument(_))
),
"df {bad} should have been rejected"
);
}
}
#[test]
fn test_nan_covariate_is_an_error() {
let cov = [1.0, f64::NAN, 3.0];
assert!(matches!(
squeeze_var(&[1.0, 2.0, 3.0], &[4.0], Some(&cov), None),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_non_finite_covariate_is_an_error_for_the_robust_fit() {
let cov: Vec<f64> = (0..40)
.map(|i| if i == 3 { f64::INFINITY } else { i as f64 })
.collect();
assert!(matches!(
squeeze_var(
&fixture_b(),
&vec![4.0; 40],
Some(&cov),
Some(SqueezeVarParams {
robust: true,
..Default::default()
})
),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_bad_winsor_tail_p_is_an_error() {
for bad in [(-0.1, 0.1), (0.05, 0.5), (0.6, 0.1), (f64::NAN, 0.1)] {
assert!(
matches!(
squeeze_var(
&fixture_b(),
&vec![4.0; 40],
None,
Some(SqueezeVarParams {
robust: true,
winsor_tail_p: bad,
span: None,
legacy: None
})
),
Err(EdgeErrors::InvalidArgument(_))
),
"winsor_tail_p {bad:?} should have been rejected"
);
}
}
#[test]
fn test_bad_span_is_an_error() {
for bad in [0.0, -0.5, 1.5, f64::NAN] {
assert!(
matches!(
squeeze_var(
&fixture_b(),
&vec![4.0; 40],
None,
Some(SqueezeVarParams {
robust: false,
winsor_tail_p: (0.05, 0.1),
span: Some(bad),
legacy: None
})
),
Err(EdgeErrors::InvalidArgument(_))
),
"span {bad} should have been rejected"
);
}
}
#[test]
fn test_all_zero_degrees_of_freedom_is_an_error() {
assert!(fit_f_dist(&[1.0, 2.0, 3.0], &[0.0, 0.0, 0.0]).is_err());
}
const B_TREND_ZERODF_DF_PRIOR: f64 = 3.1428781403090924;
const B_TREND_ZERODF_VAR_PRIOR: [f64; 40] = [
1.093287070145291,
1.809109212366991,
3.1695976440542672,
1.4961864064084491,
1.2740702863969318,
2.9264700283297334,
2.3815894804427904,
1.032235139796427,
2.4219204766484834,
3.252655312361141,
1.116778949797686,
1.7444188196732309,
3.15474204792806,
1.5597445620654493,
1.2361400035569214,
2.888530494276931,
2.504499838754266,
1.0253943465690296,
2.357027456392529,
3.2458117931091035,
1.1436967402780736,
1.681767030539927,
3.1381481763422743,
1.6282724259646797,
1.2012295261536194,
2.8478006100933224,
2.634365811751067,
1.0219796133102823,
2.2900015399385154,
3.2387961322379866,
1.1741260384330265,
1.621382550891335,
3.1196645521474418,
1.701986005663366,
1.169348321864014,
2.804251325390985,
2.7712878609845677,
1.0220851562051536,
2.221426882044691,
3.2314233934405734,
];
const B_TREND_ZERODF_VAR_POST: [f64; 40] = [
0.813547133199552,
1.4522591024535012,
3.1695976440542672,
1.962070649286237,
2.1880882391279237,
3.238895894044488,
23.447837444140717,
0.8566839775682209,
1.7918968617700142,
2.481170605328731,
1.86513053802427,
2.4650421622806604,
3.4093357526736754,
0.8350397376824595,
1.0164022475156618,
2.0672058318833684,
2.504499838754266,
1.894920956392324,
2.804590765136243,
3.519406384697505,
0.7219778054255823,
1.2824786672029191,
34.98069159187206,
1.906439049713815,
2.042288519094182,
3.090531554898673,
3.3203689405659866,
0.7384218634151563,
2.2900015399385154,
2.3613228776430644,
14.516576234866568,
2.2971563358583267,
3.28015193116923,
0.7838765414061916,
0.8732641320020631,
1.916373193217012,
2.2256182629071732,
1.779715241568084,
2.631176623581593,
3.399325799540674,
];
#[test]
fn test_squeeze_var_trended_with_zero_df_tracks_limma() {
let cov = covariate_b();
let mut df = vec![4.0; 40];
for i in [2, 16, 28] {
df[i] = 0.0;
}
let out = squeeze_var(&fixture_b(), &df, Some(&cov), None).unwrap();
assert_relative_eq!(
out.df_prior[0],
B_TREND_ZERODF_DF_PRIOR,
max_relative = 1e-12
);
for i in 0..40 {
let tol = if [2, 16, 28].contains(&i) {
2e-3
} else {
1e-12
};
assert_relative_eq!(
out.var_prior[i],
B_TREND_ZERODF_VAR_PRIOR[i],
max_relative = tol
);
assert_relative_eq!(
out.var_post[i],
B_TREND_ZERODF_VAR_POST[i],
max_relative = tol
);
}
}
const B_ROBUST_TREND_ZERODF_VAR_PRIOR: [f64; 40] = [
1.4333622450698638,
2.5136312844707507,
3.4635126858697296,
1.866767433369885,
1.9544625952714154,
3.2323044560151404,
3.13685328484227,
1.5428088570392688,
2.6849287924221663,
3.55523767986531,
1.4658742706464782,
2.454592962182901,
3.449378536691651,
1.9674417532494404,
1.9169967731877045,
3.1708692309376967,
3.3075073569912745,
1.4997577733903524,
2.6568219418972934,
3.556394698841825,
1.483800368786478,
2.40465716256889,
3.4323358503679278,
2.0710253843357904,
1.8819153740480852,
3.104511743149066,
3.4874455140803566,
1.4602023584528363,
2.6356497747742536,
3.5545140440586525,
1.5103304449849393,
2.3464067797790813,
3.4189103519815345,
2.179707101052931,
1.8463856775751422,
3.0413936864461837,
3.6774195208558744,
1.4233480539718921,
2.614646328277017,
3.549542156913126,
];
#[test]
fn test_squeeze_var_robust_trended_with_zero_df_tracks_limma() {
let cov = covariate_b();
let mut df = vec![4.0; 40];
for i in [2, 16, 28] {
df[i] = 0.0;
}
let out = squeeze_var(
&fixture_b(),
&df,
Some(&cov),
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
for (i, &want) in B_ROBUST_TREND_ZERODF_VAR_PRIOR.iter().enumerate() {
let tol = if [2, 16, 28].contains(&i) {
2e-3
} else {
1e-12
};
assert_relative_eq!(out.var_prior[i], want, max_relative = tol);
}
for (i, &v) in out.df_prior.iter().enumerate() {
if [6, 22, 30].contains(&i) {
assert!(v < 1e-5, "gene {i} should be an outlier, got {v:e}");
} else {
assert!(v.is_infinite(), "gene {i} should have an infinite prior");
}
}
}
const B_TREND_EDGE_DROP_DF_PRIOR: f64 = 3.1133565191151713;
const B_TREND_EDGE_DROP_VAR_PRIOR: [f64; 40] = [
1.1351392212183793,
1.6259983592918905,
3.2030895998182918,
1.5024948284759898,
1.2174047384019633,
2.694290075307271,
2.217266528757394,
1.0503433271810356,
2.1359568377083917,
3.6559084086236546,
1.1584457342962922,
1.5754758393831132,
3.1536027723671953,
1.5570273656314104,
1.1893481618062514,
2.6409046242394476,
2.31027252755222,
1.0483627938149591,
2.0775047082558817,
3.6037124755635026,
1.1845712505241406,
1.5268769976093624,
3.1039609894905236,
1.615135524840441,
1.1637403076684496,
2.5869588372036225,
2.407219452267836,
1.0493507247734664,
2.01877998407745,
3.552242266978073,
1.213563940539991,
1.4803154007539074,
3.0540953483565847,
1.676904040679061,
1.14060019322546,
2.5324475693162083,
2.508234601013182,
1.0532518576907344,
1.9601021721177017,
3.50139194976571,
];
#[test]
fn test_squeeze_var_trended_with_dropped_edge_genes_tracks_limma() {
let cov = covariate_b();
let mut df = vec![4.0; 40];
for i in [36, 9, 26] {
df[i] = 0.0;
}
let out = squeeze_var(&fixture_b(), &df, Some(&cov), None).unwrap();
assert_relative_eq!(
out.df_prior[0],
B_TREND_EDGE_DROP_DF_PRIOR,
max_relative = 1e-12
);
for (i, &want) in B_TREND_EDGE_DROP_VAR_PRIOR.iter().enumerate() {
let tol = if [36, 9, 26].contains(&i) {
1e-2
} else {
1e-12
};
assert_relative_eq!(out.var_prior[i], want, max_relative = tol);
}
}
#[test]
fn test_robust_fit_over_the_parallel_threshold_stays_consistent() {
let n = 5_000;
assert!(n > PARALLEL_THRESHOLD);
let var: Vec<f64> = (1..=n)
.map(|i| (((i * 37) % 4093) + 1) as f64 / 1024.0)
.collect();
let cov: Vec<f64> = (1..=n).map(|i| ((i * 29) % 997) as f64 / 64.0).collect();
for covariate in [None, Some(cov.as_slice())] {
let out = squeeze_var(
&var,
&vec![5.0; n],
covariate,
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
assert_eq!(out.var_post.len(), n);
assert_eq!(out.df_prior.len(), n);
for (i, &v) in var.iter().enumerate() {
let (lo, hi) = if v < out.var_prior[i] {
(v, out.var_prior[i])
} else {
(out.var_prior[i], v)
};
assert!(
out.var_post[i] >= lo - 1e-12 && out.var_post[i] <= hi + 1e-12,
"gene {i}: posterior {} outside [{lo}, {hi}]",
out.var_post[i]
);
assert!(out.df_prior[i] > 0.0);
}
}
}
#[test]
fn test_gauss_legendre_integrates_polynomials_exactly() {
let (nodes, weights) = gauss_legendre_unit(QUAD_NODES);
assert_eq!(nodes.len(), QUAD_NODES);
assert_relative_eq!(weights.iter().sum::<f64>(), 1.0, max_relative = 1e-14);
for k in 1..=6_u32 {
let m: f64 = nodes
.iter()
.zip(&weights)
.map(|(&x, &w)| w * x.powi(k as i32))
.sum();
assert_relative_eq!(m, 1.0 / (k as f64 + 1.0), max_relative = 1e-12);
}
for i in 0..QUAD_NODES / 2 {
assert_relative_eq!(
nodes[i] + nodes[QUAD_NODES - 1 - i],
1.0,
max_relative = 1e-15
);
assert!(nodes[i] < nodes[i + 1]);
}
}
#[test]
fn test_f_density_and_quantile_are_consistent() {
for (df1, df2) in [(4.0, 10.0), (1.0, 3.0), (8.0, f64::INFINITY)] {
let lo = f_quantile(0.25, df1, df2).unwrap();
let hi = f_quantile(0.75, df1, df2).unwrap();
let steps = 20_000;
let h = (hi - lo) / steps as f64;
let mut area = 0.0;
for k in 0..=steps {
let x = lo + k as f64 * h;
let w = if k == 0 || k == steps { 0.5 } else { 1.0 };
area += w * f_density(x, df1, df2) * h;
}
assert_relative_eq!(area, 0.5, max_relative = 1e-6);
}
}
#[test]
fn test_f_isf_inverts_the_survival_function() {
for p in [0.5, 0.1, 1e-3, 1e-8, 1e-30] {
let x = f_isf(p, 4.0, 9.0).unwrap();
assert_relative_eq!(f_sf(x, 4.0, 9.0).unwrap(), p, max_relative = 1e-12);
}
}
#[test]
fn test_posterior_var_handles_a_mixed_finite_and_infinite_prior() {
let var = [1.0, 2.0, 3.0];
let df = [4.0];
let prior = [0.5, 0.5, 0.5];
let df_prior = [f64::INFINITY, 4.0, 0.0];
let out = posterior_var(&var, &df, &prior, &df_prior);
assert_eq!(out[0], 0.5);
assert_relative_eq!(out[1], (4.0 * 2.0 + 4.0 * 0.5) / 8.0, max_relative = 1e-15);
assert_relative_eq!(out[2], 3.0, max_relative = 1e-15);
}
const UNEQUAL_TOL: f64 = 1e-10;
const UNEQUAL_FLAT_TOL: f64 = 1e-8;
fn df_a() -> Vec<f64> {
(0..24).map(|i| [2.0, 4.0, 7.0, 11.0][i % 4]).collect()
}
fn df_b() -> Vec<f64> {
(0..40).map(|i| [3.0, 5.0, 8.0, 12.0][i % 4]).collect()
}
fn weights_a() -> Vec<f64> {
(1..=24)
.map(|i| (((i * 13) % 7) + 1) as f64 / 8.0)
.collect()
}
fn fixture_l() -> Vec<f64> {
(1..=600)
.map(|k| (((k * 37) % 1009) + 1) as f64 / 64.0)
.collect()
}
fn covariate_l() -> Vec<f64> {
(1..=600).map(|k| ((k * 53) % 601) as f64 / 16.0).collect()
}
fn df_l() -> Vec<f64> {
(0..600)
.map(|i| [2.0, 4.0, 7.0, 11.0, 5.0, 9.0][i % 6])
.collect()
}
const FLAT_OBJECTIVE_DF2: f64 = 8134.844780382662;
const A_UNEQUAL_SCALE: f64 = 0.5184343518289642;
const A_UNEQUAL_DF2: f64 = 9.397745433510156;
#[test]
fn test_fit_f_dist_unequal_df1_matches_limma() {
let fit = fit_f_dist_unequal_df1(&fixture_a(), &df_a(), None, None, false, None).unwrap();
assert_eq!(fit.scale.len(), 1);
assert_relative_eq!(fit.scale[0], A_UNEQUAL_SCALE, max_relative = UNEQUAL_TOL);
assert_relative_eq!(fit.df2, A_UNEQUAL_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert!(fit.df2_shrunk.is_none());
assert!(fit.df2_outlier.is_none());
}
const A_UNEQUAL_TREND_DF2: f64 = 9.001725728292067;
const A_UNEQUAL_TREND_SCALE: [f64; 24] = [
0.4984351761472433,
0.5259190759879766,
0.5220579477174045,
0.5172782059081166,
0.5407967205854338,
0.5101683139607902,
0.5579627148258335,
0.5037522439866956,
0.5753054276830244,
0.4972336242762296,
0.5933503701444417,
0.49073140218864636,
0.6123167707662234,
0.48464653984385686,
0.6325650467286809,
0.4793839190198376,
0.6541719142254177,
0.4761910799071218,
0.6767040827302486,
0.4820446203847311,
0.5319677784294495,
0.5086669035819186,
0.5222449750000389,
0.5299374322002258,
];
#[test]
fn test_fit_f_dist_unequal_df1_with_covariate_matches_limma() {
let fit = fit_f_dist_unequal_df1(
&fixture_a(),
&df_a(),
Some(&covariate_a()),
None,
false,
None,
)
.unwrap();
assert_relative_eq!(
fit.df2,
A_UNEQUAL_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&fit.scale, &A_UNEQUAL_TREND_SCALE, UNEQUAL_TOL);
}
const B_EQUAL_DF1_TREND_DF2: f64 = 3.527946129589157;
const B_EQUAL_DF1_TREND_SCALE: [f64; 40] = [
1.5628785365841626,
1.8410717969706698,
3.0064784474370954,
1.6047464897309185,
1.550962277050608,
2.530664196930327,
1.6641285766162621,
1.538087660141601,
2.1446475377847625,
3.5300284478905115,
1.5663924718712003,
1.8077937363480212,
2.953894247581084,
1.6097397188234954,
1.5432004715623675,
2.488620106869826,
1.671851655024166,
1.5401244842767179,
2.109571135042458,
3.467530326022418,
1.5700489929120354,
1.7741615853753157,
2.902473821111272,
1.6148765515354977,
1.5381810706324393,
2.447437340451914,
1.6800178539961415,
1.542377223892185,
2.074969835233503,
3.4060893876131297,
1.5738540922468784,
1.7400131855210033,
2.85222400349544,
1.6201678713903858,
1.5351342022111514,
2.407078925442979,
1.688630274244667,
1.5448212279155087,
2.040793487554918,
3.3457097973886314,
];
#[test]
fn test_fit_f_dist_unequal_df1_falls_back_to_plain_lowess_on_equal_weights() {
let fit = fit_f_dist_unequal_df1(
&fixture_b(),
&[4.0; 40],
Some(&covariate_b()),
None,
false,
None,
)
.unwrap();
assert_relative_eq!(
fit.df2,
B_EQUAL_DF1_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&fit.scale, &B_EQUAL_DF1_TREND_SCALE, UNEQUAL_TOL);
}
const A_WEIGHTED_SCALE: f64 = 0.4245678072068865;
const A_WEIGHTED_DF2: f64 = 6.027707924548528;
const A_WEIGHTED_TREND_DF2: f64 = 6.475915483944781;
const A_WEIGHTED_TREND_SCALE: [f64; 24] = [
0.38573627933620297,
0.4835687382808096,
0.42103063978713945,
0.46075826846740037,
0.4547533830567043,
0.4413284036654768,
0.4888752712215187,
0.42391913837253875,
0.5248949744299783,
0.40740842545075884,
0.5636874738132431,
0.3918717174675754,
0.6059157342338731,
0.37786373396983325,
0.652431899788031,
0.36627090352801195,
0.7039146802298344,
0.3591970718950207,
0.7603684158920332,
0.3646918210679019,
0.4992697757771913,
0.4001001268268095,
0.47396419697150305,
0.43461067033555517,
];
#[test]
fn test_fit_f_dist_unequal_df1_with_prior_weights_matches_limma() {
let w = weights_a();
let fit =
fit_f_dist_unequal_df1(&fixture_a(), &df_a(), None, None, false, Some(&w)).unwrap();
assert_relative_eq!(fit.scale[0], A_WEIGHTED_SCALE, max_relative = UNEQUAL_TOL);
assert_relative_eq!(fit.df2, A_WEIGHTED_DF2, max_relative = UNEQUAL_FLAT_TOL);
let fit = fit_f_dist_unequal_df1(
&fixture_a(),
&df_a(),
Some(&covariate_a()),
None,
false,
Some(&w),
)
.unwrap();
assert_relative_eq!(
fit.df2,
A_WEIGHTED_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&fit.scale, &A_WEIGHTED_TREND_SCALE, UNEQUAL_TOL);
}
const B_ROBUST_UNEQ_SCALE: f64 = 1.8836705716043158;
const B_ROBUST_UNEQ_DF2: f64 = 5.139905765416566;
const B_ROBUST_UNEQ_OUTLIER: f64 = 0.31992093620075585;
const B_ROBUST_UNEQ_SHRUNK_OUTLIER: f64 = 2.1424687001362597;
#[test]
fn test_fit_f_dist_unequal_df1_robust_matches_limma() {
let fit = fit_f_dist_unequal_df1(&fixture_b(), &df_b(), None, None, true, None).unwrap();
assert_relative_eq!(
fit.scale[0],
B_ROBUST_UNEQ_SCALE,
max_relative = UNEQUAL_TOL
);
assert_relative_eq!(fit.df2, B_ROBUST_UNEQ_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert_relative_eq!(
fit.df2_outlier.unwrap(),
B_ROBUST_UNEQ_OUTLIER,
max_relative = UNEQUAL_FLAT_TOL
);
let shrunk = fit.df2_shrunk.unwrap();
for (i, &v) in shrunk.iter().enumerate() {
let want = if [6, 22, 30].contains(&i) {
B_ROBUST_UNEQ_SHRUNK_OUTLIER
} else {
B_ROBUST_UNEQ_DF2
};
assert_relative_eq!(v, want, max_relative = UNEQUAL_FLAT_TOL);
}
}
const B_ROBUST_UNEQ_TREND_DF2: f64 = 5.203801107375211;
const B_ROBUST_UNEQ_TREND_OUTLIER: f64 = 0.35604121424293306;
const B_ROBUST_UNEQ_TREND_SHRUNK_OUTLIER: f64 = 2.4117706409509885;
const B_ROBUST_UNEQ_TREND_SCALE_HEAD: [f64; 4] = [
1.7738825995062002,
1.7039177845915112,
2.5263541324098675,
1.9214959973859211,
];
#[test]
fn test_fit_f_dist_unequal_df1_robust_with_covariate_matches_limma() {
let fit = fit_f_dist_unequal_df1(
&fixture_b(),
&df_b(),
Some(&covariate_b()),
None,
true,
None,
)
.unwrap();
assert_relative_eq!(
fit.df2,
B_ROBUST_UNEQ_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_relative_eq!(
fit.df2_outlier.unwrap(),
B_ROBUST_UNEQ_TREND_OUTLIER,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(
&fit.scale[..4],
&B_ROBUST_UNEQ_TREND_SCALE_HEAD,
UNEQUAL_TOL,
);
let shrunk = fit.df2_shrunk.unwrap();
for (i, &v) in shrunk.iter().enumerate() {
let want = if [6, 22, 30].contains(&i) {
B_ROBUST_UNEQ_TREND_SHRUNK_OUTLIER
} else {
B_ROBUST_UNEQ_TREND_DF2
};
assert_relative_eq!(v, want, max_relative = UNEQUAL_FLAT_TOL);
}
}
const A_TWO_INFORMATIVE_SCALE: f64 = 4.104697185054297e-12;
const A_TWO_INFORMATIVE_DF2: f64 = 2.000332267390954;
#[test]
fn test_fit_f_dist_unequal_df1_with_two_informative_genes_matches_limma() {
let mut x = vec![0.0; 24];
x[2] = 0.5;
x[8] = 2.0;
let fit =
fit_f_dist_unequal_df1(&x, &df_a(), Some(&covariate_a()), None, false, None).unwrap();
assert_eq!(fit.scale.len(), 1);
assert_relative_eq!(
fit.scale[0],
A_TWO_INFORMATIVE_SCALE,
max_relative = UNEQUAL_TOL
);
assert_relative_eq!(
fit.df2,
A_TWO_INFORMATIVE_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
}
#[test]
fn test_fit_f_dist_unequal_df1_with_two_informative_genes_and_prior_weights() {
let mut w = vec![0.0; 24];
w[2] = 1.0;
w[8] = 1.0;
let fit =
fit_f_dist_unequal_df1(&fixture_a(), &df_a(), None, None, false, Some(&w)).unwrap();
assert!(fit.scale[0].is_nan());
assert_relative_eq!(fit.df2, FLAT_OBJECTIVE_DF2, max_relative = UNEQUAL_FLAT_TOL);
}
const A_ZERO_VAR_SCALE: f64 = 0.3718178967944987;
const A_ZERO_VAR_DF2: f64 = 6.528194830553999;
const A_ZERO_VAR_TREND_DF2: f64 = 6.99622580953699;
const A_ZERO_VAR_TREND_SCALE_HEAD: [f64; 4] = [
0.29131645136046747,
0.6312057539864651,
0.30766450270877077,
0.5792677420376308,
];
#[test]
fn test_fit_f_dist_unequal_df1_with_a_zero_variance_matches_limma() {
let mut x = fixture_a();
x[4] = 0.0;
let fit = fit_f_dist_unequal_df1(&x, &df_a(), None, None, false, None).unwrap();
assert_relative_eq!(fit.scale[0], A_ZERO_VAR_SCALE, max_relative = UNEQUAL_TOL);
assert_relative_eq!(fit.df2, A_ZERO_VAR_DF2, max_relative = UNEQUAL_FLAT_TOL);
let fit =
fit_f_dist_unequal_df1(&x, &df_a(), Some(&covariate_a()), None, false, None).unwrap();
assert_relative_eq!(
fit.df2,
A_ZERO_VAR_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&fit.scale[..4], &A_ZERO_VAR_TREND_SCALE_HEAD, UNEQUAL_TOL);
}
const A_SMALL_DF1_SCALE: f64 = 0.5133612398309259;
const A_SMALL_DF1_DF2: f64 = 8.777398446239493;
const A_SMALL_DF1_TREND_DF2: f64 = 7.86264899990717;
const A_SMALL_DF1_TREND_SCALE_HEAD: [f64; 4] = [
0.5178088802810284,
0.4553705927989164,
0.5500370079233148,
0.4554740575669429,
];
#[test]
fn test_fit_f_dist_unequal_df1_masks_tiny_df1_matches_limma() {
let mut df = df_a();
df[1] = 0.0;
df[10] = 0.0;
let fit = fit_f_dist_unequal_df1(&fixture_a(), &df, None, None, false, None).unwrap();
assert_relative_eq!(fit.scale[0], A_SMALL_DF1_SCALE, max_relative = UNEQUAL_TOL);
assert_relative_eq!(fit.df2, A_SMALL_DF1_DF2, max_relative = UNEQUAL_FLAT_TOL);
let fit =
fit_f_dist_unequal_df1(&fixture_a(), &df, Some(&covariate_a()), None, false, None)
.unwrap();
assert_relative_eq!(
fit.df2,
A_SMALL_DF1_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&fit.scale[..4], &A_SMALL_DF1_TREND_SCALE_HEAD, UNEQUAL_TOL);
}
const A_SPAN_06_DF2: f64 = 8.458142188315215;
const A_SPAN_06_SCALE_HEAD: [f64; 4] = [
0.47373315982609476,
0.636071387038315,
0.49890191505523185,
0.5689163443682537,
];
#[test]
fn test_fit_f_dist_unequal_df1_honours_a_supplied_span() {
let fit = fit_f_dist_unequal_df1(
&fixture_a(),
&df_a(),
Some(&covariate_a()),
Some(0.6),
false,
None,
)
.unwrap();
assert_relative_eq!(fit.df2, A_SPAN_06_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert_slices_close(&fit.scale[..4], &A_SPAN_06_SCALE_HEAD, UNEQUAL_TOL);
}
const B_ROBUST_SPAN_05_DF2: f64 = 5.169786259019881;
const B_ROBUST_SPAN_05_OUTLIER: f64 = 0.35610213377147193;
const B_ROBUST_SPAN_05_SHRUNK_OUTLIER: f64 = 2.207052068686688;
#[test]
fn test_fit_f_dist_unequal_df1_robust_drops_the_span_in_the_recursion() {
let fit = fit_f_dist_unequal_df1(
&fixture_b(),
&df_b(),
Some(&covariate_b()),
Some(0.5),
true,
None,
)
.unwrap();
assert_relative_eq!(
fit.df2,
B_ROBUST_SPAN_05_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_relative_eq!(
fit.df2_outlier.unwrap(),
B_ROBUST_SPAN_05_OUTLIER,
max_relative = UNEQUAL_FLAT_TOL
);
let shrunk = fit.df2_shrunk.unwrap();
for (i, &v) in shrunk.iter().enumerate() {
let want = if [6, 22, 30].contains(&i) {
B_ROBUST_SPAN_05_SHRUNK_OUTLIER
} else {
B_ROBUST_SPAN_05_DF2
};
assert_relative_eq!(v, want, max_relative = UNEQUAL_FLAT_TOL);
}
}
const D_ROBUST_UNEQ_SCALE: f64 = 1.2121144382645828;
#[test]
fn test_fit_f_dist_unequal_df1_robust_returns_early_when_nothing_is_an_outlier() {
let df: Vec<f64> = (0..60).map(|i| [5.0, 6.0][i % 2]).collect();
let fit = fit_f_dist_unequal_df1(&fixture_d(), &df, None, None, true, None).unwrap();
assert_relative_eq!(
fit.scale[0],
D_ROBUST_UNEQ_SCALE,
max_relative = UNEQUAL_TOL
);
assert_relative_eq!(fit.df2, FLAT_OBJECTIVE_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert!(fit.df2_shrunk.is_none());
assert!(fit.df2_outlier.is_none());
}
const TINY_TREND_SCALE: [f64; 4] = [
1.464482684259621,
1.3640607292931846,
1.3009682044455255,
1.2117587027943375,
];
#[test]
fn test_fit_f_dist_unequal_df1_takes_the_weighted_line_fallback() {
let fit = fit_f_dist_unequal_df1(
&[0.4, 1.1, 2.7, 0.9],
&[2.0, 5.0, 3.0, 9.0],
Some(&[1.0, 2.5, 3.5, 5.0]),
None,
false,
None,
)
.unwrap();
assert_relative_eq!(fit.df2, FLAT_OBJECTIVE_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert_slices_close(&fit.scale, &TINY_TREND_SCALE, UNEQUAL_TOL);
}
const L_SPAN: f64 = 0.95872522021672;
const L_TREND_DF2: f64 = 12.1626698953769;
const L_TREND_SCALE_HEAD: [f64; 6] = [
6.266793017150486,
6.237055818513943,
6.2106185628474035,
6.189155437282101,
6.17402435035205,
6.164737281752587,
];
const L_TREND_SCALE_TAIL: [f64; 3] = [6.187788511158097, 6.221097044299279, 6.2552025692849655];
#[test]
fn test_fit_f_dist_unequal_df1_on_six_hundred_genes_matches_limma() {
let (_, min_span, power) = LIMMA_LOWESS_DEFAULTS;
assert_relative_eq!(
choose_lowess_span(600, UNEQUAL_LOWESS_SMALL_N, min_span, power),
L_SPAN,
max_relative = 1e-14
);
let x = fixture_l();
let cov = covariate_l();
let fit = fit_f_dist_unequal_df1(&x, &df_l(), Some(&cov), None, false, None).unwrap();
assert_relative_eq!(fit.df2, L_TREND_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert_slices_close(&fit.scale[..6], &L_TREND_SCALE_HEAD, UNEQUAL_TOL);
assert_slices_close(&fit.scale[597..], &L_TREND_SCALE_TAIL, UNEQUAL_TOL);
}
const L_ROBUST_DF2: f64 = 123.71294709048462;
const L_ROBUST_OUTLIER: f64 = 5.540757379149584;
const L_ROBUST_SHRUNK_MIN: f64 = 92.75014611625062;
const L_ROBUST_SHRUNK_SUM: f64 = 61510.817250394095;
const L_ROBUST_SCALE_SUM: f64 = 4771.896665776024;
#[test]
fn test_fit_f_dist_unequal_df1_robust_on_six_hundred_genes_matches_limma() {
let x = fixture_l();
let cov = covariate_l();
let fit = fit_f_dist_unequal_df1(&x, &df_l(), Some(&cov), None, true, None).unwrap();
assert_relative_eq!(fit.df2, L_ROBUST_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert_relative_eq!(
fit.df2_outlier.unwrap(),
L_ROBUST_OUTLIER,
max_relative = UNEQUAL_FLAT_TOL
);
let shrunk = fit.df2_shrunk.unwrap();
let lo = shrunk.iter().copied().fold(f64::INFINITY, f64::min);
let hi = shrunk.iter().copied().fold(f64::NEG_INFINITY, f64::max);
assert_relative_eq!(lo, L_ROBUST_SHRUNK_MIN, max_relative = UNEQUAL_FLAT_TOL);
assert_relative_eq!(hi, L_ROBUST_DF2, max_relative = UNEQUAL_FLAT_TOL);
assert_relative_eq!(
shrunk.iter().sum::<f64>(),
L_ROBUST_SHRUNK_SUM,
max_relative = UNEQUAL_FLAT_TOL
);
assert_relative_eq!(
fit.scale.iter().sum::<f64>(),
L_ROBUST_SCALE_SUM,
max_relative = UNEQUAL_FLAT_TOL
);
}
const A_NONLEGACY_VAR_POST: [f64; 24] = [
0.5316502371302103,
0.7135240858185843,
0.39717283628309796,
0.6770166883144111,
0.671480522803574,
0.49893574225974113,
0.7373491747082962,
0.28941257658688035,
0.5453590886668146,
0.7368489057706324,
0.43052345769733313,
0.7191475700239253,
0.6851893743401781,
0.5222605622117893,
0.7706997961225314,
0.33154345829639453,
0.5590679402034189,
0.7601737257226807,
0.46387407911156825,
0.7612784517334394,
0.43294650606665963,
0.5455853821638375,
0.8040504175367664,
0.37367434000590877,
];
#[test]
fn test_squeeze_var_dispatches_to_the_unequal_df1_fit() {
let out = squeeze_var(&fixture_a(), &df_a(), None, None).unwrap();
assert_eq!(out.var_prior.len(), 1);
assert_relative_eq!(
out.var_prior[0],
A_UNEQUAL_SCALE,
max_relative = UNEQUAL_TOL
);
assert_relative_eq!(
out.df_prior[0],
A_UNEQUAL_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&out.var_post, &A_NONLEGACY_VAR_POST, UNEQUAL_TOL);
let same = squeeze_var(
&fixture_a(),
&df_a(),
None,
Some(SqueezeVarParams {
legacy: Some(false),
..Default::default()
}),
)
.unwrap();
assert_slices_close(&same.var_post, &A_NONLEGACY_VAR_POST, UNEQUAL_TOL);
}
const A_NONLEGACY_TREND_VAR_POST: [f64; 24] = [
0.5157624257454849,
0.7246483639336246,
0.39621023177628334,
0.6796361833708949,
0.6952867161375217,
0.4926188547111884,
0.7650035710730789,
0.27827046580821047,
0.5928607775802931,
0.7288233044325285,
0.470491584511392,
0.710653904660662,
0.7680074779125342,
0.49898416275024776,
0.8411469040591076,
0.31026860591294947,
0.6715924695348725,
0.7382898006143429,
0.5515579820461769,
0.74970948333179,
0.44094246280250154,
0.5396498972314272,
0.8132626598467574,
0.3759851284824094,
];
#[test]
fn test_squeeze_var_trended_dispatches_to_the_unequal_df1_fit() {
let out = squeeze_var(&fixture_a(), &df_a(), Some(&covariate_a()), None).unwrap();
assert_eq!(out.var_prior.len(), 24);
assert_relative_eq!(
out.df_prior[0],
A_UNEQUAL_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&out.var_prior, &A_UNEQUAL_TREND_SCALE, UNEQUAL_TOL);
assert_slices_close(&out.var_post, &A_NONLEGACY_TREND_VAR_POST, UNEQUAL_TOL);
}
const B_NONLEGACY_ROBUST_VAR_POST: [f64; 40] = [
1.4082643658894873,
1.5326833000894342,
1.802287600377153,
2.1948422439427717,
2.260546959807817,
2.6729798933216284,
31.948405740377268,
1.0680857573950386,
1.6674043437700605,
1.8793951020857094,
2.2303728622064014,
2.687114495347121,
2.51968693768839,
1.0858103108497903,
1.250533262908344,
1.560358008799388,
1.926544321650634,
2.226106904081985,
2.6584581240356497,
3.1793867467514705,
1.3334017056128773,
1.4325221128460657,
47.72365777520489,
2.0526302602037374,
2.185684299531207,
2.57281870607826,
3.086543385864898,
0.9258737736560045,
1.5925416834934505,
1.7792339148423408,
20.116966714256552,
2.5449025116080866,
2.4448242774117803,
0.9856491236064219,
1.1268641872687837,
1.4181460250603537,
1.851681661374024,
2.125945716838616,
2.534789048396089,
3.037174763012436,
];
#[test]
fn test_squeeze_var_robust_dispatches_to_the_unequal_df1_fit() {
let out = squeeze_var(
&fixture_b(),
&df_b(),
None,
Some(SqueezeVarParams {
robust: true,
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(
out.var_prior[0],
B_ROBUST_UNEQ_SCALE,
max_relative = UNEQUAL_TOL
);
assert_eq!(out.df_prior.len(), 40);
assert_slices_close(&out.var_post, &B_NONLEGACY_ROBUST_VAR_POST, UNEQUAL_TOL);
}
const B_NONLEGACY_ZERODF_VAR_PRIOR: f64 = 1.7559995759381142;
const B_NONLEGACY_ZERODF_DF_PRIOR: f64 = 2.976084781693831;
#[test]
fn test_squeeze_var_with_zero_df_dispatches_to_the_unequal_df1_fit() {
let mut df = df_b();
for i in [2, 16, 28] {
df[i] = 0.0;
}
let out = squeeze_var(
&fixture_b(),
&df,
None,
Some(SqueezeVarParams {
legacy: Some(false),
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(
out.var_prior[0],
B_NONLEGACY_ZERODF_VAR_PRIOR,
max_relative = UNEQUAL_TOL
);
assert_relative_eq!(
out.df_prior[0],
B_NONLEGACY_ZERODF_DF_PRIOR,
max_relative = UNEQUAL_FLAT_TOL
);
for i in [2, 16, 28] {
assert_relative_eq!(
out.var_post[i],
B_NONLEGACY_ZERODF_VAR_PRIOR,
max_relative = UNEQUAL_TOL
);
}
}
const A_SPAN_FORCES_NONLEGACY_VAR_PRIOR_HEAD: [f64; 3] =
[0.755640423479099, 0.460011626412849, 0.7753712958504761];
#[test]
fn test_a_supplied_span_forces_the_unequal_df1_fit() {
let out = squeeze_var(
&fixture_a(),
&[4.0; 24],
Some(&covariate_a()),
Some(SqueezeVarParams {
span: Some(0.7),
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(
out.df_prior[0],
FLAT_OBJECTIVE_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(
&out.var_prior[..3],
&A_SPAN_FORCES_NONLEGACY_VAR_PRIOR_HEAD,
1e-12,
);
let legacy = squeeze_var(&fixture_a(), &[4.0; 24], Some(&covariate_a()), None).unwrap();
assert_relative_eq!(
legacy.df_prior[0],
A_TREND_DF_PRIOR,
max_relative = UNEQUAL_TOL
);
}
#[test]
fn test_unequal_df1_input_validation() {
let x = fixture_a();
let df = df_a();
assert!(matches!(
fit_f_dist_unequal_df1(&x, &[1.0, 2.0], None, None, false, None),
Err(EdgeErrors::LengthMismatch { .. })
));
assert!(matches!(
fit_f_dist_unequal_df1(&x, &df, Some(&[1.0, 2.0]), None, false, None),
Err(EdgeErrors::LengthMismatch { .. })
));
assert!(matches!(
fit_f_dist_unequal_df1(&x, &df, None, None, false, Some(&[1.0, 2.0])),
Err(EdgeErrors::LengthMismatch { .. })
));
let mut bad = vec![1.0; 24];
bad[3] = -1.0;
assert!(matches!(
fit_f_dist_unequal_df1(&x, &df, None, None, false, Some(&bad)),
Err(EdgeErrors::InvalidArgument(_))
));
let mut nan_cov = covariate_a();
nan_cov[5] = f64::NAN;
assert!(matches!(
fit_f_dist_unequal_df1(&x, &df, Some(&nan_cov), None, false, None),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_unequal_df1_with_nothing_informative_is_reported_as_nan() {
let fit =
fit_f_dist_unequal_df1(&[0.0, 0.0, 1.5], &[4.0, 4.0, 4.0], None, None, false, None)
.unwrap();
assert!(fit.scale[0].is_nan());
assert!(fit.df2.is_nan());
let err = squeeze_var(
&[0.0, 0.0, 1.5],
&[4.0, 0.5, 4.0],
None,
Some(SqueezeVarParams {
legacy: Some(false),
..Default::default()
}),
)
.unwrap_err();
assert!(matches!(err, EdgeErrors::InvalidArgument(_)));
}
#[test]
fn test_brent_fmin_reproduces_r_optimize_on_a_flat_objective() {
let par = brent_fmin(UNEQUAL_PAR_LOWER, UNEQUAL_PAR_UPPER, |_| 0.0, OPTIMIZE_TOL);
assert_relative_eq!(
2.0 * par / (1.0 - par),
FLAT_OBJECTIVE_DF2,
max_relative = 1e-13
);
let par = brent_fmin(0.0, 4.0, |p: f64| (p - 1.75).powi(2), OPTIMIZE_TOL);
assert_relative_eq!(par, 1.75, max_relative = 1e-6);
}
const A_MISSING_SCALE: f64 = 0.4642260179706808;
const A_MISSING_DF2: f64 = 7.768255432326142;
const A_MISSING_TREND_DF2: f64 = 7.21925270452184;
const A_MISSING_TREND_SCALE_HEAD: [f64; 4] = [
0.47205935858500275,
0.4287917134093374,
0.483877239045185,
0.4304121690245412,
];
#[test]
fn test_fit_f_dist_unequal_df1_treats_nan_variances_as_missing() {
let mut x = fixture_a();
x[3] = f64::NAN;
x[14] = f64::NAN;
let fit = fit_f_dist_unequal_df1(&x, &df_a(), None, None, false, None).unwrap();
assert_relative_eq!(fit.scale[0], A_MISSING_SCALE, max_relative = UNEQUAL_TOL);
assert_relative_eq!(fit.df2, A_MISSING_DF2, max_relative = UNEQUAL_FLAT_TOL);
let fit =
fit_f_dist_unequal_df1(&x, &df_a(), Some(&covariate_a()), None, false, None).unwrap();
assert_relative_eq!(
fit.df2,
A_MISSING_TREND_DF2,
max_relative = UNEQUAL_FLAT_TOL
);
assert_slices_close(&fit.scale[..4], &A_MISSING_TREND_SCALE_HEAD, UNEQUAL_TOL);
}
const A_SCALAR_DF1_SCALE: f64 = 0.6313881564196626;
const A_SCALAR_DF1_DF2: f64 = 26.23142957377924;
#[test]
fn test_fit_f_dist_unequal_df1_accepts_a_scalar_df1() {
let fit = fit_f_dist_unequal_df1(&fixture_a(), &[6.0], None, None, false, None).unwrap();
assert_relative_eq!(fit.scale[0], A_SCALAR_DF1_SCALE, max_relative = UNEQUAL_TOL);
assert_relative_eq!(fit.df2, A_SCALAR_DF1_DF2, max_relative = UNEQUAL_FLAT_TOL);
let fit = fit_f_dist_unequal_df1(&fixture_a(), &[0.005], None, None, false, None).unwrap();
assert!(fit.scale[0].is_nan());
assert!(fit.df2.is_nan());
}
#[test]
fn test_unequal_df1_robust_over_the_parallel_threshold_stays_consistent() {
let n = PARALLEL_THRESHOLD + 64;
let x: Vec<f64> = (1..=n)
.map(|k| (((k * 37) % 1009) + 1) as f64 / 64.0)
.collect();
let df: Vec<f64> = (0..n).map(|i| [2.0, 4.0, 7.0, 11.0][i % 4]).collect();
let a = fit_f_dist_unequal_df1(&x, &df, None, None, true, None).unwrap();
let b = fit_f_dist_unequal_df1(&x, &df, None, None, true, None).unwrap();
assert_eq!(a.df2, b.df2);
assert_eq!(a.scale, b.scale);
assert!(a.df2.is_finite() && a.df2 > 0.0);
}
#[test]
fn test_all_positive_df_equal_matches_limma_dispatch() {
assert!(all_positive_df_equal(&[4.0, 4.0, 0.0, 4.0]));
assert!(all_positive_df_equal(&[4.0]));
assert!(!all_positive_df_equal(&[4.0, 5.0]));
assert!(!all_positive_df_equal(&[0.0, 0.0]));
}
}