use crate::gcbias::{GcContext, GcFragModel, GcView};
use crate::posbias::{length_class_index, SimplePosBias, NUM_LENGTH_CLASSES, NUM_POS_BINS};
use crate::seqbias::{
conditional_cdf, revcomp_bytes, LogBiasTable, CONTEXT_LEFT, CONTEXT_LENGTH, MIN_ALPHA,
MIN_CDF_MASS,
};
const EPSILON: f64 = 0.375e-10;
const POS_SMOOTH_FRAC: f64 = 0.1;
pub fn positional_factor(obs: &[f64], exp: &[f64]) -> Vec<f64> {
let mut out = Vec::new();
positional_factor_into(obs, exp, &mut out);
out
}
pub fn positional_factor_into(obs: &[f64], exp: &[f64], out: &mut Vec<f64>) {
out.clear();
let n = exp.len();
if n == 0 {
return;
}
let mean_exp: f64 = exp.iter().sum::<f64>() / n as f64;
let c = (POS_SMOOTH_FRAC * mean_exp).max(f64::MIN_POSITIVE);
out.extend(obs.iter().zip(exp).map(|(&o, &e)| (o + c) / (e + c)));
}
#[derive(Clone, Copy, Default)]
pub struct BiasInputs<'a> {
pub seq: Option<(&'a LogBiasTable, &'a LogBiasTable)>,
pub gc: Option<(&'a GcFragModel, GcView<'a>)>,
pub pos: Option<(&'a [f64], &'a [f64])>,
}
impl BiasInputs<'_> {
fn any(&self) -> bool {
self.seq.is_some() || self.gc.is_some() || self.pos.is_some()
}
}
#[allow(clippy::too_many_arguments)]
pub fn corrected_effective_length_full(
seq: &[u8],
cdf: &[f64],
fld_low: usize,
fld_high: usize,
bias: &BiasInputs,
elen: f64,
stride: usize,
no_length_threshold: bool,
) -> f64 {
if !bias.any() {
return elen;
}
let k = if bias.seq.is_some() {
CONTEXT_LENGTH
} else {
1
};
let ref_len = seq.len();
let unprocessed = (ref_len as i32 - elen as i32).max(0);
let cdf_max_arg = (cdf.len() - 1).min(ref_len);
let cdf_max_val = cdf[cdf_max_arg];
if ref_len < k || unprocessed <= 0 || cdf_max_val < MIN_CDF_MASS {
return elen;
}
let cond = |x: i32| conditional_cdf(cdf, cdf_max_arg, cdf_max_val, x);
let have_seq = bias.seq.is_some();
let mut fw: Vec<f64> = Vec::new();
let mut rc: Vec<f64> = Vec::new();
if let Some((tab_fw, tab_rc)) = bias.seq {
fw = vec![1.0f64; ref_len];
rc = vec![1.0f64; ref_len];
let cu = CONTEXT_LEFT;
let rc_seq = revcomp_bytes(seq);
for frag_start in 0..(ref_len - CONTEXT_LENGTH) {
let read_start = frag_start + cu;
if read_start < ref_len {
fw[read_start] = tab_fw
.eval(&seq[frag_start..frag_start + CONTEXT_LENGTH], false)
.exp();
rc[read_start] = tab_rc
.eval(&rc_seq[frag_start..frag_start + CONTEXT_LENGTH], false)
.exp();
}
}
rc.reverse();
}
let gc_model = bias.gc.map(|(m, _)| m);
let gc_ctx = bias.gc.map(|(_, view)| GcContext::build(&view));
let (pos_fw, pos_rc) = match bias.pos {
Some((a, b)) => (Some(a), Some(b)),
None => (None, None),
};
if gc_model.is_none() && (have_seq || pos_fw.is_some()) {
let mut a = vec![1.0f64; ref_len];
let mut b = vec![1.0f64; ref_len];
if have_seq {
a.copy_from_slice(&fw);
b.copy_from_slice(&rc);
}
if let (Some(pf), Some(pr)) = (pos_fw, pos_rc) {
for (((ai, bi), &pfi), &pri) in
a.iter_mut().zip(b.iter_mut()).zip(pf.iter()).zip(pr.iter())
{
*ai *= pfi;
*bi *= pri;
}
}
return crate::seqbias::eff_len_from_xcorr(
&a,
&b,
cond,
fld_low,
fld_high,
elen,
unprocessed,
stride.max(1),
no_length_threshold,
);
}
let stride = stride.max(1) as i32;
let max_len = (ref_len as i32).min(fld_high as i32 + 1);
let mut fl = fld_low as i32;
let mut done = fl >= max_len;
let sp = if fl > 0 { fl - 1 } else { 0 };
let mut prev_mass = cond(sp);
let mut eff = 0.0f64;
while !done {
if fl >= max_len {
done = true;
fl = max_len - 1;
}
let fl_weight = cond(fl) - prev_mass;
prev_mass = cond(fl);
let mut mass = 0.0f64;
let kmax = ref_len as i32 - fl;
match (have_seq, gc_model.zip(gc_ctx.as_ref()), pos_fw.zip(pos_rc)) {
(false, Some((gc, ctx)), None) => {
let mut kstart = 0i32;
while kstart < kmax {
let frag_end = kstart + fl - 1;
if let Some((ff, cf)) = ctx.desc(kstart, frag_end) {
mass += gc.get(ff, cf);
} else {
mass += 1.0;
}
kstart += 1;
}
}
_ => {
let mut kstart = 0i32;
while kstart < kmax {
let frag_start = kstart;
let frag_end = kstart + fl - 1;
let mut frag_factor = if have_seq {
fw[frag_start as usize] * rc[frag_end as usize]
} else {
1.0
};
if let (Some(gc), Some(ctx)) = (gc_model, gc_ctx.as_ref()) {
if let Some((ff, cf)) = ctx.desc(frag_start, frag_end) {
frag_factor *= gc.get(ff, cf);
}
}
if let (Some(pf), Some(pr)) = (pos_fw, pos_rc) {
frag_factor *= pf[frag_start as usize] * pr[frag_end as usize];
}
mass += frag_factor;
kstart += 1;
}
}
}
eff += fl_weight * mass;
fl += stride;
}
if no_length_threshold {
if eff > 1.0 {
eff
} else {
elen
}
} else {
let offset = (unprocessed as f64).max(1.0);
eff.max(elen.min(offset))
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_expected_pos<FL>(
num_targets: usize,
ref_len_of: FL,
alphas: &[f64],
eff_lens: &[f64],
cdf: &[f64],
quantiles: &[u32],
k: usize,
) -> (Vec<SimplePosBias>, Vec<SimplePosBias>)
where
FL: Fn(usize) -> usize + Sync,
{
use rayon::prelude::*;
type Partials = (Vec<SimplePosBias>, Vec<SimplePosBias>);
fn empty() -> Partials {
(
(0..NUM_LENGTH_CLASSES)
.map(|_| SimplePosBias::new_empty(NUM_POS_BINS))
.collect(),
(0..NUM_LENGTH_CLASSES)
.map(|_| SimplePosBias::new_empty(NUM_POS_BINS))
.collect(),
)
}
let (sum5, sum3) = (0..num_targets)
.into_par_iter()
.fold(empty, |mut acc, tid| {
if alphas[tid] < MIN_ALPHA || eff_lens[tid] <= 0.0 {
return acc;
}
let ref_len = ref_len_of(tid) as i32;
if (ref_len as usize) <= k {
return acc;
}
let unprocessed = ref_len - eff_lens[tid] as i32;
if unprocessed <= 0 {
return acc;
}
let cdf_max_arg = (cdf.len() - 1).min(ref_len as usize);
let cdf_max_val = cdf[cdf_max_arg];
if cdf_max_val < MIN_CDF_MASS {
return acc;
}
let weight = alphas[tid] / eff_lens[tid];
let lc = length_class_index(quantiles, ref_len as u32);
let cond = |x: i32| conditional_cdf(cdf, cdf_max_arg, cdf_max_val, x);
for frag_start in 0..(ref_len - k as i32) {
let max_fw = ref_len - frag_start + 1;
let max_rc = frag_start;
let density_fw = cond(max_fw);
let density_rc = cond(max_rc);
if weight * density_fw > EPSILON {
acc.0[lc].add_mass(frag_start, ref_len, weight * density_fw);
}
if weight * density_rc > EPSILON {
acc.1[lc].add_mass(frag_start, ref_len, weight * density_rc);
}
}
acc
})
.reduce(empty, |mut a, b| {
for (x, y) in a.0.iter_mut().zip(&b.0) {
x.combine(y);
}
for (x, y) in a.1.iter_mut().zip(&b.1) {
x.combine(y);
}
a
});
let seed = |sums: Vec<SimplePosBias>| -> Vec<SimplePosBias> {
sums.into_iter()
.map(|s| {
let mut m = SimplePosBias::default();
m.combine(&s);
m.finalize();
m
})
.collect()
};
(seed(sum5), seed(sum3))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::posbias::compute_length_quantiles;
use crate::seqbias::fld_cdf_and_bounds;
type PosModels = (Vec<SimplePosBias>, Vec<SimplePosBias>);
fn mass_diff(a: &PosModels, b: &PosModels) -> f64 {
a.0.iter()
.chain(a.1.iter())
.zip(b.0.iter().chain(b.1.iter()))
.map(|(pa, pb)| {
pa.masses()
.iter()
.zip(pb.masses())
.map(|(x, y)| (x - y).abs())
.sum::<f64>()
})
.sum()
}
#[test]
fn build_expected_pos_respects_num_targets_bound() {
let lens = [200usize, 200, 200, 200, 200, 400];
let num_refs = lens.len();
let alphas = vec![1.0; num_refs];
let eff_lens = vec![150.0; num_refs];
let mut pmf = vec![0.0; 200];
pmf[100] = 1.0;
let (cdf, _lo, _hi) = fld_cdf_and_bounds(&pmf);
let qlens: Vec<u32> = lens.iter().map(|&l| l as u32).collect();
let quantiles = compute_length_quantiles(&qlens, NUM_LENGTH_CLASSES);
let k = 1usize;
let exclude = build_expected_pos(5, |t| lens[t], &alphas, &eff_lens, &cdf, &quantiles, k);
let include = build_expected_pos(6, |t| lens[t], &alphas, &eff_lens, &cdf, &quantiles, k);
assert!(exclude
.0
.iter()
.chain(exclude.1.iter())
.all(|p| p.masses().iter().all(|v| v.is_finite())));
let diff = mass_diff(&exclude, &include);
assert!(
diff > 1e-9,
"a target beyond num_targets must not contribute (diff={diff})"
);
let mut alphas0 = alphas.clone();
alphas0[5] = 0.0;
let include0 = build_expected_pos(6, |t| lens[t], &alphas0, &eff_lens, &cdf, &quantiles, k);
let diff2 = mass_diff(&exclude, &include0);
assert!(
diff2 < 1e-9,
"zero-alpha target must not contribute (diff={diff2})"
);
}
}