use rayon::prelude::*;
use crate::cdf::{ScoreDistribution, grid_value};
use crate::convert::convert_type;
use crate::error::{Error, Result, arg_err};
use crate::matrix::Matrix;
use crate::motif::{Motif, MotifType};
use crate::ops::with_pseudocount;
#[derive(Debug, Clone, PartialEq)]
pub struct PvalueOptions {
pub bkg: Vec<Vec<f64>>,
pub nthreads: usize,
}
impl Default for PvalueOptions {
fn default() -> Self {
PvalueOptions {
bkg: Vec::new(),
nthreads: 1,
}
}
}
pub(crate) fn with_threads<T: Send>(nthreads: usize, f: impl FnOnce() -> T + Send) -> Result<T> {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(nthreads)
.build()
.map_err(|e| Error::Argument(format!("could not start threads: {e}")))?;
Ok(pool.install(f))
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScoringMatrix {
pub pwm: Matrix,
pub bkg: Vec<f64>,
pub pseudocount_added: bool,
}
pub fn scoring_matrix(motif: &Motif, bkg: Option<&[f64]>) -> Result<ScoringMatrix> {
let bkg = match bkg {
Some(b) => {
if b.len() != motif.alphabet.len() {
return arg_err(format!(
"the background must have {} values, one per letter",
motif.alphabet.len()
));
}
b.to_vec()
}
None => motif.bkg.clone(),
};
let pwm = convert_type(motif, MotifType::Pwm, None, false)?;
let infinite = pwm.matrix.as_slice().iter().any(|v| v.is_infinite());
if bkg == motif.bkg {
let pwm = if infinite {
convert_type(&with_pseudocount(motif)?, MotifType::Pwm, None, false)?
} else {
pwm
};
return Ok(ScoringMatrix {
pwm: pwm.into_parts().matrix,
bkg,
pseudocount_added: infinite,
});
}
let base = if infinite {
with_pseudocount(motif)?
} else {
motif.clone()
};
let ppm = convert_type(&base, MotifType::Ppm, None, false)?;
let pseudocount_added = infinite;
let mut parts = ppm.into_parts();
parts.bkg = bkg.clone();
let ppm = Motif::from_parts(parts)?;
let pwm = convert_type(&ppm, MotifType::Pwm, None, false)?;
Ok(ScoringMatrix {
pwm: pwm.into_parts().matrix,
bkg,
pseudocount_added,
})
}
pub fn score_distributions(
motifs: &[Motif],
opts: &PvalueOptions,
) -> Result<Vec<ScoreDistribution>> {
if opts.bkg.len() > 1 && opts.bkg.len() != motifs.len() {
return arg_err("give one background for all motifs or one per motif");
}
let mats = motifs
.iter()
.enumerate()
.map(|(i, m)| {
let bkg = match opts.bkg.len() {
0 => None,
1 => Some(opts.bkg[0].as_slice()),
_ => Some(opts.bkg[i].as_slice()),
};
scoring_matrix(m, bkg)
})
.collect::<Result<Vec<_>>>()?;
with_threads(opts.nthreads, || {
mats.par_iter()
.map(|m| ScoreDistribution::new(&m.pwm, &m.bkg))
.collect()
})
}
fn check_queries(n: usize, queries: &[(usize, f64)]) -> Result<()> {
for &(i, v) in queries {
if i >= n {
return arg_err(format!("query refers to motif {i}, but there are {n}"));
}
if !v.is_finite() {
return arg_err("scores and P-values must be finite numbers");
}
}
Ok(())
}
fn per_motif(
motifs: &[Motif],
queries: &[(usize, f64)],
opts: &PvalueOptions,
answer: impl Fn(&ScoreDistribution, f64) -> f64 + Sync,
) -> Result<Vec<f64>> {
if opts.bkg.len() > 1 && opts.bkg.len() != motifs.len() {
return arg_err("give one background for all motifs or one per motif");
}
let mut by_motif: Vec<Vec<usize>> = vec![Vec::new(); motifs.len()];
for (q, &(i, _)) in queries.iter().enumerate() {
by_motif[i].push(q);
}
let answers: Vec<Vec<(usize, f64)>> = with_threads(opts.nthreads, || {
by_motif
.par_iter()
.enumerate()
.filter(|(_, qs)| !qs.is_empty())
.map(|(i, qs)| {
let bkg = match opts.bkg.len() {
0 => None,
1 => Some(opts.bkg[0].as_slice()),
_ => Some(opts.bkg[i].as_slice()),
};
let m = scoring_matrix(&motifs[i], bkg)?;
let dist = ScoreDistribution::new(&m.pwm, &m.bkg);
Ok(qs
.iter()
.map(|&q| (q, answer(&dist, queries[q].1)))
.collect())
})
.collect::<Result<Vec<_>>>()
})??;
let mut out = vec![0.0; queries.len()];
for (q, v) in answers.into_iter().flatten() {
out[q] = v;
}
Ok(out)
}
pub fn motif_pvalue(
motifs: &[Motif],
queries: &[(usize, f64)],
opts: &PvalueOptions,
) -> Result<Vec<f64>> {
check_queries(motifs.len(), queries)?;
per_motif(motifs, queries, opts, |d, s| d.pvalue(s))
}
pub fn pvalue_to_score(
motifs: &[Motif],
queries: &[(usize, f64)],
opts: &PvalueOptions,
) -> Result<Vec<f64>> {
check_queries(motifs.len(), queries)?;
per_motif(motifs, queries, opts, |d, p| d.score(p))
}
pub fn min_pvalues(motifs: &[Motif], opts: &PvalueOptions) -> Result<Vec<f64>> {
if opts.bkg.len() > 1 && opts.bkg.len() != motifs.len() {
return arg_err("give one background for all motifs or one per motif");
}
motifs
.iter()
.enumerate()
.map(|(i, motif)| {
let bkg = match opts.bkg.len() {
0 => None,
1 => Some(opts.bkg[0].as_slice()),
_ => Some(opts.bkg[i].as_slice()),
};
let m = scoring_matrix(motif, bkg)?;
let (mut top, mut total) = (1.0, 1.0);
for col in m.pwm.columns() {
let grid: Vec<i32> = col.iter().map(|&v| grid_value(v)).collect();
let best = *grid.iter().max().unwrap();
top *= grid
.iter()
.zip(&m.bkg)
.filter(|(g, _)| **g == best)
.map(|(_, b)| b)
.sum::<f64>();
total *= m.bkg.iter().sum::<f64>();
}
Ok(if total > 0.0 {
(top / total).min(1.0)
} else {
0.0
})
})
.collect()
}
pub fn log_pvalue_text(pvalue: &str) -> Result<f64> {
let s = pvalue.trim();
let bad = || Error::Argument(format!("'{pvalue}' is not a P-value"));
let value: f64 = s.parse().map_err(|_| bad())?;
if value.is_nan() || value < 0.0 {
return Err(bad());
}
if value >= f64::MIN_POSITIVE {
return Ok(value.ln());
}
match s.split_once(['e', 'E']) {
Some((m, e)) => {
let m: f64 = m.parse().map_err(|_| bad())?;
let e = e.parse::<i64>().map_err(|_| bad())? as f64;
Ok(m.ln() + e * std::f64::consts::LN_10)
}
None => Ok(value.ln()),
}
}