use crate::errors::EdgeErrors;
use crate::limma::squeeze_var::{SqueezeVarParams, squeeze_var};
use crate::numeric::stats::median as numeric_median;
const PHI_FLOOR: f64 = 1e-8;
const MIN_GENES_FOR_PRIOR: usize = 3;
#[derive(Clone, Copy, Debug)]
pub struct ShrinkParams {
pub robust: bool,
}
impl Default for ShrinkParams {
fn default() -> Self {
Self { robust: true }
}
}
#[derive(Clone, Debug)]
pub struct ShrinkResult {
pub phi_raw: Vec<f64>,
pub phi_post: Vec<f64>,
pub phi_prior: Vec<f64>,
pub df_residual: f64,
pub df_prior: Vec<f64>,
pub dispersion_shrunk: Vec<f64>,
}
pub fn sc_residual_df(n_cells: usize, n_coef: usize, n_subjects: usize) -> f64 {
let used = n_coef + n_subjects.saturating_sub(1);
(n_cells.saturating_sub(used)).max(1) as f64
}
pub fn shrink_sc_dispersion(
dispersion: &[f64],
convergence: &[i32],
covariate: Option<&[f64]>,
df_residual: f64,
params: Option<ShrinkParams>,
) -> Result<ShrinkResult, EdgeErrors> {
let params = params.unwrap_or_default();
let n_genes = dispersion.len();
if n_genes == 0 {
return Err(EdgeErrors::InvalidArgument(
"shrink_sc_dispersion was given an empty dispersion vector.".to_string(),
));
}
if convergence.len() != n_genes {
return Err(EdgeErrors::LengthMismatch {
name: "convergence",
expected: n_genes,
got: convergence.len(),
});
}
if let Some(cov) = covariate
&& cov.len() != n_genes
{
return Err(EdgeErrors::LengthMismatch {
name: "covariate",
expected: n_genes,
got: cov.len(),
});
}
if !(df_residual.is_finite() && df_residual > 0.0) {
return Err(EdgeErrors::InvalidArgument(format!(
"shrink_sc_dispersion needs a positive finite df_residual; got {df_residual}."
)));
}
let phi_raw: Vec<f64> = dispersion
.iter()
.map(|&d| if d > 0.0 { 1.0 / d } else { f64::INFINITY })
.collect();
let usable: Vec<usize> = (0..n_genes)
.filter(|&g| convergence[g] == 1 && phi_raw[g].is_finite())
.collect();
if usable.len() < MIN_GENES_FOR_PRIOR {
return Ok(unshrunk(phi_raw, df_residual, dispersion));
}
let phi_usable: Vec<f64> = usable.iter().map(|&g| phi_raw[g].max(PHI_FLOOR)).collect();
let cov_usable: Option<Vec<f64>> = covariate.map(|c| usable.iter().map(|&g| c[g]).collect());
let df = [df_residual];
let mut attempts: Vec<(Option<&[f64]>, bool)> = Vec::with_capacity(3);
if let Some(cov) = cov_usable.as_deref() {
attempts.push((Some(cov), params.robust));
}
attempts.push((None, params.robust));
if params.robust {
attempts.push((None, false));
}
let mut fit = None;
for (cov, robust) in attempts {
let sv_params = SqueezeVarParams {
robust,
..SqueezeVarParams::default()
};
if let Ok(result) = squeeze_var(&phi_usable, &df, cov, Some(sv_params)) {
fit = Some(result);
break;
}
}
let Some(sv) = fit else {
return Ok(unshrunk(phi_raw, df_residual, dispersion));
};
let median_prior = median(&sv.var_prior);
let mut phi_post = vec![median_prior; n_genes];
let mut phi_prior = vec![median_prior; n_genes];
for (slot, &gene) in usable.iter().enumerate() {
phi_post[gene] = sv.var_post[slot];
phi_prior[gene] = if sv.var_prior.len() == 1 {
sv.var_prior[0]
} else {
sv.var_prior[slot]
};
}
let dispersion_shrunk = phi_post
.iter()
.map(|&p| if p > 0.0 { 1.0 / p } else { f64::INFINITY })
.collect();
Ok(ShrinkResult {
phi_raw,
phi_post,
phi_prior,
df_residual,
df_prior: sv.df_prior,
dispersion_shrunk,
})
}
fn unshrunk(phi_raw: Vec<f64>, df_residual: f64, dispersion: &[f64]) -> ShrinkResult {
let finite: Vec<f64> = phi_raw.iter().copied().filter(|p| p.is_finite()).collect();
let prior = median(&finite);
ShrinkResult {
phi_post: phi_raw.clone(),
phi_prior: vec![prior; phi_raw.len()],
phi_raw,
df_residual,
df_prior: vec![0.0],
dispersion_shrunk: dispersion.to_vec(),
}
}
fn median(values: &[f64]) -> f64 {
let finite: Vec<f64> = values.iter().copied().filter(|v| v.is_finite()).collect();
numeric_median(&finite)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn fixture() -> (Vec<f64>, Vec<i32>) {
let dispersion = vec![0.5, 0.4, 0.6, 0.55, 0.45, 0.52, 0.48, 5.0, 0.51, 0.49];
let convergence = vec![1; 10];
(dispersion, convergence)
}
#[test]
fn test_residual_df_subtracts_coefficients_and_subjects() {
assert_relative_eq!(sc_residual_df(100, 2, 6), 93.0);
assert_relative_eq!(sc_residual_df(10, 2, 20), 1.0);
}
#[test]
fn test_shrinkage_pulls_the_outlier_towards_the_others() {
let (dispersion, convergence) = fixture();
let out = shrink_sc_dispersion(&dispersion, &convergence, None, 93.0, None).unwrap();
let raw_outlier = out.phi_raw[7];
let post_outlier = out.phi_post[7];
let prior = out.phi_prior[7];
assert!(raw_outlier < prior);
assert!(post_outlier > raw_outlier);
assert!(post_outlier < prior);
}
#[test]
fn test_non_converged_genes_get_the_prior() {
let (dispersion, mut convergence) = fixture();
convergence[3] = -20;
let out = shrink_sc_dispersion(&dispersion, &convergence, None, 93.0, None).unwrap();
assert_relative_eq!(out.phi_post[3], out.phi_prior[3]);
}
#[test]
fn test_zero_dispersion_gives_infinite_phi_and_is_excluded() {
let (mut dispersion, convergence) = fixture();
dispersion[0] = 0.0;
let out = shrink_sc_dispersion(&dispersion, &convergence, None, 93.0, None).unwrap();
assert!(out.phi_raw[0].is_infinite());
assert!(out.phi_post[0].is_finite());
assert_relative_eq!(out.phi_post[0], out.phi_prior[0]);
}
#[test]
fn test_too_few_converged_genes_returns_unshrunk() {
let (dispersion, mut convergence) = fixture();
for c in convergence.iter_mut().skip(2) {
*c = -20;
}
let out = shrink_sc_dispersion(&dispersion, &convergence, None, 93.0, None).unwrap();
assert_eq!(out.dispersion_shrunk, dispersion);
assert_relative_eq!(out.df_prior[0], 0.0);
}
#[test]
fn test_a_covariate_gives_a_trended_prior() {
let (dispersion, convergence) = fixture();
let covariate: Vec<f64> = (0..10).map(|i| i as f64).collect();
let out =
shrink_sc_dispersion(&dispersion, &convergence, Some(&covariate), 93.0, None).unwrap();
let first = out.phi_prior[0];
assert!(out.phi_prior.iter().any(|p| (p - first).abs() > 1e-12));
}
#[test]
fn test_shrunk_dispersion_is_the_reciprocal_of_the_posterior() {
let (dispersion, convergence) = fixture();
let out = shrink_sc_dispersion(&dispersion, &convergence, None, 93.0, None).unwrap();
for (&d, &p) in out.dispersion_shrunk.iter().zip(out.phi_post.iter()) {
assert_relative_eq!(d, 1.0 / p, max_relative = 1e-14);
}
}
#[test]
fn test_empty_input_is_rejected() {
assert!(shrink_sc_dispersion(&[], &[], None, 93.0, None).is_err());
}
#[test]
fn test_length_mismatch_is_rejected() {
let (dispersion, _) = fixture();
assert!(shrink_sc_dispersion(&dispersion, &[1, 1], None, 93.0, None).is_err());
}
#[test]
fn test_non_positive_df_is_rejected() {
let (dispersion, convergence) = fixture();
assert!(shrink_sc_dispersion(&dispersion, &convergence, None, 0.0, None).is_err());
assert!(shrink_sc_dispersion(&dispersion, &convergence, None, f64::NAN, None).is_err());
}
}