use super::cover::{Epochs, compute_epochs};
use super::samples::{SampleSet, TrainingError, refuse};
use alloc::vec::Vec;
use std::io;
const PRIME_4_BYTES: u32 = 2_654_435_761;
const PRIME_5_BYTES: u64 = 889_523_592_379;
const PRIME_6_BYTES: u64 = 227_718_039_650_203;
const PRIME_7_BYTES: u64 = 58_295_818_150_454_627;
const PRIME_8_BYTES: u64 = 0xCF1B_BCDC_B7A5_6463;
pub(super) const MAX_F: u32 = 31;
pub(super) const MAX_ACCEL: u32 = 10;
#[inline]
fn dmer_read_len(d: usize) -> usize {
d.max(8)
}
#[inline(always)]
unsafe fn hash_at<const H: usize>(ptr: *const u8, f: u32) -> usize {
let v = u64::from_le(unsafe { ptr.cast::<u64>().read_unaligned() });
match H {
4 => ((v as u32).wrapping_mul(PRIME_4_BYTES) >> (32 - f)) as usize,
5 => ((v << 24).wrapping_mul(PRIME_5_BYTES) >> (64 - f)) as usize,
6 => ((v << 16).wrapping_mul(PRIME_6_BYTES) >> (64 - f)) as usize,
7 => ((v << 8).wrapping_mul(PRIME_7_BYTES) >> (64 - f)) as usize,
_ => (v.wrapping_mul(PRIME_8_BYTES) >> (64 - f)) as usize,
}
}
macro_rules! with_hashed_bytes {
($d:expr, $h:ident => $body:expr) => {
match $d.min(8) {
4 => {
const $h: usize = 4;
$body
}
5 => {
const $h: usize = 5;
$body
}
6 => {
const $h: usize = 6;
$body
}
7 => {
const $h: usize = 7;
$body
}
_ => {
const $h: usize = 8;
$body
}
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TableTooLarge {
pub(crate) entries: usize,
}
impl From<TableTooLarge> for io::Error {
fn from(table: TableTooLarge) -> Self {
io::Error::new(
io::ErrorKind::OutOfMemory,
std::format!(
"a FastCOVER table of {} entries does not fit in memory; use a smaller f",
table.entries
),
)
}
}
fn zeroed_counts<C: WindowCount>(len: usize) -> Result<Vec<C>, TableTooLarge> {
let too_large = TableTooLarge { entries: len };
let layout = core::alloc::Layout::array::<C>(len).map_err(|_| too_large)?;
if layout.size() == 0 {
return Ok(Vec::new());
}
let pointer = unsafe { alloc::alloc::alloc_zeroed(layout) };
if pointer.is_null() {
return Err(too_large);
}
Ok(unsafe { Vec::from_raw_parts(pointer.cast::<C>(), len, len) })
}
const ACCEL_FINALIZE_PERCENT: [usize; MAX_ACCEL as usize + 1] =
[100, 100, 50, 34, 25, 20, 17, 14, 13, 11, 10];
pub(super) fn finalize_samples(train: usize, accel: u32) -> usize {
train * ACCEL_FINALIZE_PERCENT[accel as usize] / 100
}
pub(super) struct FastCoverContext<'s> {
data: &'s [u8],
nb_dmers: usize,
freqs: Vec<u32>,
d: usize,
f: u32,
}
impl<'s> FastCoverContext<'s> {
pub(super) fn new(
samples: &SampleSet<'s>,
train: usize,
d: usize,
f: u32,
accel: u32,
) -> io::Result<Self> {
debug_assert!((4..).contains(&d) && (1..=MAX_F).contains(&f));
debug_assert!((1..=MAX_ACCEL).contains(&accel));
let data = samples.leading(train);
let read_len = dmer_read_len(d);
let Some(nb_dmers) = data.len().checked_sub(read_len).map(|n| n + 1) else {
return Err(refuse(
TrainingError::Samples,
&std::format!(
"the training samples total {} bytes; FastCOVER needs at least {read_len}",
data.len()
),
));
};
if u32::try_from(nb_dmers).is_err() {
return Err(refuse(
TrainingError::Samples,
"the training samples are too large for FastCOVER (4 GiB at most)",
));
}
samples.check_holds_dmer(train, read_len)?;
let mut freqs = zeroed_counts::<u32>(1usize << f)?;
let offsets = &samples.offsets()[..=train];
with_hashed_bytes!(d, H => count_dmers::<H>(data, offsets, read_len, f, accel as usize, &mut freqs));
Ok(Self {
data,
nb_dmers,
freqs,
d,
f,
})
}
pub(super) fn build<'o>(
&self,
freqs: &mut Vec<u32>,
window: &mut WindowCounts,
out: &'o mut Vec<u8>,
capacity: usize,
k: usize,
) -> Result<&'o [u8], TableTooLarge> {
debug_assert!(self.d <= k);
freqs.clear();
freqs
.try_reserve_exact(self.freqs.len())
.map_err(|_| TableTooLarge {
entries: self.freqs.len(),
})?;
freqs.extend_from_slice(&self.freqs);
let layout = EpochLayout {
dmers_in_k: k - self.d + 1,
epochs: compute_epochs(capacity, self.nb_dmers, k, 1),
};
out.resize(capacity, 0);
let WindowCounts { narrow, wide, ring } = window;
let tail = if layout.dmers_in_k < usize::from(u16::MAX) {
let counts = narrow.get_or_insert_with_result(self.f)?;
with_hashed_bytes!(self.d, H => self.select_segments::<H, u16>(out, freqs, counts, ring, layout))
} else {
let counts = wide.get_or_insert_with_result(self.f)?;
with_hashed_bytes!(self.d, H => self.select_segments::<H, u32>(out, freqs, counts, ring, layout))
};
Ok(&out[tail..])
}
fn select_segments<const H: usize, C: WindowCount>(
&self,
out: &mut [u8],
freqs: &mut [u32],
segment_freqs: &mut [C],
ring: &mut Vec<u32>,
layout: EpochLayout,
) -> usize {
let EpochLayout { dmers_in_k, epochs } = layout;
let (sample, f, d) = (self.data, self.f, self.d);
let zero = C::from(0);
let one = C::from(1);
debug_assert!(freqs.len() == 1 << f && segment_freqs.len() == 1 << f);
debug_assert!(epochs.num * epochs.size <= self.nb_dmers);
let base = sample.as_ptr();
let freq_ptr = freqs.as_mut_ptr();
let window_ptr = segment_freqs.as_mut_ptr();
let window_limit = dmers_in_k + 1;
let longest_epoch = self.nb_dmers - (epochs.num - 1) * epochs.size;
let ring_len = window_limit.min(longest_epoch);
ring.clear();
ring.resize(ring_len, 0);
let ring_ptr = ring.as_mut_ptr();
let mut tail = out.len();
let max_zero_score_run = epochs.num.min(10);
let mut zero_score_run = 0usize;
let mut epoch = 0usize;
while tail > 0 {
let (epoch_begin, epoch_end) = epochs.bounds(epoch, self.nb_dmers);
epoch = (epoch + 1) % epochs.num;
let mut best_begin = 0usize;
let mut best_end = 0usize;
let mut best_score = 0u64;
let mut active_begin = epoch_begin;
let mut active_end = epoch_begin;
let mut active_score = 0u64;
let mut ring_head = 0usize;
let mut ring_tail = 0usize;
while active_end < epoch_end {
let idx = unsafe { hash_at::<H>(base.add(active_end), f) };
unsafe { *ring_ptr.add(ring_tail) = idx as u32 };
ring_tail += 1;
if ring_tail == ring_len {
ring_tail = 0;
}
let count = unsafe { &mut *window_ptr.add(idx) };
if *count == zero {
active_score += u64::from(unsafe { *freq_ptr.add(idx) });
}
*count += one;
active_end += 1;
if active_end - active_begin == window_limit {
let del = unsafe { *ring_ptr.add(ring_head) } as usize;
ring_head += 1;
if ring_head == ring_len {
ring_head = 0;
}
let count = unsafe { &mut *window_ptr.add(del) };
*count -= one;
if *count == zero {
active_score -= u64::from(unsafe { *freq_ptr.add(del) });
}
active_begin += 1;
}
if active_score > best_score {
best_begin = active_begin;
best_end = active_end;
best_score = active_score;
}
}
while active_begin < epoch_end {
let del = unsafe { *ring_ptr.add(ring_head) } as usize;
ring_head += 1;
if ring_head == ring_len {
ring_head = 0;
}
unsafe { *window_ptr.add(del) -= one };
active_begin += 1;
}
while best_begin < best_end
&& unsafe { *freq_ptr.add(hash_at::<H>(base.add(best_begin), f)) } == 0
{
best_begin += 1;
}
debug_assert!(
best_score == 0
|| unsafe { *freq_ptr.add(hash_at::<H>(base.add(best_end - 1), f)) } != 0,
"the best segment ends on a dmer that scores"
);
for pos in best_begin..best_end {
unsafe { *freq_ptr.add(hash_at::<H>(base.add(pos), f)) = 0 };
}
if best_score == 0 {
zero_score_run += 1;
if zero_score_run >= max_zero_score_run {
break;
}
continue;
}
zero_score_run = 0;
let segment_size = (best_end - best_begin + d - 1).min(tail);
if segment_size < d {
break;
}
tail -= segment_size;
out[tail..tail + segment_size]
.copy_from_slice(&sample[best_begin..best_begin + segment_size]);
}
tail
}
}
fn count_dmers<const H: usize>(
data: &[u8],
offsets: &[usize],
read_len: usize,
f: u32,
step: usize,
freqs: &mut [u32],
) {
debug_assert_eq!(freqs.len(), 1 << f);
debug_assert_eq!(offsets.last(), Some(&data.len()));
let base = data.as_ptr();
let counts = freqs.as_mut_ptr();
for bounds in offsets.windows(2) {
let end = bounds[1];
let mut start = bounds[0];
while start + read_len <= end {
unsafe { *counts.add(hash_at::<H>(base.add(start), f)) += 1 };
start += step;
}
}
}
#[derive(Clone, Copy)]
struct EpochLayout {
dmers_in_k: usize,
epochs: Epochs,
}
trait WindowCount: Copy + PartialEq + core::ops::AddAssign + core::ops::SubAssign + From<u8> {}
impl WindowCount for u16 {}
impl WindowCount for u32 {}
#[derive(Default)]
pub(super) struct WindowCounts {
narrow: LazyCounts<u16>,
wide: LazyCounts<u32>,
ring: Vec<u32>,
}
struct LazyCounts<C>(Option<Vec<C>>);
impl<C> Default for LazyCounts<C> {
fn default() -> Self {
Self(None)
}
}
impl<C: WindowCount> LazyCounts<C> {
fn get_or_insert_with_result(&mut self, f: u32) -> Result<&mut [C], TableTooLarge> {
let len = 1usize << f;
if self.0.as_ref().is_none_or(|counts| counts.len() != len) {
self.0 = Some(zeroed_counts::<C>(len)?);
}
Ok(self.0.as_mut().expect("allocated above"))
}
}
#[cfg(test)]
mod tests;