2026-07-08 18:16:08 +02:00
|
|
|
//! Incremental (streaming) normalized k-mer entropy.
|
|
|
|
|
//!
|
|
|
|
|
//! [`EntropyTracker`] maintains, over a sliding window of the last `k` bases,
|
2026-07-08 19:26:33 +02:00
|
|
|
//! the per-sub-word-size raw-word frequency statistics needed to evaluate
|
|
|
|
|
//! the corrected Shannon entropy described in `docmd/theory/entropy.md`,
|
|
|
|
|
//! updated in O(1) per base rather than recomputed from scratch. No
|
|
|
|
|
//! canonicalization is applied — each sub-word is tallied under its own raw
|
|
|
|
|
//! 2-bit-packed value; only the small-sample max-entropy correction departs
|
|
|
|
|
//! from a textbook Shannon entropy.
|
2026-07-08 18:16:08 +02:00
|
|
|
//!
|
|
|
|
|
//! It carries no notion of minimizers or superkmer segmentation — callers
|
|
|
|
|
//! that need both (e.g. `obiskbuilder::RollingStat`) compose an
|
|
|
|
|
//! `EntropyTracker` as a plain field alongside their own state, so the two
|
|
|
|
|
//! concerns update in the same streaming pass without being conflated in one
|
|
|
|
|
//! struct.
|
|
|
|
|
|
|
|
|
|
use crate::ring::Ring;
|
2026-07-08 19:26:33 +02:00
|
|
|
use crate::table::{WS_MAX, emax, log_nwords, n_log_n};
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
/// Incremental normalized-entropy accumulator over a sliding window of `k`
|
|
|
|
|
/// bases. Composed as a plain field by callers that also need other
|
|
|
|
|
/// per-base state (e.g. minimizer selection) in the same streaming pass.
|
|
|
|
|
pub struct EntropyTracker {
|
|
|
|
|
k: usize,
|
|
|
|
|
steady: bool,
|
|
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
// Sliding-window queues over the last `k` raw sub-words, one per word
|
|
|
|
|
// size — stack-allocated, capacity ≤ k ≤ 31.
|
2026-07-08 18:16:08 +02:00
|
|
|
k1q: Ring<u64, 32>,
|
|
|
|
|
k2q: Ring<u64, 32>,
|
|
|
|
|
k3q: Ring<u64, 32>,
|
|
|
|
|
k4q: Ring<u64, 32>,
|
|
|
|
|
k5q: Ring<u64, 32>,
|
|
|
|
|
k6q: Ring<u64, 32>,
|
|
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
// Frequency count arrays, indexed by the raw sub-word value (2 bits per
|
|
|
|
|
// base). Max count per cell ≤ k ≤ 31 → u8 is sufficient.
|
2026-07-08 18:16:08 +02:00
|
|
|
k1c: [u8; 4],
|
|
|
|
|
k2c: [u8; 16],
|
|
|
|
|
k3c: [u8; 64],
|
|
|
|
|
k4c: [u8; 256],
|
|
|
|
|
k5c: [u8; 1024],
|
|
|
|
|
k6c: [u8; 4096],
|
|
|
|
|
|
|
|
|
|
sum_f_log_f: [f64; WS_MAX + 1],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EntropyTracker {
|
|
|
|
|
/// New tracker for a window of `k` bases (1..=31).
|
|
|
|
|
pub fn new(k: usize) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
k,
|
|
|
|
|
steady: false,
|
|
|
|
|
k1q: Ring::new(),
|
|
|
|
|
k2q: Ring::new(),
|
|
|
|
|
k3q: Ring::new(),
|
|
|
|
|
k4q: Ring::new(),
|
|
|
|
|
k5q: Ring::new(),
|
|
|
|
|
k6q: Ring::new(),
|
|
|
|
|
k1c: [0; 4],
|
|
|
|
|
k2c: [0; 16],
|
|
|
|
|
k3c: [0; 64],
|
|
|
|
|
k4c: [0; 256],
|
|
|
|
|
k5c: [0; 1024],
|
|
|
|
|
k6c: [0; 4096],
|
|
|
|
|
sum_f_log_f: [0.0; WS_MAX + 1],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Clear all accumulated state, ready to track a new window from
|
|
|
|
|
/// scratch (`k` is unchanged).
|
|
|
|
|
pub fn reset(&mut self) {
|
|
|
|
|
self.steady = false;
|
|
|
|
|
|
|
|
|
|
self.k1c.fill(0);
|
|
|
|
|
self.k2c.fill(0);
|
|
|
|
|
self.k3c.fill(0);
|
|
|
|
|
self.k4c.fill(0);
|
|
|
|
|
self.k5c.fill(0);
|
|
|
|
|
self.k6c.fill(0);
|
|
|
|
|
|
|
|
|
|
self.k1q.clear();
|
|
|
|
|
self.k2q.clear();
|
|
|
|
|
self.k3q.clear();
|
|
|
|
|
self.k4q.clear();
|
|
|
|
|
self.k5q.clear();
|
|
|
|
|
self.k6q.clear();
|
|
|
|
|
|
|
|
|
|
self.sum_f_log_f = [0.0; WS_MAX + 1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[inline]
|
2026-07-08 19:26:33 +02:00
|
|
|
fn update_sums_decrement<const K: usize>(sum_f_log_f: &mut [f64; WS_MAX + 1], f: usize) {
|
2026-07-08 18:16:08 +02:00
|
|
|
sum_f_log_f[K] += n_log_n(f - 1) - n_log_n(f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[inline]
|
2026-07-08 19:26:33 +02:00
|
|
|
fn update_sums_increment<const K: usize>(sum_f_log_f: &mut [f64; WS_MAX + 1], g: usize) {
|
2026-07-08 18:16:08 +02:00
|
|
|
sum_f_log_f[K] += n_log_n(g + 1) - n_log_n(g);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Advance the window by one base. `received` is the caller's running
|
|
|
|
|
/// count of bases pushed so far (1-based, i.e. after this base);
|
2026-07-08 19:26:33 +02:00
|
|
|
/// `rolling_kmer` is the current right-aligned, 2-bit-packed k-mer
|
|
|
|
|
/// window (same convention as `obiskbuilder::RollingStat::rolling_k`).
|
|
|
|
|
pub fn push(&mut self, received: usize, rolling_kmer: u64) {
|
|
|
|
|
let raw1 = rolling_kmer & 3;
|
|
|
|
|
let raw2 = rolling_kmer & 15;
|
|
|
|
|
let raw3 = rolling_kmer & 63;
|
|
|
|
|
let raw4 = rolling_kmer & 255;
|
|
|
|
|
let raw5 = rolling_kmer & 1023;
|
|
|
|
|
let raw6 = rolling_kmer & 4095;
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
if received > self.k {
|
|
|
|
|
let old1 = self.k1q.pop_front();
|
|
|
|
|
let f1 = self.k1c[old1 as usize] as usize;
|
2026-07-08 19:26:33 +02:00
|
|
|
Self::update_sums_decrement::<1>(&mut self.sum_f_log_f, f1);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.k1c[old1 as usize] -= 1;
|
|
|
|
|
|
|
|
|
|
let old2 = self.k2q.pop_front();
|
|
|
|
|
let f2 = self.k2c[old2 as usize] as usize;
|
2026-07-08 19:26:33 +02:00
|
|
|
Self::update_sums_decrement::<2>(&mut self.sum_f_log_f, f2);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.k2c[old2 as usize] -= 1;
|
|
|
|
|
|
|
|
|
|
let old3 = self.k3q.pop_front();
|
|
|
|
|
let f3 = self.k3c[old3 as usize] as usize;
|
2026-07-08 19:26:33 +02:00
|
|
|
Self::update_sums_decrement::<3>(&mut self.sum_f_log_f, f3);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.k3c[old3 as usize] -= 1;
|
|
|
|
|
|
|
|
|
|
let old4 = self.k4q.pop_front();
|
|
|
|
|
let f4 = self.k4c[old4 as usize] as usize;
|
2026-07-08 19:26:33 +02:00
|
|
|
Self::update_sums_decrement::<4>(&mut self.sum_f_log_f, f4);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.k4c[old4 as usize] -= 1;
|
|
|
|
|
|
|
|
|
|
let old5 = self.k5q.pop_front();
|
|
|
|
|
let f5 = self.k5c[old5 as usize] as usize;
|
2026-07-08 19:26:33 +02:00
|
|
|
Self::update_sums_decrement::<5>(&mut self.sum_f_log_f, f5);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.k5c[old5 as usize] -= 1;
|
|
|
|
|
|
|
|
|
|
let old6 = self.k6q.pop_front();
|
|
|
|
|
let f6 = self.k6c[old6 as usize] as usize;
|
2026-07-08 19:26:33 +02:00
|
|
|
Self::update_sums_decrement::<6>(&mut self.sum_f_log_f, f6);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.k6c[old6 as usize] -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.steady {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g1 = self.k1c[raw1 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<1>(&mut self.sum_f_log_f, g1);
|
|
|
|
|
self.k1c[raw1 as usize] += 1;
|
|
|
|
|
self.k1q.push_back(raw1);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
let g2 = self.k2c[raw2 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<2>(&mut self.sum_f_log_f, g2);
|
|
|
|
|
self.k2c[raw2 as usize] += 1;
|
|
|
|
|
self.k2q.push_back(raw2);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
let g3 = self.k3c[raw3 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<3>(&mut self.sum_f_log_f, g3);
|
|
|
|
|
self.k3c[raw3 as usize] += 1;
|
|
|
|
|
self.k3q.push_back(raw3);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
let g4 = self.k4c[raw4 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<4>(&mut self.sum_f_log_f, g4);
|
|
|
|
|
self.k4c[raw4 as usize] += 1;
|
|
|
|
|
self.k4q.push_back(raw4);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
let g5 = self.k5c[raw5 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<5>(&mut self.sum_f_log_f, g5);
|
|
|
|
|
self.k5c[raw5 as usize] += 1;
|
|
|
|
|
self.k5q.push_back(raw5);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
2026-07-08 19:26:33 +02:00
|
|
|
let g6 = self.k6c[raw6 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<6>(&mut self.sum_f_log_f, g6);
|
|
|
|
|
self.k6c[raw6 as usize] += 1;
|
|
|
|
|
self.k6q.push_back(raw6);
|
2026-07-08 18:16:08 +02:00
|
|
|
} else {
|
2026-07-08 19:26:33 +02:00
|
|
|
self.push_warmup_increments(received, raw1, raw2, raw3, raw4, raw5, raw6);
|
2026-07-08 18:16:08 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cold]
|
|
|
|
|
#[inline(never)]
|
|
|
|
|
fn push_warmup_increments(
|
|
|
|
|
&mut self,
|
|
|
|
|
received: usize,
|
2026-07-08 19:26:33 +02:00
|
|
|
raw1: u64, raw2: u64, raw3: u64,
|
|
|
|
|
raw4: u64, raw5: u64, raw6: u64,
|
2026-07-08 18:16:08 +02:00
|
|
|
) {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g1 = self.k1c[raw1 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<1>(&mut self.sum_f_log_f, g1);
|
|
|
|
|
self.k1c[raw1 as usize] += 1;
|
|
|
|
|
self.k1q.push_back(raw1);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
if received >= 2 {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g2 = self.k2c[raw2 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<2>(&mut self.sum_f_log_f, g2);
|
|
|
|
|
self.k2c[raw2 as usize] += 1;
|
|
|
|
|
self.k2q.push_back(raw2);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
if received >= 3 {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g3 = self.k3c[raw3 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<3>(&mut self.sum_f_log_f, g3);
|
|
|
|
|
self.k3c[raw3 as usize] += 1;
|
|
|
|
|
self.k3q.push_back(raw3);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
if received >= 4 {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g4 = self.k4c[raw4 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<4>(&mut self.sum_f_log_f, g4);
|
|
|
|
|
self.k4c[raw4 as usize] += 1;
|
|
|
|
|
self.k4q.push_back(raw4);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
if received >= 5 {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g5 = self.k5c[raw5 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<5>(&mut self.sum_f_log_f, g5);
|
|
|
|
|
self.k5c[raw5 as usize] += 1;
|
|
|
|
|
self.k5q.push_back(raw5);
|
2026-07-08 18:16:08 +02:00
|
|
|
|
|
|
|
|
if received >= 6 {
|
2026-07-08 19:26:33 +02:00
|
|
|
let g6 = self.k6c[raw6 as usize] as usize;
|
|
|
|
|
Self::update_sums_increment::<6>(&mut self.sum_f_log_f, g6);
|
|
|
|
|
self.k6c[raw6 as usize] += 1;
|
|
|
|
|
self.k6q.push_back(raw6);
|
2026-07-08 18:16:08 +02:00
|
|
|
self.steady = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Normalized entropy at sub-word size `order` (1..=6). The caller is
|
|
|
|
|
/// responsible for not calling this before the window is full (`k`
|
|
|
|
|
/// bases pushed) — an empty/partial window yields a meaningless value.
|
|
|
|
|
pub fn entropy(&self, order: usize) -> f64 {
|
|
|
|
|
let k = self.k;
|
|
|
|
|
let em = emax(k, order);
|
|
|
|
|
if em <= 0.0 {
|
|
|
|
|
return 1.0;
|
|
|
|
|
}
|
|
|
|
|
let nwords = k - order + 1;
|
|
|
|
|
let log_nw = log_nwords(k, order);
|
|
|
|
|
let nw_f = nwords as f64;
|
2026-07-08 19:26:33 +02:00
|
|
|
let h_corr = log_nw - self.sum_f_log_f[order] / nw_f;
|
2026-07-08 18:16:08 +02:00
|
|
|
(h_corr / em).max(0.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Minimum of [`Self::entropy`] over sub-word sizes `1..=order_max`, same
|
|
|
|
|
/// caller responsibility re: window readiness as `entropy`.
|
|
|
|
|
pub fn normalized_entropy(&self, order_max: usize) -> f64 {
|
|
|
|
|
let min_e = (1..=order_max)
|
|
|
|
|
.map(|ws| self.entropy(ws))
|
|
|
|
|
.fold(f64::MAX, f64::min);
|
|
|
|
|
if min_e == f64::MAX { 1.0 } else { min_e }
|
|
|
|
|
}
|
|
|
|
|
}
|