pub const DEFAULT_COND_BINS: usize = 3;
pub const DEFAULT_GC_BINS: usize = 25;
pub const GC_MAX_RATIO: f64 = 1000.0;
const NORM_PRIOR: f64 = 0.1;
#[inline]
pub fn bin_frac(frac: i32, n: usize) -> usize {
if n == 101 {
return (frac.clamp(0, 100)) as usize;
}
let w = 100.0 / n as f64;
let b = (frac as f64 / w) as i32;
b.clamp(0, n as i32 - 1) as usize
}
#[derive(Debug, Clone)]
pub struct GcFragModel {
cond_bins: usize,
gc_bins: usize,
counts: Vec<f64>,
normalized: bool,
ctx_lut: Vec<u8>,
gc_lut: Vec<u8>,
}
impl GcFragModel {
pub fn new(cond_bins: usize, gc_bins: usize) -> Self {
let ctx_lut = (0..=100).map(|f| bin_frac(f, cond_bins) as u8).collect();
let gc_lut = (0..=100).map(|f| bin_frac(f, gc_bins) as u8).collect();
Self {
cond_bins,
gc_bins,
counts: vec![0.0; cond_bins * gc_bins],
normalized: false,
ctx_lut,
gc_lut,
}
}
pub fn default_model() -> Self {
Self::new(DEFAULT_COND_BINS, DEFAULT_GC_BINS)
}
#[inline]
fn idx(&self, ctx_frac: i32, gc_frac: i32) -> usize {
let ctx = if self.cond_bins > 1 {
self.ctx_lut[ctx_frac.clamp(0, 100) as usize] as usize
} else {
0
};
let gc = self.gc_lut[gc_frac.clamp(0, 100) as usize] as usize;
ctx * self.gc_bins + gc
}
pub fn dump(&self) -> &[f64] {
&self.counts
}
pub fn inc(&mut self, gc_frac: i32, ctx_frac: i32, weight: f64) {
debug_assert!(!self.normalized, "cannot inc a normalized model");
let i = self.idx(ctx_frac, gc_frac);
self.counts[i] += weight;
}
#[inline]
pub fn get(&self, gc_frac: i32, ctx_frac: i32) -> f64 {
self.counts[self.idx(ctx_frac, gc_frac)]
}
pub fn combine_counts(&mut self, other: &GcFragModel) {
debug_assert!(!self.normalized && !other.normalized);
for (a, b) in self.counts.iter_mut().zip(&other.counts) {
*a += *b;
}
}
pub fn normalize(&mut self) {
if self.normalized {
return;
}
for r in 0..self.cond_bins {
let base = r * self.gc_bins;
let row = &mut self.counts[base..base + self.gc_bins];
let row_mass: f64 = row.iter().map(|c| NORM_PRIOR + *c).sum();
if row_mass > 0.0 {
let norm = 1.0 / row_mass;
for c in row.iter_mut() {
*c = (NORM_PRIOR + *c) * norm;
}
}
}
self.normalized = true;
}
}
pub fn gc_ratio(
observed: &mut GcFragModel,
expected: &mut GcFragModel,
max_ratio: f64,
) -> GcFragModel {
observed.normalize();
expected.normalize();
let min_ratio = 1.0 / max_ratio;
let mut out = GcFragModel::new(observed.cond_bins, observed.gc_bins);
for i in 0..out.counts.len() {
let e = expected.counts[i];
let rat = if e != 0.0 {
observed.counts[i] / e
} else {
max_ratio
};
out.counts[i] = rat.clamp(min_ratio, max_ratio);
}
out.normalized = true; out
}
use crate::seqbias::{
conditional_cdf, log_bias, revcomp_bytes, SBModel, CONTEXT_LEFT, CONTEXT_LENGTH, MIN_ALPHA,
MIN_CDF_MASS,
};
pub const GC_SAMP_STRIDE: usize = 5;
const OUTSIDE_5P: i32 = 4; const OUTSIDE_3P: i32 = 3; const INSIDE_5P: i32 = 1; const INSIDE_3P: i32 = 2;
#[inline]
fn lrint(x: f64) -> i32 {
#[cfg(target_arch = "x86_64")]
{
#[allow(unsafe_code)]
unsafe {
core::arch::x86_64::_mm_cvtsd_si32(core::arch::x86_64::_mm_set_sd(x))
}
}
#[cfg(not(target_arch = "x86_64"))]
{
x.round_ties_even() as i32
}
}
pub fn gc_prefix(seq: &[u8]) -> Vec<u32> {
let mut prefix = Vec::with_capacity(seq.len());
let mut acc = 0u32;
for &b in seq {
if matches!(b, b'G' | b'g' | b'C' | b'c') {
acc += 1;
}
prefix.push(acc);
}
prefix
}
#[inline]
fn gc_in(prefix: &[u32], a: i32, b: i32) -> i64 {
let lo = if a > 0 {
prefix[(a - 1) as usize] as i64
} else {
0
};
prefix[b as usize] as i64 - lo
}
#[inline]
pub fn gc_frac(prefix: &[u32], s: i32, e: i32) -> i32 {
lrint(100.0 * gc_in(prefix, s, e) as f64 / (e - s + 1) as f64)
}
pub struct GcRank {
rank: sux::rank_sel::Rank9,
}
impl GcRank {
pub fn new(concat: &[u8]) -> GcRank {
use sux::traits::bit_vec_ops::BitVecOpsMut;
let mut bv = sux::bits::BitVec::new(concat.len());
for (i, &b) in concat.iter().enumerate() {
if matches!(b, b'G' | b'g' | b'C' | b'c') {
bv.set(i, true);
}
}
GcRank {
rank: sux::rank_sel::Rank9::new(bv),
}
}
#[inline]
pub fn view(&self, off: usize, len: usize) -> GcView<'_> {
use sux::traits::Rank;
GcView::Rank {
rank: &self.rank,
off,
base: self.rank.rank(off),
len,
}
}
}
#[derive(Clone, Copy)]
pub enum GcView<'a> {
Dense(&'a [u32]),
Rank {
rank: &'a sux::rank_sel::Rank9,
off: usize,
base: usize,
len: usize,
},
}
impl GcView<'_> {
#[inline]
pub fn cum(&self, p: i32) -> i64 {
match self {
GcView::Dense(prefix) => prefix[p as usize] as i64,
GcView::Rank {
rank, off, base, ..
} => {
use sux::traits::Rank;
(rank.rank(off + p as usize + 1) - base) as i64
}
}
}
#[inline]
pub fn ref_len(&self) -> usize {
match self {
GcView::Dense(prefix) => prefix.len(),
GcView::Rank { len, .. } => *len,
}
}
}
#[derive(Clone, Copy)]
pub enum GcStore<'a> {
Dense(&'a [Vec<u32>]),
Rank {
rank: &'a GcRank,
offsets: &'a [u64],
},
}
impl<'a> GcStore<'a> {
#[inline]
pub fn view(&self, tid: usize) -> GcView<'a> {
match *self {
GcStore::Dense(prefixes) => GcView::Dense(&prefixes[tid]),
GcStore::Rank { rank, offsets } => {
let off = offsets[tid] as usize;
let len = (offsets[tid + 1] - offsets[tid]) as usize;
rank.view(off, len)
}
}
}
}
#[inline]
pub fn gc_desc(v: &GcView, s: i32, e: i32) -> Option<(i32, i32)> {
let last = v.ref_len() as i32 - 1;
let cs = if s > 0 { v.cum(s - 1) } else { 0 };
let ce = v.cum(e);
let fs = s - OUTSIDE_5P;
let fe = s + INSIDE_5P;
let ts = e - INSIDE_3P;
let te = e + OUTSIDE_3P;
let fp_left = fs >= 0;
let fp_right = fe <= last;
let tp_left = ts >= 0;
let tp_right = te <= last;
let fps = if fp_left { v.cum(fs) } else { 0 };
let fpe = if fp_right { v.cum(fe) } else { ce };
let tps = if tp_left { v.cum(ts) } else { 0 };
let tpe = if tp_right { v.cum(te) } else { ce };
let fs_c = fs.max(0);
let fe_c = fe.min(last);
let ts_c = ts.max(0);
let te_c = te.min(last);
let fp_context_size = if !fp_left { fe_c + 1 } else { fe_c - fs_c };
let tp_context_size = if !tp_left { te_c + 1 } else { te_c - ts_c };
let context_size = (fp_context_size + tp_context_size) as f64;
if context_size == 0.0 {
return None;
}
let frag_frac = lrint(100.0 * (ce - cs) as f64 / (e - s + 1) as f64);
let context_frac = lrint(100.0 * ((fpe - fps) + (tpe - tps)) as f64 / context_size);
Some((frag_frac, context_frac))
}
pub struct GcContext {
cum: Vec<u32>, fp_count: Vec<i64>, fp_wlen: Vec<i32>, tp_count: Vec<i64>, tp_wlen: Vec<i32>, }
impl GcContext {
pub fn build(v: &GcView) -> GcContext {
let n = v.ref_len();
let last = n as i32 - 1;
let cum: Vec<u32> = (0..n).map(|p| v.cum(p as i32) as u32).collect();
let at = |i: i32| cum[i as usize] as i64;
let mut fp_count = vec![0i64; n];
let mut fp_wlen = vec![0i32; n];
let mut tp_count = vec![0i64; n];
let mut tp_wlen = vec![0i32; n];
for p in 0..n {
let pos = p as i32;
let ts = pos - INSIDE_3P;
let te = pos + OUTSIDE_3P;
let tp_left = ts >= 0;
let tp_right = te <= last;
let tps = if tp_left { at(ts) } else { 0 };
let tpe = if tp_right { at(te) } else { at(pos) };
let ts_c = ts.max(0);
let te_c = te.min(last);
tp_count[p] = tpe - tps;
tp_wlen[p] = if !tp_left { te_c + 1 } else { te_c - ts_c };
let fs = pos - OUTSIDE_5P;
let fe = pos + INSIDE_5P;
let fp_left = fs >= 0;
let fp_right = fe <= last;
let fps = if fp_left { at(fs) } else { 0 };
let fpe = if fp_right { at(fe) } else { at(pos) };
let fs_c = fs.max(0);
let fe_c = fe.min(last);
fp_count[p] = fpe - fps;
fp_wlen[p] = if !fp_left { fe_c + 1 } else { fe_c - fs_c };
}
GcContext {
cum,
fp_count,
fp_wlen,
tp_count,
tp_wlen,
}
}
#[inline]
pub fn desc(&self, s: i32, e: i32) -> Option<(i32, i32)> {
let context_size = (self.fp_wlen[s as usize] + self.tp_wlen[e as usize]) as f64;
if context_size == 0.0 {
return None;
}
let cs = if s > 0 {
self.cum[(s - 1) as usize] as i64
} else {
0
};
let ce = self.cum[e as usize] as i64;
let count = (self.fp_count[s as usize] + self.tp_count[e as usize]) as f64;
let frag_frac = lrint(100.0 * (ce - cs) as f64 / (e - s + 1) as f64);
let context_frac = lrint(100.0 * count / context_size);
Some((frag_frac, context_frac))
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_expected_gc<'a, FS, FP>(
num_refs: usize,
seq_of: FS,
view_of: FP,
alphas: &[f64],
eff_lens: &[f64],
cdf: &[f64],
fld_low: usize,
fld_high: usize,
cond_bins: usize,
gc_bins: usize,
k: usize,
stride: usize,
) -> GcFragModel
where
FS: Fn(usize) -> &'a [u8] + Sync,
FP: Fn(usize) -> GcView<'a> + Sync,
{
let stride = stride.max(1) as i32;
use rayon::prelude::*;
let per_tid = |tid: usize| -> Option<GcFragModel> {
if alphas[tid] < MIN_ALPHA || eff_lens[tid] <= 0.0 {
return None;
}
let seq = seq_of(tid);
let ref_len = seq.len();
if ref_len <= k {
return None;
}
let cdf_max_arg = (cdf.len() - 1).min(ref_len);
let cdf_max_val = cdf[cdf_max_arg];
if cdf_max_val < MIN_CDF_MASS {
return None;
}
let view = view_of(tid);
let ctx = GcContext::build(&view);
let weight = alphas[tid] / eff_lens[tid];
let cond = |x: i32| conditional_cdf(cdf, cdf_max_arg, cdf_max_val, x);
let sp = if fld_low > 0 { fld_low as i32 - 1 } else { 0 };
let mut model = GcFragModel::new(cond_bins, gc_bins);
for frag_start in 0..(ref_len - k) {
let mut prev = cond(sp);
let mut fl = fld_low as i32;
while fl <= fld_high as i32 {
let frag_end = frag_start as i32 + fl - 1;
if (frag_end as usize) < ref_len {
if let Some((ff, cf)) = ctx.desc(frag_start as i32, frag_end) {
model.inc(ff, cf, weight * (cond(fl) - prev));
}
prev = cond(fl);
} else {
break;
}
fl += stride;
}
}
Some(model)
};
(0..num_refs)
.into_par_iter()
.fold(
|| GcFragModel::new(cond_bins, gc_bins),
|mut acc, tid| {
if let Some(m) = per_tid(tid) {
acc.combine_counts(&m);
}
acc
},
)
.reduce(
|| GcFragModel::new(cond_bins, gc_bins),
|mut a, b| {
a.combine_counts(&b);
a
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn gc_corrected_effective_length(
seq: &[u8],
prefix: &[u32],
cdf: &[f64],
fld_low: usize,
fld_high: usize,
gc_bias: &GcFragModel,
seq_models: Option<(&SBModel, &SBModel, &SBModel, &SBModel)>,
elen: f64,
stride: usize,
) -> f64 {
let k = if seq_models.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 mut fw = vec![1.0f64; ref_len];
let mut rc = vec![1.0f64; ref_len];
if let Some((obs_fw, exp_fw, obs_rc, exp_rc)) = seq_models {
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] = log_bias(
obs_fw,
exp_fw,
&seq[frag_start..frag_start + CONTEXT_LENGTH],
false,
)
.exp();
rc[read_start] = log_bias(
obs_rc,
exp_rc,
&rc_seq[frag_start..frag_start + CONTEXT_LENGTH],
false,
)
.exp();
}
}
rc.reverse();
}
let ctx = GcContext::build(&GcView::Dense(prefix));
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;
let mut kstart = 0i32;
while kstart < kmax {
let frag_start = kstart;
let frag_end = kstart + fl - 1;
let mut frag_factor = fw[frag_start as usize] * rc[frag_end as usize];
if let Some((ff, cf)) = ctx.desc(frag_start, frag_end) {
frag_factor *= gc_bias.get(ff, cf);
}
mass += frag_factor;
kstart += 1;
}
eff += fl_weight * mass;
fl += stride;
}
let offset = (unprocessed as f64).max(1.0);
eff.max(elen.min(offset))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bin_frac_identity_at_101() {
assert_eq!(bin_frac(0, 101), 0);
assert_eq!(bin_frac(57, 101), 57);
assert_eq!(bin_frac(100, 101), 100);
}
#[test]
fn bin_frac_coarse() {
assert_eq!(bin_frac(0, 3), 0);
assert_eq!(bin_frac(30, 3), 0);
assert_eq!(bin_frac(40, 3), 1);
assert_eq!(bin_frac(70, 3), 2);
assert_eq!(bin_frac(100, 3), 2); }
#[test]
fn normalize_makes_rows_sum_to_one() {
let mut m = GcFragModel::new(2, 5);
for gc in 0..5 {
m.inc(gc * 25, 10, (gc + 1) as f64); }
m.normalize();
let row0: f64 = (0..5).map(|c| m.counts[c]).sum();
assert!((row0 - 1.0).abs() < 1e-9, "row0 sum = {row0}");
}
#[test]
fn ratio_of_identical_models_is_one() {
let mut obs = GcFragModel::new(1, 101);
let mut exp = GcFragModel::new(1, 101);
for gc in 0..=100 {
obs.inc(gc, 0, (gc + 1) as f64);
exp.inc(gc, 0, (gc + 1) as f64);
}
let r = gc_ratio(&mut obs, &mut exp, GC_MAX_RATIO);
for gc in 0..=100 {
assert!(
(r.get(gc, 0) - 1.0).abs() < 1e-9,
"gc {gc}: {}",
r.get(gc, 0)
);
}
}
#[test]
fn gc_frac_basic() {
let seq = b"ACGTACGTAC"; let p = gc_prefix(seq);
assert_eq!(gc_frac(&p, 0, 9), 50);
let seq2 = b"GGGGCCCC";
let p2 = gc_prefix(seq2);
assert_eq!(gc_frac(&p2, 0, 7), 100);
assert_eq!(gc_frac(&p, 2, 2), 100); assert_eq!(gc_frac(&p, 0, 0), 0); }
#[test]
fn gc_desc_matches_frag_and_context() {
let seq: Vec<u8> = b"ACGT".iter().cycle().take(40).copied().collect();
let p = gc_prefix(&seq);
let (ff, cf) = gc_desc(&GcView::Dense(&p), 10, 29).unwrap();
assert_eq!(ff, 50, "fragFrac");
assert!((0..=100).contains(&cf), "contextFrac in range: {cf}");
assert_eq!(ff, gc_frac(&p, 10, 29));
}
#[test]
fn idx_lut_matches_bin_frac() {
for &(cb, gb) in &[(3usize, 25usize), (1, 101), (3, 101), (4, 50)] {
let m = GcFragModel::new(cb, gb);
for cf in -5i32..=105 {
for ff in -5i32..=105 {
let want_ctx = if cb > 1 { bin_frac(cf, cb) } else { 0 };
let want = want_ctx * gb + bin_frac(ff, gb);
assert_eq!(m.idx(cf, ff), want, "cb={cb} gb={gb} cf={cf} ff={ff}");
}
}
}
}
#[test]
fn gc_context_matches_gc_desc() {
let bases = [b'A', b'C', b'G', b'T', b'C', b'G', b'A', b'T', b'G', b'C'];
let seq: Vec<u8> = (0..200).map(|i| bases[(i * 7 + 3) % bases.len()]).collect();
let p = gc_prefix(&seq);
let v = GcView::Dense(&p);
let ctx = GcContext::build(&v);
let n = seq.len() as i32;
for fl in 1..=120 {
let kmax = n - fl;
for s in 0..kmax {
let e = s + fl - 1;
assert_eq!(
ctx.desc(s, e),
gc_desc(&v, s, e),
"mismatch at fl={fl}, s={s}, e={e}"
);
}
}
}
#[test]
fn gc_rank_view_matches_dense() {
let bases = [b'A', b'C', b'G', b'T', b'G', b'C', b'A', b'T'];
let pad: Vec<u8> = (0..37).map(|i| bases[(i * 3) % bases.len()]).collect();
let seq: Vec<u8> = (0..150).map(|i| bases[(i * 5 + 1) % bases.len()]).collect();
let mut concat = pad.clone();
concat.extend_from_slice(&seq);
let p = gc_prefix(&seq);
let rank = GcRank::new(&concat);
let dense = GcView::Dense(&p);
let rview = rank.view(pad.len(), seq.len());
for fl in 1..=100 {
let n = seq.len() as i32;
for s in 0..(n - fl) {
let e = s + fl - 1;
assert_eq!(
gc_desc(&dense, s, e),
gc_desc(&rview, s, e),
"fl={fl} s={s}"
);
}
}
}
#[test]
fn gc_desc_context_window_geometry() {
let mut seq = vec![b'A'; 60];
for i in 17..=21 {
seq[i] = b'G'; }
for i in 39..=43 {
seq[i] = b'C'; }
let p = gc_prefix(&seq);
let (_ff, cf) = gc_desc(&GcView::Dense(&p), 20, 40).unwrap();
assert_eq!(cf, 100, "contextFrac");
}
#[test]
fn ratio_clamped() {
let mut obs = GcFragModel::new(1, 2);
let mut exp = GcFragModel::new(1, 2);
obs.inc(0, 0, 1e6); exp.inc(75, 0, 1e6); let r = gc_ratio(&mut obs, &mut exp, 1000.0);
for v in [r.get(0, 0), r.get(75, 0)] {
assert!(
(1.0 / 1000.0 - 1e-12..=1000.0 + 1e-6).contains(&v),
"ratio {v} out of clamp"
);
}
}
}