1const ORDER: [u32; 9] = [0, 1, 2, 2, 2, 2, 2, 2, 2];
17pub const CONTEXT_LENGTH: usize = 9;
19pub const CONTEXT_LEFT: usize = 3;
21pub const CONTEXT_RIGHT: usize = 5;
23const ROWS: usize = 64;
25const PRIOR: f64 = 1e-10;
27const LOG_SMALL: f64 = -11.512_925_464_970_229; #[inline]
32fn base2bit(b: u8) -> u32 {
33 match b {
34 b'A' | b'a' => 0,
35 b'C' | b'c' => 1,
36 b'G' | b'g' => 2,
37 b'T' | b't' => 3,
38 _ => 0,
39 }
40}
41
42#[inline]
43fn complement_bit(x: u32) -> u32 {
44 3 - x }
46
47#[derive(Debug, Clone)]
49pub struct SBModel {
50 probs: Vec<f64>,
53 marginals: Vec<f64>,
55 shifts: [u32; CONTEXT_LENGTH],
56 masks: [u32; CONTEXT_LENGTH],
57 trained: bool,
58}
59
60impl Default for SBModel {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl SBModel {
67 pub fn new() -> Self {
68 let mut shifts = [0u32; CONTEXT_LENGTH];
69 let mut masks = [0u32; CONTEXT_LENGTH];
70 for i in 0..CONTEXT_LENGTH {
71 shifts[i] = (2 * CONTEXT_LENGTH as u32) - 2 * (i as u32 + 1);
73 let width = 2 * (ORDER[i] + 1);
74 masks[i] = (1u32 << width) - 1;
75 }
76 Self {
77 probs: vec![PRIOR; ROWS * CONTEXT_LENGTH],
78 marginals: vec![PRIOR; 4 * CONTEXT_LENGTH],
79 shifts,
80 masks,
81 trained: false,
82 }
83 }
84
85 fn encode(context: &[u8], rev_comp: bool) -> u32 {
88 debug_assert_eq!(context.len(), CONTEXT_LENGTH);
89 let mut mer = 0u32;
90 if rev_comp {
91 for &b in context.iter().rev() {
93 mer = (mer << 2) | complement_bit(base2bit(b));
94 }
95 } else {
96 for &b in context {
97 mer = (mer << 2) | base2bit(b);
98 }
99 }
100 mer
101 }
102
103 #[inline]
104 fn index_at(&self, mer: u32, pos: usize) -> usize {
105 ((mer >> self.shifts[pos]) & self.masks[pos]) as usize
106 }
107
108 pub fn dump(&self) -> &[f64] {
112 &self.probs
113 }
114
115 pub fn add_context(&mut self, context: &[u8], rev_comp: bool, weight: f64) {
117 debug_assert!(!self.trained, "cannot add to a normalized model");
118 let mer = Self::encode(context, rev_comp);
119 for pos in 0..CONTEXT_LENGTH {
120 let idx = self.index_at(mer, pos);
121 self.probs[pos * ROWS + idx] += weight;
122 }
123 }
124
125 pub fn normalize(&mut self) {
128 if self.trained {
129 return;
130 }
131 for pos in 0..CONTEXT_LENGTH {
132 let num_states = 4usize.pow(ORDER[pos]);
133 for s in 0..num_states {
134 let node = s * 4;
135 let base = pos * ROWS + node;
136 let tot: f64 = self.probs[base..base + 4].iter().sum();
137 if tot > 0.0 {
138 for j in 0..4 {
139 self.probs[base + j] /= tot;
140 self.marginals[pos * 4 + j] += self.probs[base + j];
141 }
142 }
143 }
144 for j in 0..4 {
145 self.marginals[pos * 4 + j] /= num_states as f64;
146 }
147 }
148 for p in &mut self.probs {
149 *p = if *p > 0.0 { p.ln() } else { LOG_SMALL };
150 }
151 self.trained = true;
152 }
153
154 pub fn evaluate_log(&self, context: &[u8], rev_comp: bool) -> f64 {
156 debug_assert!(self.trained, "evaluate_log requires a normalized model");
157 let mer = Self::encode(context, rev_comp);
158 let mut lp = 0.0;
159 for pos in 0..CONTEXT_LENGTH {
160 let idx = self.index_at(mer, pos);
161 lp += self.probs[pos * ROWS + idx];
162 }
163 lp
164 }
165
166 pub fn is_trained(&self) -> bool {
167 self.trained
168 }
169
170 pub fn combine_counts(&mut self, other: &SBModel) {
173 debug_assert!(!self.trained && !other.trained, "combine before normalize");
174 for (a, b) in self.probs.iter_mut().zip(&other.probs) {
175 *a += *b - PRIOR; }
177 }
178}
179
180pub(crate) fn revcomp_bytes(seq: &[u8]) -> Vec<u8> {
182 seq.iter()
183 .rev()
184 .map(|&b| match b {
185 b'A' | b'a' => b'T',
186 b'C' | b'c' => b'G',
187 b'G' | b'g' => b'C',
188 b'T' | b't' => b'A',
189 _ => b'A',
190 })
191 .collect()
192}
193
194pub(crate) const MIN_ALPHA: f64 = 1e-8;
197pub(crate) const MIN_CDF_MASS: f64 = 1e-10;
199pub const FLD_SAMP_STRIDE: usize = 5;
202
203pub fn fld_cdf_and_bounds(pmf_lin: &[f64]) -> (Vec<f64>, usize, usize) {
207 let mut cdf = vec![0.0f64; pmf_lin.len()];
208 let mut acc = 0.0;
209 let (mut lo, mut hi) = (0usize, 1usize);
210 let (mut lb, mut ub) = (false, false);
211 for i in 0..pmf_lin.len() {
212 acc += pmf_lin[i];
213 cdf[i] = acc;
214 if !lb && acc >= 0.005 {
215 lb = true;
216 lo = i;
217 }
218 if !ub && acc >= 0.995 {
219 ub = true;
220 hi = i;
221 }
222 }
223 (cdf, lo, hi)
224}
225
226#[inline]
231pub(crate) fn conditional_cdf(cdf: &[f64], cdf_max_arg: usize, cdf_max_val: f64, x: i32) -> f64 {
232 if x > cdf_max_arg as i32 {
233 1.0
234 } else if x <= 0 {
235 cdf[0] / cdf_max_val
236 } else {
237 cdf[x as usize] / cdf_max_val
238 }
239}
240
241pub fn build_expected<'a, F>(
247 num_refs: usize,
248 seq_of: F,
249 alphas: &[f64],
250 eff_lens: &[f64],
251 cdf: &[f64],
252) -> (SBModel, SBModel)
253where
254 F: Fn(usize) -> &'a [u8],
255{
256 let k = CONTEXT_LENGTH;
257 let cu = CONTEXT_LEFT as i32;
258 let mut exp_fw = SBModel::new();
259 let mut exp_rc = SBModel::new();
260 for tid in 0..num_refs {
261 if alphas[tid] < MIN_ALPHA || eff_lens[tid] <= 0.0 {
262 continue;
263 }
264 let seq = seq_of(tid);
265 let ref_len = seq.len();
266 if ref_len < k {
267 continue;
268 }
269 let cdf_max_arg = (cdf.len() - 1).min(ref_len);
270 let cdf_max_val = cdf[cdf_max_arg];
271 if cdf_max_val < MIN_CDF_MASS {
272 continue;
273 }
274 let weight = alphas[tid] / eff_lens[tid];
275 let rc = revcomp_bytes(seq);
276 for frag_start in 0..(ref_len - k) {
278 let max_frag_len = ref_len as i32 - (frag_start as i32 + cu);
279 if max_frag_len >= 0 && (max_frag_len as usize) < ref_len {
280 let cdensity = conditional_cdf(cdf, cdf_max_arg, cdf_max_val, max_frag_len);
281 let w = weight * cdensity;
282 exp_fw.add_context(&seq[frag_start..frag_start + k], false, w);
283 exp_rc.add_context(&rc[frag_start..frag_start + k], false, w);
284 }
285 }
286 }
287 exp_fw.normalize();
288 exp_rc.normalize();
289 (exp_fw, exp_rc)
290}
291
292#[allow(clippy::too_many_arguments)]
308pub fn corrected_effective_length(
309 seq: &[u8],
310 cdf: &[f64],
311 fld_low: usize,
312 fld_high: usize,
313 obs_fw: &SBModel,
314 exp_fw: &SBModel,
315 obs_rc: &SBModel,
316 exp_rc: &SBModel,
317 elen: f64,
318 stride: usize,
319) -> f64 {
320 let k = CONTEXT_LENGTH;
321 let cu = CONTEXT_LEFT; let ref_len = seq.len();
323 let unprocessed = (ref_len as i32 - elen as i32).max(0);
324 let cdf_max_arg = (cdf.len() - 1).min(ref_len);
325 let cdf_max_val = cdf[cdf_max_arg];
326 if ref_len < k || unprocessed <= 0 || cdf_max_val < MIN_CDF_MASS {
327 return elen;
328 }
329 let cond = |x: i32| conditional_cdf(cdf, cdf_max_arg, cdf_max_val, x);
330
331 let rc_seq = revcomp_bytes(seq);
333 let mut fw = vec![1.0f64; ref_len];
334 let mut rc = vec![1.0f64; ref_len];
335 for frag_start in 0..(ref_len - k) {
336 let read_start = frag_start + cu;
337 if read_start < ref_len {
338 fw[read_start] =
339 log_bias(obs_fw, exp_fw, &seq[frag_start..frag_start + k], false).exp();
340 rc[read_start] =
341 log_bias(obs_rc, exp_rc, &rc_seq[frag_start..frag_start + k], false).exp();
342 }
343 }
344 rc.reverse(); let stride = stride.max(1) as i32;
348 let max_len = (ref_len as i32).min(fld_high as i32 + 1);
349 let mut fl = fld_low as i32;
350 let mut done = fl >= max_len;
351 let sp = if fl > 0 { fl - 1 } else { 0 };
352 let mut prev_mass = cond(sp);
353 let mut eff = 0.0f64;
354 while !done {
355 if fl >= max_len {
356 done = true;
357 fl = max_len - 1;
358 }
359 let fl_weight = cond(fl) - prev_mass;
360 prev_mass = cond(fl);
361 let mut mass = 0.0f64;
362 let mut kstart = 0i32;
363 while kstart < ref_len as i32 - fl {
364 let frag_start = kstart as usize;
365 let frag_end = (kstart + fl - 1) as usize;
366 if frag_end < ref_len {
367 mass += fw[frag_start] * rc[frag_end];
368 } else {
369 break;
370 }
371 kstart += 1;
372 }
373 eff += fl_weight * mass;
374 fl += stride;
375 }
376
377 let offset = (unprocessed as f64).max(1.0);
379 eff.max(elen.min(offset))
380}
381
382pub fn log_bias(observed: &SBModel, expected: &SBModel, context: &[u8], rev_comp: bool) -> f64 {
386 observed.evaluate_log(context, rev_comp) - expected.evaluate_log(context, rev_comp)
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn uniform_contexts_give_near_zero_bias() {
395 let ctxs: Vec<Vec<u8>> = (0..256)
397 .map(|i| {
398 let bases = b"ACGT";
399 (0..CONTEXT_LENGTH)
400 .map(|p| bases[((i >> (p * 2)) & 3) as usize])
401 .collect()
402 })
403 .collect();
404 let mut obs = SBModel::new();
405 let mut exp = SBModel::new();
406 for c in &ctxs {
407 obs.add_context(c, false, 1.0);
408 exp.add_context(c, false, 1.0);
409 }
410 obs.normalize();
411 exp.normalize();
412 for c in &ctxs {
413 assert!(log_bias(&obs, &exp, c, false).abs() < 1e-9);
414 }
415 }
416
417 #[test]
418 fn enriched_context_has_positive_bias() {
419 let target: Vec<u8> = b"ACGTACGTA".to_vec();
421 let bases = b"ACGT";
422 let uniform: Vec<Vec<u8>> = (0..4096)
423 .map(|i| {
424 (0..CONTEXT_LENGTH)
425 .map(|p| bases[((i >> (p * 2)) & 3) as usize])
426 .collect()
427 })
428 .collect();
429
430 let mut exp = SBModel::new();
431 for c in &uniform {
432 exp.add_context(c, false, 1.0);
433 }
434 exp.normalize();
435
436 let mut obs = SBModel::new();
437 for c in &uniform {
438 obs.add_context(c, false, 1.0);
439 }
440 for _ in 0..5000 {
441 obs.add_context(&target, false, 1.0); }
443 obs.normalize();
444
445 assert!(
446 log_bias(&obs, &exp, &target, false) > 0.5,
447 "enriched context should have positive log-bias"
448 );
449 }
450
451 #[test]
452 fn unbiased_correction_reduces_to_standard_eff_len() {
453 let bases = b"ACGTACGTAGGCCTTAACCGGTTACGTACGT";
456 let seq: Vec<u8> = (0..400).map(|i| bases[i % bases.len()]).collect();
457 let mut m = SBModel::new();
458 let rc = revcomp_bytes(&seq);
459 for p in 0..=(seq.len() - CONTEXT_LENGTH) {
460 m.add_context(&seq[p..p + CONTEXT_LENGTH], false, 1.0);
461 m.add_context(&rc[p..p + CONTEXT_LENGTH], false, 1.0);
462 }
463 let mut obs = m.clone();
464 let mut exp = m.clone();
465 obs.normalize();
466 exp.normalize();
467
468 let mut pmf = vec![0.0; 200];
469 pmf[100] = 1.0;
470 let (cdf, lo, hi) = fld_cdf_and_bounds(&pmf);
471 let eff = corrected_effective_length(&seq, &cdf, lo, hi, &obs, &exp, &obs, &exp, 300.0, 1);
473 assert!((eff - 300.0).abs() < 1e-6, "got {eff}");
474 }
475
476 #[test]
477 fn revcomp_encoding_is_consistent() {
478 let ctx: Vec<u8> = b"ACGTACGTA".to_vec();
480 let rc: Vec<u8> = ctx
481 .iter()
482 .rev()
483 .map(|&b| match b {
484 b'A' => b'T',
485 b'C' => b'G',
486 b'G' => b'C',
487 b'T' => b'A',
488 x => x,
489 })
490 .collect();
491 assert_eq!(SBModel::encode(&ctx, true), SBModel::encode(&rc, false));
492 }
493}