From 75ccc2ece420b08c99c2f7d4c514fb9e9f515aef Mon Sep 17 00:00:00 2001 From: ignatiuspang Date: Fri, 25 Sep 2026 22:57:25 +1000 Subject: [PATCH 1/2] fix(bitkmer): correct reverse complement window size and length tag in minimizer --- src/bitkmer.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/bitkmer.rs b/src/bitkmer.rs index ca0bf65..d90b5e5 100644 --- a/src/bitkmer.rs +++ b/src/bitkmer.rs @@ -144,21 +144,28 @@ pub fn canonical(kmer: BitKmer) -> (BitKmer, bool) { /// Find the lexicographically lowest substring of a given length in the `BitKmer` pub fn minimizer(kmer: BitKmer, minmer_size: u8) -> BitKmer { + if minmer_size == 0 || minmer_size > kmer.1 { + return (0, minmer_size); + } let mut new_kmer = kmer.0; let mut lowest = !0; - let bitmask = (BitKmerSeq::pow(2, u32::from(2 * minmer_size)) - 1) as BitKmerSeq; + let bitmask = if minmer_size >= 32 { + BitKmerSeq::MAX + } else { + (1u64 << (2 * minmer_size)) - 1 + }; for _ in 0..=(kmer.1 - minmer_size) { let cur = bitmask & new_kmer; if cur < lowest { lowest = cur; } - let cur_rev = reverse_complement((bitmask & new_kmer, kmer.1)); + let cur_rev = reverse_complement((bitmask & new_kmer, minmer_size)); if cur_rev.0 < lowest { lowest = cur_rev.0; } new_kmer >>= 2; } - (lowest, kmer.1) + (lowest, minmer_size) } pub fn bitmer_to_bytes(kmer: BitKmer) -> Vec { @@ -260,10 +267,24 @@ mod tests { #[test] fn test_minimizer() { - assert_eq!(minimizer((0b00_1011, 3), 2).0, 0b0010); - assert_eq!(minimizer((0b00_1011, 3), 1).0, 0b00); - assert_eq!(minimizer((0b1100_0011, 4), 2).0, 0b0000); - assert_eq!(minimizer((0b11_0001, 3), 2).0, 0b0001); + // "AGT" has 2-mers "AG" (2) and "GT" (11). + // The reverse complement of "GT" is "AC" (1), which is the canonical minimizer. + assert_eq!(minimizer((0b00_1011, 3), 2), (0b0001, 2)); + assert_eq!(minimizer((0b00_1011, 3), 1), (0b00, 1)); + assert_eq!(minimizer((0b1100_0011, 4), 2), (0b0000, 2)); + assert_eq!(minimizer((0b11_0001, 3), 2), (0b0001, 2)); + } + + #[test] + fn test_minimizer_matches_sequence_minimizer() { + let seq = b"AGTCAGTCAGT"; + let kmer = bytes_to_bitmer(seq); + for minmer_size in 1..=seq.len() as u8 { + let bit_min = minimizer(kmer, minmer_size); + let seq_min = crate::sequence::minimizer(seq, minmer_size as usize); + assert_eq!(bit_min.1, minmer_size); + assert_eq!(bitmer_to_bytes(bit_min), seq_min.as_ref()); + } } #[test] From 16bbca7429c1e0156392ad4669d703d27af2d85a Mon Sep 17 00:00:00 2001 From: ignatiuspang <2321366+IgnatiusPang@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:43:03 +1000 Subject: [PATCH 2/2] fix(bitkmer): assert valid minmer_size in minimizer and add panic tests --- src/bitkmer.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/bitkmer.rs b/src/bitkmer.rs index d90b5e5..4c78f0f 100644 --- a/src/bitkmer.rs +++ b/src/bitkmer.rs @@ -144,9 +144,10 @@ pub fn canonical(kmer: BitKmer) -> (BitKmer, bool) { /// Find the lexicographically lowest substring of a given length in the `BitKmer` pub fn minimizer(kmer: BitKmer, minmer_size: u8) -> BitKmer { - if minmer_size == 0 || minmer_size > kmer.1 { - return (0, minmer_size); - } + assert!( + minmer_size > 0 && minmer_size <= kmer.1, + "minmer_size must be between 1 and the kmer length" + ); let mut new_kmer = kmer.0; let mut lowest = !0; let bitmask = if minmer_size >= 32 { @@ -287,6 +288,18 @@ mod tests { } } + #[test] + #[should_panic] + fn test_minimizer_zero_size() { + minimizer((0b00_1011, 3), 0); + } + + #[test] + #[should_panic] + fn test_minimizer_size_greater_than_kmer() { + minimizer((0b00_1011, 3), 4); + } + #[test] fn test_bytes_to_bitkmer() { let mut ikmer: BitKmer = bytes_to_bitmer(b"C");