use crate::{
Result, StrobeError,
constants::DEFAULT_PRIME_NUMBER,
hashes::{KmerHasher, MinWindow, NtHash64, check_hash_count, compute_min_hashes},
util::{last_start_index, roundup64},
};
const MAX_MINIMA_TABLE_BYTES: usize = 8 * 1024 * 1024;
const MINIMA_BYTES_PER_KMER: usize = size_of::<usize>() + size_of::<u64>();
#[derive(Debug, Clone)]
enum Minima {
Table { loc: Vec<usize>, val: Vec<u64> },
Online(MinWindow),
}
impl Minima {
fn new(hashes: &[u64], window: usize, lag: usize) -> Self {
if hashes.len().saturating_mul(MINIMA_BYTES_PER_KMER) <= MAX_MINIMA_TABLE_BYTES {
let (loc, val) = compute_min_hashes(hashes, window);
Self::Table { loc, val }
} else {
Self::Online(MinWindow::new(window, lag))
}
}
#[inline]
fn at(&mut self, hashes: &[u64], p: usize) -> (usize, u64) {
match self {
Self::Table { loc, val } => (loc[p], val[p]),
Self::Online(win) => win.advance_to(hashes, p),
}
}
}
#[derive(Debug, Clone)]
pub struct MinStrobes {
n: u8, w_min: usize, w_max: usize,
hashes: Vec<u64>,
minima: Minima,
idx: usize, end_idx: Option<usize>,
end_hash: usize,
idx2: usize, idx3: usize,
prime: u64, shrink: bool,
h1: u64, h2: u64, h3: u64, }
impl MinStrobes {
pub fn new(seq: &[u8], n: u8, k: usize, w_min: usize, w_max: usize) -> Result<Self> {
Self::with_hasher(seq, n, k, w_min, w_max, &NtHash64)
}
pub fn with_hasher<H>(
seq: &[u8],
n: u8,
k: usize,
w_min: usize,
w_max: usize,
hasher: &H,
) -> Result<Self>
where
H: KmerHasher,
{
validate_params!(seq, n, k, w_min, w_max);
let hashes = hasher.hash_all(seq, k)?;
check_hash_count(&hashes, seq.len(), k)?;
let window = w_max - w_min + 1;
let minima = Minima::new(&hashes, window, if n == 3 { w_max } else { 0 });
let seq_len = seq.len();
let end_hash = seq_len - k;
let end_idx = seq_len.checked_sub(k + (n as usize - 1) * k);
Ok(Self {
n,
w_min,
w_max,
hashes,
minima,
idx: 0,
end_hash,
end_idx,
idx2: 0,
idx3: 0,
prime: DEFAULT_PRIME_NUMBER,
shrink: true,
h1: 0,
h2: 0,
h3: 0,
})
}
pub fn set_prime(&mut self, q: u64) -> Result<()> {
if q < 256 {
return Err(StrobeError::PrimeNumberTooSmall);
}
self.prime = roundup64(q).wrapping_sub(1);
Ok(())
}
pub fn set_window_shrink(&mut self, s: bool) {
self.shrink = s;
}
pub fn index(&self) -> Option<usize> {
self.idx.checked_sub(1)
}
pub fn indexes(&self) -> [usize; 3] {
[self.index().unwrap_or(0), self.idx2, self.idx3]
}
#[inline]
fn remaining(&self) -> usize {
let (Some(end_idx), Some(limit)) = (
self.end_idx,
last_start_index(self.n, self.shrink, self.w_min, self.w_max, self.end_hash),
) else {
return 0;
};
(limit.min(end_idx) + 1).saturating_sub(self.idx)
}
fn next_order2(&mut self) -> Option<u64> {
if self.idx > self.end_idx? {
return None;
}
let w_start = self.idx + self.w_min;
let mut w_end = self.idx + self.w_max;
self.h1 = self.hashes[self.idx];
if w_end > self.end_hash {
if !self.shrink {
return None;
}
w_end = self.end_hash;
}
if w_end == self.idx + self.w_max {
let (loc, val) = self.minima.at(&self.hashes, w_end);
self.idx2 = loc;
self.h2 = (self.h1 >> 1) + val / 3;
} else {
if w_start > w_end {
return None;
}
let (mut best_hash, mut best_pos) = (u64::MAX, w_start);
for pos in w_start..=w_end {
let cand = self.hashes[pos];
if cand < best_hash {
best_hash = cand;
best_pos = pos;
}
}
self.idx2 = best_pos;
self.h2 = self.h1 / 2 + best_hash / 3;
}
self.idx += 1;
Some(self.h2)
}
fn next_order3(&mut self) -> Option<u64> {
if self.idx > self.end_idx? {
return None;
}
let w_end = self.idx + self.w_max;
let w2_start = self.idx + self.w_max + self.w_min;
let mut w2_end = self.idx + (self.w_max << 1);
if w2_start > self.end_hash {
return None;
}
if w2_end > self.end_hash {
if !self.shrink {
return None;
}
w2_end = self.end_hash;
}
self.h1 = self.hashes[self.idx];
let (loc2, val2) = self.minima.at(&self.hashes, w_end);
self.idx2 = loc2;
self.h2 = self.h1 / 3 + (val2 >> 2);
if w2_end == self.idx + (self.w_max << 1) {
let (loc3, val3) = self.minima.at(&self.hashes, w2_end);
self.idx3 = loc3;
self.h3 = self.h2 + val3 / 5;
} else {
let (mut best_hash, mut best_pos) = (u64::MAX, w2_start);
for pos in w2_start..=w2_end {
let cand = self.h2.wrapping_add(self.hashes[pos]) & self.prime;
if cand < best_hash {
best_hash = cand;
best_pos = pos;
}
}
self.idx3 = best_pos;
self.h3 = self.h2 + self.hashes[self.idx3] / 5;
}
self.idx += 1;
Some(self.h3)
}
}
impl Iterator for MinStrobes {
type Item = u64;
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let n = self.remaining();
(n, Some(n))
}
fn next(&mut self) -> Option<Self::Item> {
match self.n {
2 => self.next_order2(),
3 => self.next_order3(),
_ => None, }
}
}
impl ExactSizeIterator for MinStrobes {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn order2_basic() {
let mut ms = MinStrobes::new("ACGTACGTACGT".as_bytes(), 2, 3, 1, 4).unwrap();
assert!(ms.next().is_some());
}
fn long_seq(n: usize) -> Vec<u8> {
let mut state: u64 = 0x2545F491_4F6CDD1D;
const BASES: [u8; 4] = *b"ACGT";
(0..n)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
BASES[(state >> 33) as usize & 3]
})
.collect()
}
#[test]
fn minima_strategies_agree() {
let (k, w_min, w_max) = (21usize, 10usize, 25usize);
let seq = long_seq(MAX_MINIMA_TABLE_BYTES / MINIMA_BYTES_PER_KMER + k + 1);
for n in [2u8, 3] {
let mut online = MinStrobes::new(&seq, n, k, w_min, w_max).unwrap();
assert!(
matches!(online.minima, Minima::Online(_)),
"expected the online strategy for a {}-base sequence",
seq.len()
);
let mut table = MinStrobes::new(&seq, n, k, w_min, w_max).unwrap();
let (loc, val) = compute_min_hashes(&table.hashes, w_max - w_min + 1);
table.minima = Minima::Table { loc, val };
let mut a = Vec::new();
while let Some(h) = online.next() {
a.push((online.indexes(), h));
}
let mut b = Vec::new();
while let Some(h) = table.next() {
b.push((table.indexes(), h));
}
assert!(!a.is_empty(), "expected output for order {n}");
assert_eq!(a, b, "strategies disagree for order {n}");
}
}
#[test]
fn strategy_switches_on_table_size() {
let cutoff = MAX_MINIMA_TABLE_BYTES / MINIMA_BYTES_PER_KMER;
let small = vec![0u64; cutoff];
let large = vec![0u64; cutoff + 1];
assert!(matches!(Minima::new(&small, 4, 0), Minima::Table { .. }));
assert!(matches!(Minima::new(&large, 4, 0), Minima::Online(_)));
}
#[test]
fn order3_basic() {
let seq = "ACGTACGTACGTACGTACGTACGT";
let ms = MinStrobes::new(seq.as_bytes(), 3, 3, 1, 4).unwrap();
assert_eq!(ms.take(10).count(), 10);
}
}