refactor: remove equivalence class folding from entropy pipeline
Release / create-release (push) Successful in 2m27s
Release / build-linux-x86_64 (push) Successful in 8m17s
Release / build-macos-arm64 (push) Successful in 1m41s
CI / build (pull_request) Successful in 3m33s

Removes circular-reverse complement machinery and explicit k-mer canonicalization across the entropy pipeline. Frequency tallying and Shannon entropy computation now operate directly on raw k-mer values, eliminating prior score inflation and alignment-dependent artifacts while preserving orientation invariance. Updates build scripts to generate normalized lookup tables for k-mer lengths 1–6, restricts the public API to `EntropyTracker`, and bumps crate versions. Documentation is updated to reflect the simplified raw-value approach and revised module structure.
This commit is contained in:
Eric Coissac
2026-07-08 19:36:30 +02:00
parent 912f788f7f
commit fd2c23e7df
10 changed files with 142 additions and 286 deletions
+1 -1
View File
@@ -1711,7 +1711,7 @@ dependencies = [
[[package]]
name = "obikmer"
version = "1.1.38"
version = "1.1.39"
dependencies = [
"clap",
"csv",
+4 -59
View File
@@ -4,57 +4,6 @@ use std::path::PathBuf;
const K_MAX: usize = 32;
const WS_MAX: usize = 6;
fn normalize_circular(kmer: u64, ws: usize) -> u64 {
let mask = (1u64 << (ws * 2)) - 1;
let mut canonical = kmer & mask;
let mut current = canonical;
for _ in 0..ws - 1 {
let top = (current >> ((ws - 1) * 2)) & 3;
current = ((current << 2) | top) & mask;
if current < canonical {
canonical = current;
}
}
canonical
}
fn revcomp_raw(x: u64, k: usize) -> u64 {
let x = !x;
let x = x.swap_bytes();
let x = ((x >> 4) & 0x0F0F0F0F0F0F0F0F) | ((x & 0x0F0F0F0F0F0F0F0F) << 4);
let x = ((x >> 2) & 0x3333333333333333) | ((x & 0x3333333333333333) << 2);
x << (64 - 2 * k)
}
fn build_normalized_kmer(k: usize) -> Vec<u64> {
let n = 1usize << (k * 2);
let shift = 64 - k * 2;
let mut result = vec![0u64; n];
for i in 0..n {
let la = (i as u64) << shift;
let ra = i as u64;
let rc_ra = revcomp_raw(la, k) >> shift;
let circ = normalize_circular(ra, k);
let circ_rc = normalize_circular(rc_ra, k);
result[i] = if circ < circ_rc { circ } else { circ_rc };
}
result
}
fn build_ln_class(norm: &[u64]) -> Vec<f64> {
let n = norm.len();
let mut sizes = vec![0u32; n];
for &c in norm {
sizes[c as usize] += 1;
}
norm.iter()
.map(|&c| {
let s = sizes[c as usize];
if s > 0 { (s as f64).ln() } else { 0.0 }
})
.collect()
}
fn build_n_log_n() -> [f64; K_MAX + 1] {
let mut t = [0.0f64; K_MAX + 1];
for n in 1..=K_MAX {
@@ -63,6 +12,9 @@ fn build_n_log_n() -> [f64; K_MAX + 1] {
t
}
/// Max achievable entropy over `4^ws` raw sub-words given only `nwords`
/// observations (most-uniform integer partition), per
/// `docmd/theory/entropy.md`.
fn build_emax() -> [[f64; WS_MAX + 1]; K_MAX + 1] {
let mut t = [[0.0f64; WS_MAX + 1]; K_MAX + 1];
for k in 2..=K_MAX {
@@ -125,13 +77,6 @@ fn main() {
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let mut out = String::new();
for k in 1..=6usize {
let n = 1usize << (k * 2);
let norm = build_normalized_kmer(k);
let ln_class = build_ln_class(&norm);
emit_f64_1d(&mut out, &format!("LN_CLASS{k}"), n, &ln_class);
}
let n_log_n = build_n_log_n();
emit_f64_1d(&mut out, "N_LOG_N", K_MAX + 1, &n_log_n);
@@ -141,5 +86,5 @@ fn main() {
let log_nwords = build_log_nwords();
emit_f64_2d(&mut out, "LOG_NWORDS", K_MAX + 1, WS_MAX + 1, &log_nwords);
fs::write(out_dir.join("ln_class_tables.rs"), out).unwrap();
fs::write(out_dir.join("entropy_tables.rs"), out).unwrap();
}
+2 -2
View File
@@ -7,7 +7,7 @@
use obikseq::CanonicalKmer;
use crate::tracker::{EntropyTracker, sub_word_canon};
use crate::tracker::EntropyTracker;
/// Extension trait: compute the normalized entropy of a single canonical
/// k-mer, independent of any surrounding sequence.
@@ -30,7 +30,7 @@ impl KmerEntropy for CanonicalKmer {
let shift = 64 - 2 * (i + 1);
let base = (raw >> shift) & 3;
rolling = ((rolling << 2) | base) & mask;
tracker.push(i + 1, sub_word_canon(rolling));
tracker.push(i + 1, rolling);
}
tracker.normalized_entropy(level_max)
}
+1 -1
View File
@@ -14,4 +14,4 @@ mod table;
mod tracker;
pub use kmer_entropy::KmerEntropy;
pub use tracker::{EntropyTracker, SubWordCanon, sub_word_canon};
pub use tracker::EntropyTracker;
+12 -94
View File
@@ -1,68 +1,16 @@
//! Compile-time tables backing the normalized k-mer entropy formula:
//! circular-canonical sub-word classes, their log-sizes, and the max-entropy
//! correction for small samples. See `docmd/theory/entropy.md`.
//! Compile-time tables backing the normalized k-mer entropy formula: the
//! max-entropy correction for small samples. See `docmd/theory/entropy.md`.
//!
//! Entropy is computed directly on raw (non-canonicalized) sub-words — no
//! equivalence-class folding. Empirically (see the discussion that produced
//! this crate's history), folding sub-words into circular/revcomp classes
//! before unfolding them back buys nothing for the invariances it was meant
//! to guarantee (both hold for raw sub-word entropy already, by a direct
//! bijection argument for revcomp and by the sliding window's own dynamics
//! for tandem repeats), while it measurably *weakens* detection of the
//! low-complexity sequences the filter exists to catch.
pub(crate) const NORMK1: [u64; 4] = build_normalized_kmer::<4>();
pub(crate) const NORMK2: [u64; 16] = build_normalized_kmer::<16>();
pub(crate) const NORMK3: [u64; 64] = build_normalized_kmer::<64>();
pub(crate) const NORMK4: [u64; 256] = build_normalized_kmer::<256>();
pub(crate) const NORMK5: [u64; 1024] = build_normalized_kmer::<1024>();
pub(crate) const NORMK6: [u64; 4096] = build_normalized_kmer::<4096>();
include!(concat!(env!("OUT_DIR"), "/ln_class_tables.rs"));
const fn normalize_circular(kmer: u64, ws: usize) -> u64 {
let mask = (1u64 << (ws * 2)) - 1;
let mut canonical = kmer & mask;
let mut current = canonical;
let mut i = 0;
while i < (ws - 1) {
let top = (current >> ((ws - 1) * 2)) & 3;
current = ((current << 2) | top) & mask;
if current < canonical {
canonical = current;
}
i += 1;
}
canonical
}
const fn build_normalized_kmer<const N: usize>() -> [u64; N] {
let mut result = [0u64; N];
let k = k_from_n::<N>();
let shift = 64 - k * 2;
let mut i = 0;
while i < N {
let la = (i as u64) << shift;
let ra = i as u64;
let rc_ra = revcomp_raw(la, k) >> shift;
let circ = normalize_circular(ra, k);
let circ_rc = normalize_circular(rc_ra, k);
result[i] = if circ < circ_rc { circ } else { circ_rc };
i += 1;
}
result
}
const fn revcomp_raw(x: u64, k: usize) -> u64 {
let x = !x;
let x = x.swap_bytes();
let x = ((x >> 4) & 0x0F0F0F0F0F0F0F0F) | ((x & 0x0F0F0F0F0F0F0F0F) << 4);
let x = ((x >> 2) & 0x3333333333333333) | ((x & 0x3333333333333333) << 2);
x << (64 - 2 * k)
}
const fn k_from_n<const N: usize>() -> usize {
match N {
4 => 1,
16 => 2,
64 => 3,
256 => 4,
1024 => 5,
4096 => 6,
_ => panic!("N must be a power of 4"),
}
}
include!(concat!(env!("OUT_DIR"), "/entropy_tables.rs"));
pub(crate) const WS_MAX: usize = 6;
@@ -80,33 +28,3 @@ pub(crate) const fn emax(k: usize, ws: usize) -> f64 {
pub(crate) const fn log_nwords(k: usize, ws: usize) -> f64 {
LOG_NWORDS[k][ws]
}
#[inline(always)]
pub(crate) const fn entropy_norm_kmer<const LEFT: bool, const K: usize>(kmer: u64) -> u64 {
const SHIFT: [usize; 7] = [0, 62, 60, 58, 56, 54, 52];
const NORM: [&[u64]; 7] = [&[], &NORMK1, &NORMK2, &NORMK3, &NORMK4, &NORMK5, &NORMK6];
let shift = SHIFT[K];
let ra = if LEFT { kmer >> shift } else { kmer };
let canonical_ra = NORM[K][ra as usize];
if LEFT {
canonical_ra << shift
} else {
canonical_ra
}
}
#[inline(always)]
pub(crate) const fn ln_class_size<const LEFT: bool, const K: usize>(kmer: u64) -> f64 {
const SHIFT: [usize; 7] = [0, 62, 60, 58, 56, 54, 52];
let ra = if LEFT { kmer >> SHIFT[K] } else { kmer };
match K {
1 => LN_CLASS1[ra as usize],
2 => LN_CLASS2[ra as usize],
3 => LN_CLASS3[ra as usize],
4 => LN_CLASS4[ra as usize],
5 => LN_CLASS5[ra as usize],
6 => LN_CLASS6[ra as usize],
_ => panic!("k must be 1..=6"),
}
}
+80 -105
View File
@@ -1,9 +1,12 @@
//! Incremental (streaming) normalized k-mer entropy.
//!
//! [`EntropyTracker`] maintains, over a sliding window of the last `k` bases,
//! the per-sub-word-size 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.
//! 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.
//!
//! It carries no notion of minimizers or superkmer segmentation — callers
//! that need both (e.g. `obiskbuilder::RollingStat`) compose an
@@ -12,25 +15,7 @@
//! struct.
use crate::ring::Ring;
use crate::table::{WS_MAX, emax, entropy_norm_kmer, ln_class_size, log_nwords, n_log_n};
/// Canonical sub-word values for word sizes 1..=6, computed by the caller
/// from its own rolling k-mer bits (see [`EntropyTracker::push`]).
pub type SubWordCanon = [u64; WS_MAX];
/// Compute the six canonical sub-word values (word sizes 1..=6) for the
/// current rolling right-aligned k-mer window.
#[inline]
pub fn sub_word_canon(rolling_kmer: u64) -> SubWordCanon {
[
entropy_norm_kmer::<false, 1>(rolling_kmer & 3),
entropy_norm_kmer::<false, 2>(rolling_kmer & 15),
entropy_norm_kmer::<false, 3>(rolling_kmer & 63),
entropy_norm_kmer::<false, 4>(rolling_kmer & 255),
entropy_norm_kmer::<false, 5>(rolling_kmer & 1023),
entropy_norm_kmer::<false, 6>(rolling_kmer & 4095),
]
}
use crate::table::{WS_MAX, emax, log_nwords, n_log_n};
/// Incremental normalized-entropy accumulator over a sliding window of `k`
/// bases. Composed as a plain field by callers that also need other
@@ -39,8 +24,8 @@ pub struct EntropyTracker {
k: usize,
steady: bool,
// Sliding-window queues over the last `k` canonical sub-words, one per
// word size — stack-allocated, capacity ≤ k ≤ 31.
// Sliding-window queues over the last `k` raw sub-words, one per word
// size — stack-allocated, capacity ≤ k ≤ 31.
k1q: Ring<u64, 32>,
k2q: Ring<u64, 32>,
k3q: Ring<u64, 32>,
@@ -48,7 +33,8 @@ pub struct EntropyTracker {
k5q: Ring<u64, 32>,
k6q: Ring<u64, 32>,
// Frequency count arrays. Max count per cell ≤ k ≤ 31 → u8 is sufficient.
// Frequency count arrays, indexed by the raw sub-word value (2 bits per
// base). Max count per cell ≤ k ≤ 31 → u8 is sufficient.
k1c: [u8; 4],
k2c: [u8; 16],
k3c: [u8; 64],
@@ -57,7 +43,6 @@ pub struct EntropyTracker {
k6c: [u8; 4096],
sum_f_log_f: [f64; WS_MAX + 1],
sum_f_log_s: [f64; WS_MAX + 1],
}
impl EntropyTracker {
@@ -79,7 +64,6 @@ impl EntropyTracker {
k5c: [0; 1024],
k6c: [0; 4096],
sum_f_log_f: [0.0; WS_MAX + 1],
sum_f_log_s: [0.0; WS_MAX + 1],
}
}
@@ -103,103 +87,94 @@ impl EntropyTracker {
self.k6q.clear();
self.sum_f_log_f = [0.0; WS_MAX + 1];
self.sum_f_log_s = [0.0; WS_MAX + 1];
}
#[inline]
fn update_sums_decrement<const K: usize>(
sum_f_log_f: &mut [f64; WS_MAX + 1],
sum_f_log_s: &mut [f64; WS_MAX + 1],
canonical: u64,
f: usize,
) {
fn update_sums_decrement<const K: usize>(sum_f_log_f: &mut [f64; WS_MAX + 1], f: usize) {
sum_f_log_f[K] += n_log_n(f - 1) - n_log_n(f);
sum_f_log_s[K] -= ln_class_size::<false, K>(canonical);
}
#[inline]
fn update_sums_increment<const K: usize>(
sum_f_log_f: &mut [f64; WS_MAX + 1],
sum_f_log_s: &mut [f64; WS_MAX + 1],
canonical: u64,
g: usize,
) {
fn update_sums_increment<const K: usize>(sum_f_log_f: &mut [f64; WS_MAX + 1], g: usize) {
sum_f_log_f[K] += n_log_n(g + 1) - n_log_n(g);
sum_f_log_s[K] += ln_class_size::<false, K>(canonical);
}
/// 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);
/// `canon` are the six canonical sub-word values for the current
/// rolling k-mer, from [`sub_word_canon`].
pub fn push(&mut self, received: usize, canon: SubWordCanon) {
let [canonical_k1, canonical_k2, canonical_k3, canonical_k4, canonical_k5, canonical_k6] =
canon;
/// `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;
if received > self.k {
let old1 = self.k1q.pop_front();
let f1 = self.k1c[old1 as usize] as usize;
Self::update_sums_decrement::<1>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, old1, f1);
Self::update_sums_decrement::<1>(&mut self.sum_f_log_f, f1);
self.k1c[old1 as usize] -= 1;
let old2 = self.k2q.pop_front();
let f2 = self.k2c[old2 as usize] as usize;
Self::update_sums_decrement::<2>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, old2, f2);
Self::update_sums_decrement::<2>(&mut self.sum_f_log_f, f2);
self.k2c[old2 as usize] -= 1;
let old3 = self.k3q.pop_front();
let f3 = self.k3c[old3 as usize] as usize;
Self::update_sums_decrement::<3>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, old3, f3);
Self::update_sums_decrement::<3>(&mut self.sum_f_log_f, f3);
self.k3c[old3 as usize] -= 1;
let old4 = self.k4q.pop_front();
let f4 = self.k4c[old4 as usize] as usize;
Self::update_sums_decrement::<4>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, old4, f4);
Self::update_sums_decrement::<4>(&mut self.sum_f_log_f, f4);
self.k4c[old4 as usize] -= 1;
let old5 = self.k5q.pop_front();
let f5 = self.k5c[old5 as usize] as usize;
Self::update_sums_decrement::<5>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, old5, f5);
Self::update_sums_decrement::<5>(&mut self.sum_f_log_f, f5);
self.k5c[old5 as usize] -= 1;
let old6 = self.k6q.pop_front();
let f6 = self.k6c[old6 as usize] as usize;
Self::update_sums_decrement::<6>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, old6, f6);
Self::update_sums_decrement::<6>(&mut self.sum_f_log_f, f6);
self.k6c[old6 as usize] -= 1;
}
if self.steady {
let g1 = self.k1c[canonical_k1 as usize] as usize;
Self::update_sums_increment::<1>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k1, g1);
self.k1c[canonical_k1 as usize] += 1;
self.k1q.push_back(canonical_k1);
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);
let g2 = self.k2c[canonical_k2 as usize] as usize;
Self::update_sums_increment::<2>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k2, g2);
self.k2c[canonical_k2 as usize] += 1;
self.k2q.push_back(canonical_k2);
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);
let g3 = self.k3c[canonical_k3 as usize] as usize;
Self::update_sums_increment::<3>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k3, g3);
self.k3c[canonical_k3 as usize] += 1;
self.k3q.push_back(canonical_k3);
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);
let g4 = self.k4c[canonical_k4 as usize] as usize;
Self::update_sums_increment::<4>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k4, g4);
self.k4c[canonical_k4 as usize] += 1;
self.k4q.push_back(canonical_k4);
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);
let g5 = self.k5c[canonical_k5 as usize] as usize;
Self::update_sums_increment::<5>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k5, g5);
self.k5c[canonical_k5 as usize] += 1;
self.k5q.push_back(canonical_k5);
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);
let g6 = self.k6c[canonical_k6 as usize] as usize;
Self::update_sums_increment::<6>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k6, g6);
self.k6c[canonical_k6 as usize] += 1;
self.k6q.push_back(canonical_k6);
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);
} else {
self.push_warmup_increments(received, canonical_k1, canonical_k2, canonical_k3, canonical_k4, canonical_k5, canonical_k6);
self.push_warmup_increments(received, raw1, raw2, raw3, raw4, raw5, raw6);
}
}
@@ -208,43 +183,43 @@ impl EntropyTracker {
fn push_warmup_increments(
&mut self,
received: usize,
canonical_k1: u64, canonical_k2: u64, canonical_k3: u64,
canonical_k4: u64, canonical_k5: u64, canonical_k6: u64,
raw1: u64, raw2: u64, raw3: u64,
raw4: u64, raw5: u64, raw6: u64,
) {
let g1 = self.k1c[canonical_k1 as usize] as usize;
Self::update_sums_increment::<1>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k1, g1);
self.k1c[canonical_k1 as usize] += 1;
self.k1q.push_back(canonical_k1);
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);
if received >= 2 {
let g2 = self.k2c[canonical_k2 as usize] as usize;
Self::update_sums_increment::<2>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k2, g2);
self.k2c[canonical_k2 as usize] += 1;
self.k2q.push_back(canonical_k2);
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);
if received >= 3 {
let g3 = self.k3c[canonical_k3 as usize] as usize;
Self::update_sums_increment::<3>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k3, g3);
self.k3c[canonical_k3 as usize] += 1;
self.k3q.push_back(canonical_k3);
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);
if received >= 4 {
let g4 = self.k4c[canonical_k4 as usize] as usize;
Self::update_sums_increment::<4>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k4, g4);
self.k4c[canonical_k4 as usize] += 1;
self.k4q.push_back(canonical_k4);
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);
if received >= 5 {
let g5 = self.k5c[canonical_k5 as usize] as usize;
Self::update_sums_increment::<5>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k5, g5);
self.k5c[canonical_k5 as usize] += 1;
self.k5q.push_back(canonical_k5);
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);
if received >= 6 {
let g6 = self.k6c[canonical_k6 as usize] as usize;
Self::update_sums_increment::<6>(&mut self.sum_f_log_f, &mut self.sum_f_log_s, canonical_k6, g6);
self.k6c[canonical_k6 as usize] += 1;
self.k6q.push_back(canonical_k6);
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);
self.steady = true;
}
}
@@ -265,7 +240,7 @@ impl EntropyTracker {
let nwords = k - order + 1;
let log_nw = log_nwords(k, order);
let nw_f = nwords as f64;
let h_corr = log_nw + (self.sum_f_log_s[order] - self.sum_f_log_f[order]) / nw_f;
let h_corr = log_nw - self.sum_f_log_f[order] / nw_f;
(h_corr / em).max(0.0)
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obikmer"
version = "1.1.38"
version = "1.1.39"
edition = "2024"
[[bin]]
+2 -4
View File
@@ -1,4 +1,4 @@
use obikentropy::{EntropyTracker, sub_word_canon};
use obikentropy::EntropyTracker;
use obikseq::kmer::{Minimizer, hash_kmer};
use obikseq::params;
@@ -136,8 +136,6 @@ impl RollingStat {
self.rolling_rck =
((self.rolling_rck >> 2) | ((cnuc as u64) << ((k - 1) * 2))) & self.k_mask;
let canon = sub_word_canon(self.rolling_k);
self.received += 1;
if self.received >= m {
@@ -170,7 +168,7 @@ impl RollingStat {
}
}
self.entropy.push(self.received, canon);
self.entropy.push(self.received, self.rolling_k);
}
pub fn ready(&self) -> bool {