use std::collections::HashSet;
use std::hash::{BuildHasherDefault, Hash, Hasher};
#[derive(Default)]
pub struct U64Hasher(u64);
impl Hasher for U64Hasher {
#[inline(always)]
fn write_u64(&mut self, n: u64) {
self.0 = n.wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
#[inline]
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, _bytes: &[u8]) {
unreachable!("U64Hasher only supports u64 keys");
}
}
#[derive(Default)]
pub struct U32Hasher(u64);
impl Hasher for U32Hasher {
#[inline(always)]
fn write_u32(&mut self, n: u32) {
self.0 = (n as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
#[inline]
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, _bytes: &[u8]) {
unreachable!("U32Hasher only supports u32 keys");
}
}
const MAX_BIN_SHIFT: u32 = 32;
pub fn pick_bin_shift(total_len: u64, min_bin_count: u32) -> u32 {
let min = min_bin_count.max(1) as u64;
for shift in (0..=MAX_BIN_SHIFT).rev() {
let bin_count = if shift >= 64 { 0 } else { (total_len + (1u64 << shift) - 1) >> shift };
if bin_count >= min {
return shift;
}
}
0
}
pub struct BinIndex {
seq_offs: Vec<u64>,
total_len: u64,
max_read_length: i32,
bin_shift: u32,
bin_mask: u64,
}
impl BinIndex {
pub fn from_ref_lengths(ref_lengths: &[i32], max_read_length: i32, min_bin_count: u32) -> Self {
let mut seq_offs = Vec::with_capacity(ref_lengths.len() + 1);
let mut total_len: u64 = 0;
seq_offs.push(0);
total_len += pad_length_for(0, max_read_length) as u64 + 1;
for &len in ref_lengths {
let seq_off = total_len;
seq_offs.push(seq_off);
total_len += pad_length_for(len, max_read_length) as u64 + 1;
}
let bin_shift = pick_bin_shift(total_len, min_bin_count);
let bin_mask = if bin_shift >= 64 { u64::MAX } else { (1u64 << bin_shift) - 1 };
Self { seq_offs, total_len, max_read_length, bin_shift, bin_mask }
}
pub fn bin_shift(&self) -> u32 {
self.bin_shift
}
pub fn bin_count(&self) -> u32 {
if self.bin_shift >= 64 {
1
} else {
((self.total_len + self.bin_mask) >> self.bin_shift) as u32
}
}
pub fn num_seqs(&self) -> usize {
self.seq_offs.len()
}
pub fn bin_for(&self, seq_num: usize, pos: i32) -> (u32, u32, bool) {
let padded = self.seq_offs[seq_num] as i64 + pad_pos(pos, self.max_read_length) as i64;
let lo = self.seq_offs[seq_num] as i64;
let next_off = self.seq_offs.get(seq_num + 1).copied().unwrap_or(self.total_len);
let hi = next_off as i64 - 1;
let clamped = padded.clamp(lo, hi);
let was_clamped = clamped != padded;
let combined = clamped as u64;
let bin_num = (combined >> self.bin_shift) as u32;
let bin_pos = (combined & self.bin_mask) as u32;
(bin_num, bin_pos, was_clamped)
}
}
pub fn pad_length_for(n: i32, max_read_length: i32) -> i32 {
n + 2 * max_read_length
}
pub fn pad_pos(p: i32, max_read_length: i32) -> i32 {
p + max_read_length
}
pub fn five_prime_aligned_pos(
rapos: i32,
sclip: i32,
eclip: i32,
ra_len: i32,
is_reverse: bool,
) -> i32 {
if is_reverse { rapos + ra_len + eclip - 1 } else { rapos - sclip }
}
pub fn orphan_pos_override(rapos: i32, sclip: i32) -> i32 {
rapos - sclip
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SingleEndStrategy {
StrandAware,
PicardApprox,
PicardExact,
SamblasterLegacy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethylationMode {
Directional,
}
#[inline]
pub(crate) fn stride_for(bin_count: u32) -> u32 {
(bin_count + 1) * 2
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Slot<S> {
pub off: usize,
pub sig: S,
}
pub(crate) type PairSlot = Slot<u64>;
pub(crate) type FragmentSlot = Slot<u32>;
#[inline]
pub(crate) fn single_end_slot(
bin_num: u32,
bin_pos: u32,
rev: bool,
strand_aware: bool,
) -> FragmentSlot {
let s = bin_num * 2 + (rev && strand_aware) as u32;
Slot { off: s as usize, sig: bin_pos }
}
pub struct DupTable<S, H>
where
S: Copy + Eq + Hash,
H: Hasher + Default,
{
stride: u32,
sets: Vec<HashSet<S, BuildHasherDefault<H>>>,
}
pub type PairDupTable = DupTable<u64, U64Hasher>;
pub type FragmentDupTable = DupTable<u32, U32Hasher>;
impl<S, H> DupTable<S, H>
where
S: Copy + Eq + Hash,
H: Hasher + Default,
{
pub fn new_pair(bin_count: u32, cell_cap: usize) -> Self {
let stride = stride_for(bin_count);
let n = (stride as usize).saturating_mul(stride as usize);
Self::with_n_cells(stride, n, cell_cap)
}
pub fn new_single_end(bin_count: u32, cell_cap: usize) -> Self {
let stride = stride_for(bin_count);
let n = stride as usize;
Self::with_n_cells(stride, n, cell_cap)
}
fn with_n_cells(stride: u32, n: usize, cell_cap: usize) -> Self {
let mut sets = Vec::with_capacity(n);
sets.resize_with(n, || HashSet::with_capacity_and_hasher(cell_cap, Default::default()));
Self { stride, sets }
}
}
impl PairDupTable {
#[inline]
pub(crate) fn pair_slot(
&self,
bin_num1: u32,
bin_pos1: u32,
rev1: bool,
bin_num2: u32,
bin_pos2: u32,
rev2: bool,
) -> PairSlot {
let s1 = bin_num1 * 2 + (rev1 as u32);
let s2 = bin_num2 * 2 + (rev2 as u32);
Slot {
off: (s1 * self.stride + s2) as usize,
sig: ((bin_pos1 as u64) << 32) | (bin_pos2 as u64),
}
}
#[inline]
pub(crate) fn insert_pair(&mut self, slot: PairSlot) -> bool {
!self.sets[slot.off].insert(slot.sig)
}
#[inline]
pub(crate) fn insert_orphan(&mut self, slot: FragmentSlot) -> bool {
!self.sets[slot.off].insert(slot.sig as u64)
}
pub fn drain_into_fragment_table(&mut self, cell_cap: usize) -> FragmentDupTable {
let stride = self.stride as usize;
let bin_count = self.stride / 2 - 1;
let mut frag = FragmentDupTable::new_single_end(bin_count, cell_cap);
for off in 0..self.sets.len() {
let s1 = (off / stride) as u32;
let s2 = (off % stride) as u32;
let (bin_num1, rev1) = (s1 >> 1, s1 & 1 == 1);
let (bin_num2, rev2) = (s2 >> 1, s2 & 1 == 1);
let cell = std::mem::take(&mut self.sets[off]);
for sig in cell {
let bin_pos1 = (sig >> 32) as u32;
let bin_pos2 = (sig & 0xFFFF_FFFF) as u32;
frag.check_or_insert(bin_num1, bin_pos1, rev1);
frag.check_or_insert(bin_num2, bin_pos2, rev2);
}
}
frag
}
}
impl FragmentDupTable {
#[inline]
pub fn check_or_insert(&mut self, bin_num: u32, bin_pos: u32, rev: bool) -> bool {
self.insert(single_end_slot(bin_num, bin_pos, rev, true))
}
#[inline]
pub(crate) fn insert(&mut self, slot: FragmentSlot) -> bool {
!self.sets[slot.off].insert(slot.sig)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bin_for_does_not_clamp_positions_within_the_padding() {
let bins = BinIndex::from_ref_lengths(&[1_000_000], 1000, 32);
assert!(!bins.bin_for(1, 0).2);
assert!(!bins.bin_for(1, 999_999).2);
assert!(!bins.bin_for(1, -1000).2); }
#[test]
fn bin_for_clamps_positions_beyond_the_padding() {
let bins = BinIndex::from_ref_lengths(&[1_000_000], 1000, 32);
let (_, _, clamped_start) = bins.bin_for(1, -10_000);
assert!(clamped_start, "a clip far past the start should clamp");
let (_, _, clamped_end) = bins.bin_for(1, 1_010_000);
assert!(clamped_end, "a clip far past the end should clamp");
}
#[test]
fn num_seqs_counts_contigs_plus_unmapped_slot() {
let bins = BinIndex::from_ref_lengths(&[100, 200, 300], 1000, 32);
assert_eq!(bins.num_seqs(), 4); }
#[test]
fn dm_repeat_detected_as_duplicate() {
let mut t = PairDupTable::new_pair(8, 64);
let s = t.pair_slot(1, 100, false, 2, 200, true);
assert!(!t.insert_pair(s));
assert!(t.insert_pair(s));
}
#[test]
fn dm_strand_differs_no_collision() {
let mut t = PairDupTable::new_pair(8, 64);
let fwd = t.pair_slot(1, 100, false, 2, 200, false);
let rev = t.pair_slot(1, 100, false, 2, 200, true);
assert!(!t.insert_pair(fwd));
assert!(!t.insert_pair(rev));
}
#[test]
fn single_end_slot_is_strand_aware_unless_legacy() {
let fwd = single_end_slot(3, 555, false, true);
let rev = single_end_slot(3, 555, true, true);
assert_ne!(fwd.off, rev.off, "strand-aware fwd/rev must land in different cells");
assert_eq!(fwd.sig, rev.sig);
assert_eq!(
single_end_slot(3, 555, false, false),
single_end_slot(3, 555, true, false),
"legacy fwd/rev must be the same slot"
);
}
#[test]
fn strand_aware_orphan_repeat_detected() {
let mut t = PairDupTable::new_pair(8, 64);
let s = single_end_slot(3, 555, true, true);
assert!(!t.insert_orphan(s));
assert!(t.insert_orphan(s));
}
#[test]
fn strand_aware_orphan_fwd_rev_do_not_collide() {
let mut t = PairDupTable::new_pair(8, 64);
assert!(!t.insert_orphan(single_end_slot(3, 555, false, true)));
assert!(!t.insert_orphan(single_end_slot(3, 555, true, true)));
}
#[test]
fn samblaster_legacy_orphan_fwd_rev_collide() {
let mut t = PairDupTable::new_pair(8, 64);
assert!(!t.insert_orphan(single_end_slot(3, 555, false, false)));
assert!(t.insert_orphan(single_end_slot(3, 555, true, false)));
}
#[test]
fn fragment_dup_table_repeat_detected() {
let mut t = FragmentDupTable::new_single_end(8, 64);
assert!(!t.check_or_insert(3, 555, true));
assert!(t.check_or_insert(3, 555, true));
}
#[test]
fn fragment_dup_table_strand_aware() {
let mut t = FragmentDupTable::new_single_end(8, 64);
assert!(!t.check_or_insert(3, 555, false));
assert!(!t.check_or_insert(3, 555, true));
}
#[test]
fn drain_recovers_both_pair_ends_into_fragment_table() {
let mut pairs = PairDupTable::new_pair(8, 64);
let s = pairs.pair_slot(1, 100, false, 2, 200, true);
assert!(!pairs.insert_pair(s));
let mut frag = pairs.drain_into_fragment_table(64);
assert!(frag.check_or_insert(1, 100, false), "pair end A should be present");
assert!(frag.check_or_insert(2, 200, true), "pair end B should be present");
assert!(!frag.check_or_insert(3, 300, false), "unrelated coord should be absent");
}
#[test]
fn drain_is_strand_aware() {
let mut pairs = PairDupTable::new_pair(8, 64);
let s = pairs.pair_slot(4, 555, false, 5, 600, false);
assert!(!pairs.insert_pair(s));
let mut frag = pairs.drain_into_fragment_table(64);
assert!(!frag.check_or_insert(4, 555, true), "reverse coord must not collide with fwd end");
assert!(frag.check_or_insert(4, 555, false), "forward end should be present");
}
#[test]
fn fragment_dup_table_register_and_query() {
let mut t = FragmentDupTable::new_single_end(8, 64);
assert!(!t.check_or_insert(3, 555, false));
assert!(t.check_or_insert(3, 555, false));
}
}