use edge_rs::core::dgelist::DgeList;
use edge_rs::core::expression::{ave_log_cpm, cpm};
use edge_rs::core::filtering::filter_by_expr;
use edge_rs::core::normalisation::{NormMethod, calc_norm_factors};
use edge_rs::dispersion::estimate::estimate_disp;
use edge_rs::glm::ql_fit::{QlFitParams, glm_ql_fit};
use edge_rs::glm::test::{GlmTestInput, Tested, glm_ql_ftest};
use edge_rs::limma::{
contrasts::contrasts_fit,
ebayes::{EBayesParams, EBayesTrend, ebayes},
lm_fit::{LmFitResult, lm_fit},
marray::MArrayLm,
toptable::{DEFAULT_CONF_LEVEL, TopTableParams, TopTableSort, top_table},
voom::{VoomParams, voom_lmfit},
};
use edge_rs::numeric::stats::p_adjust_bh;
use edge_rs::prelude::Recycled;
use crate::prelude::*;
const AVE_LOG_CPM_PRIOR: f64 = 2.0;
const VOOM_PRIOR: f64 = 0.5;
const TREND_PRIOR: f64 = 2.0;
#[allow(clippy::too_many_arguments)]
fn keep_mask(
counts: &[f64],
n_genes: usize,
n_samples: usize,
lib_size: Option<&[f64]>,
design: &[f64],
n_coef: usize,
filter: bool,
min_mean: f64,
) -> Result<Vec<bool>, MethodsErrors> {
let mut keep = if filter {
filter_by_expr(
counts,
n_genes,
n_samples,
lib_size,
None,
Some((design, n_coef)),
None,
)?
} else {
vec![true; n_genes]
};
if min_mean > 0.0 {
for (gene, flag) in keep.iter_mut().enumerate() {
let mean = counts[gene * n_samples..(gene + 1) * n_samples]
.iter()
.sum::<f64>()
/ n_samples as f64;
*flag &= mean >= min_mean;
}
}
if !keep.iter().any(|k| *k) {
return Err(edge_rs::errors::EdgeErrors::NoGenesAfterFiltering { n_genes }.into());
}
Ok(keep)
}
#[derive(Clone, Copy, Debug)]
pub struct EdgeRQlParams {
pub norm_method: NormMethod,
pub filter: bool,
pub min_mean: f64,
pub robust: bool,
pub legacy: bool,
}
impl Default for EdgeRQlParams {
fn default() -> Self {
Self {
norm_method: NormMethod::Tmm,
filter: true,
min_mean: 0.0,
robust: false,
legacy: false,
}
}
}
#[derive(Clone, Debug)]
pub struct EdgeRDgeRes {
pub genes_to_keep: Vec<bool>,
pub log_fc: Vec<f64>,
pub log_cpm: Vec<f64>,
pub f_stat: Vec<f64>,
pub p_val: Vec<f64>,
pub fdr: Vec<f64>,
}
pub fn run_edger_ql(
counts: &[f64],
n_genes: usize,
n_samples: usize,
design: &[f64],
n_coef: usize,
tested: &Tested,
params: &EdgeRQlParams,
) -> Result<EdgeRDgeRes, MethodsErrors> {
let dge = DgeList::new(counts.to_vec(), n_genes, n_samples, None)?;
let keep = keep_mask(
&dge.counts,
n_genes,
n_samples,
None,
design,
n_coef,
params.filter,
params.min_mean,
)?;
let mut dge = dge.subset_genes(&keep)?;
let n_kept = dge.n_genes;
let lib_size = dge.lib_size.clone();
dge.norm_factors = calc_norm_factors(
&dge.counts,
n_kept,
n_samples,
Some(&lib_size),
params.norm_method,
None,
None,
)?;
let offset = dge.offset()?;
let abundance = ave_log_cpm(
&dge.counts,
n_kept,
n_samples,
None,
Some(&offset),
AVE_LOG_CPM_PRIOR,
None,
)?;
let dispersion = if params.legacy {
let disp = estimate_disp(
&dge.counts,
n_kept,
n_samples,
design,
n_coef,
&offset,
None,
Some(&abundance),
None,
)?;
let per_gene = disp.trended.unwrap_or_else(|| vec![disp.common; n_kept]);
Some(Recycled::by_gene(per_gene))
} else {
None
};
let fit = glm_ql_fit(
&dge.counts,
n_kept,
n_samples,
design,
n_coef,
dispersion.as_ref(),
&offset,
None,
&abundance,
Some(QlFitParams {
robust: params.robust,
legacy: params.legacy,
..Default::default()
}),
)?;
let base = fit.as_glm_fit();
let ql = fit.ql_summary();
let input = GlmTestInput {
counts: &dge.counts,
n_genes: n_kept,
n_samples,
design,
n_coef,
dispersion: &fit.dispersion,
offset: &offset,
weights: None,
log_cpm: Some(&abundance),
};
let test = glm_ql_ftest(&input, &base, &ql, tested, true)?;
let fdr = p_adjust_bh(&test.p_value);
Ok(EdgeRDgeRes {
genes_to_keep: keep,
log_fc: test.log_fc,
log_cpm: test.log_cpm.unwrap_or(abundance),
f_stat: test.statistic,
p_val: test.p_value,
fdr,
})
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LimmaRoute {
#[default]
Voom,
Trend,
}
pub fn parse_limma_route(s: &str) -> Option<LimmaRoute> {
match s {
"voom" => Some(LimmaRoute::Voom),
"trend" => Some(LimmaRoute::Trend),
_ => None,
}
}
#[derive(Clone, Copy, Debug)]
pub struct LimmaParams {
pub route: LimmaRoute,
pub norm_method: NormMethod,
pub filter: bool,
pub min_mean: f64,
pub robust: bool,
pub prior_count: Option<f64>,
pub adaptive_span: bool,
pub span: f64,
pub proportion: f64,
}
impl Default for LimmaParams {
fn default() -> Self {
Self {
route: LimmaRoute::Voom,
norm_method: NormMethod::Tmm,
filter: true,
min_mean: 0.0,
robust: false,
prior_count: None,
adaptive_span: true,
span: 0.5,
proportion: 0.01,
}
}
}
impl LimmaParams {
fn resolve_prior_count(&self) -> f64 {
self.prior_count.unwrap_or(match self.route {
LimmaRoute::Voom => VOOM_PRIOR,
LimmaRoute::Trend => TREND_PRIOR,
})
}
}
#[derive(Clone, Debug)]
pub struct LimmaDgeRes {
pub genes_to_keep: Vec<bool>,
pub log_fc: Vec<f64>,
pub ci_lower: Vec<f64>,
pub ci_upper: Vec<f64>,
pub ave_expr: Vec<f64>,
pub t_stat: Vec<f64>,
pub p_val: Vec<f64>,
pub fdr: Vec<f64>,
pub b_stat: Vec<f64>,
}
#[allow(clippy::too_many_arguments)]
pub fn run_limma_dge(
counts: &[f64],
n_genes: usize,
n_samples: usize,
lib_size: Option<&[f64]>,
design: &[f64],
n_coef: usize,
tested: &Tested,
params: &LimmaParams,
) -> Result<LimmaDgeRes, MethodsErrors> {
if let Tested::Coef(coef) = tested
&& coef.len() != 1
{
return Err(MethodsErrors::LimmaMultiCoef { n_coef: coef.len() });
}
let mut dge = DgeList::new(counts.to_vec(), n_genes, n_samples, None)?;
if let Some(ls) = lib_size {
if ls.len() != n_samples {
return Err(MethodsErrors::DgeShapeMismatch {
name: "lib_size",
expected: n_samples,
got: ls.len(),
});
}
dge.lib_size = ls.to_vec();
}
let keep = keep_mask(
&dge.counts,
n_genes,
n_samples,
Some(&dge.lib_size),
design,
n_coef,
params.filter,
params.min_mean,
)?;
let mut dge = dge.subset_genes(&keep)?;
let n_kept = dge.n_genes;
let lib_size = dge.lib_size.clone();
dge.norm_factors = calc_norm_factors(
&dge.counts,
n_kept,
n_samples,
Some(&lib_size),
params.norm_method,
None,
None,
)?;
let prior_count = params.resolve_prior_count();
let (fit, amean): (LmFitResult, Vec<f64>) = match params.route {
LimmaRoute::Voom => {
let eff_lib: Vec<f64> = lib_size
.iter()
.zip(dge.norm_factors.iter())
.map(|(l, f)| l * f)
.collect();
let (voom, lm) = voom_lmfit(
&dge.counts,
n_kept,
n_samples,
design,
n_coef,
Some(&eff_lib),
None,
Some(VoomParams {
normalize_method: NormMethod::None,
span: params.span,
adaptive_span: params.adaptive_span,
prior_count,
save_trend: false,
}),
)?;
(lm, voom.amean)
}
LimmaRoute::Trend => {
let offset = dge.offset()?;
let e = cpm(
&dge.counts,
n_kept,
n_samples,
None,
Some(&offset),
true,
prior_count,
)?;
let amean: Vec<f64> = e
.chunks_exact(n_samples)
.map(|row| row.iter().sum::<f64>() / n_samples as f64)
.collect();
let lm = lm_fit(&e, n_kept, n_samples, design, n_coef, None, None, None)?;
(lm, amean)
}
};
let fit = MArrayLm::from_lm_fit(fit, design, n_coef, n_samples, Some(amean))?;
let (fit, coef) = match tested {
Tested::Coef(coef) => (fit, coef[0]),
Tested::Contrast {
values,
n_contrasts,
} => (contrasts_fit(fit, values, *n_contrasts)?, 0),
};
let trend = match params.route {
LimmaRoute::Voom => EBayesTrend::None,
LimmaRoute::Trend => EBayesTrend::Amean,
};
let fit = ebayes(
fit,
Some(EBayesParams {
proportion: params.proportion,
trend,
robust: params.robust,
..Default::default()
}),
)?;
let top = top_table(
&fit,
coef,
Some(TopTableParams {
number: usize::MAX,
sort_by: TopTableSort::None,
confint: Some(DEFAULT_CONF_LEVEL),
..Default::default()
}),
)?;
let missing = |name| MethodsErrors::DgeShapeMismatch {
name,
expected: n_kept,
got: 0,
};
let ave_expr = top.ave_expr.ok_or_else(|| missing("ave_expr"))?;
let ci_lower = top.ci_lower.ok_or_else(|| missing("ci_lower"))?;
let ci_upper = top.ci_upper.ok_or_else(|| missing("ci_upper"))?;
Ok(LimmaDgeRes {
genes_to_keep: keep,
log_fc: top.log_fc,
ci_lower,
ci_upper,
ave_expr,
t_stat: top.t,
p_val: top.p_value,
fdr: top.adj_p_value,
b_stat: top.b,
})
}