Skip to content

fix(bitkmer): correct reverse complement window size and length tag in minimizer - #121

Merged
audy merged 2 commits into
onecodex:masterfrom
IgnatiusPang:fix/bitkmer-minimizer-reverse-complement
Oct 6, 2026
Merged

audy merged 2 commits into
onecodex:masterfrom
IgnatiusPang:fix/bitkmer-minimizer-reverse-complement

Conversation

@IgnatiusPang

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Fixes an algorithmic defect where bitkmer::minimizer miscalculated reverse complement values of candidate minimizers and returned an incorrect length tag, causing algorithmic divergence from sequence::minimizer.


Rationale for this change

1. Reverse Complement Window Size Mismatch

In bitkmer::minimizer:

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));
    if cur_rev.0 < lowest {
        lowest = cur_rev.0;
    }
    new_kmer >>= 2;
}

bitmask & new_kmer extracts a window of length minmer_size. However, the tuple passed to reverse_complement was (bitmask & new_kmer, kmer.1), passing the parent k-mer length kmer.1 (e.g., 31) instead of minmer_size (e.g., 15).

Because reverse_complement shifts right by 2 * (32 - len), passing kmer.1 causes the reversed bits to be shifted by 2 * (32 - kmer.1) instead of 2 * (32 - minmer_size). This misaligns the reverse complement by 2 * (kmer.1 - minmer_size) bits and leaves bit-inverted junk in the lowest bits:

  • Concrete Example: For sequence "AGT" ((0b00_1011, 3)) with minmer_size = 2:
    • 2-mer "GT" (0b1011 = 11) has reverse complement "AC" (0b0001 = 1).
    • Passing k=3 shifted by 58 instead of 60 bits, computing cur_rev = 0b111 (7) instead of 1.
    • As a result, bitkmer::minimizer failed to find the true minimizer "AC" (1) and erroneously selected "AG" (2).
    • This caused bitkmer::minimizer to disagree with sequence::minimizer on the exact same input (sequence::minimizer(b"AGT", 2) correctly returned "AC").

2. Return Value Length Tag

minimizer returned (lowest, kmer.1). A minimizer of length minmer_size must carry minmer_size as its length tag. Returning kmer.1 caused downstream functions like bitmer_to_bytes to read kmer.1 bases from the top, decoding prepended phantom 'A' bases (e.g. decoding a 2-mer within a 3-mer as "AAC" instead of "AC").


What changes are included in this PR?

  1. src/bitkmer.rs:
    • Passed minmer_size instead of kmer.1 into reverse_complement.
    • Returned (lowest, minmer_size) as the function result and on early-exit bounds.
    • Guarded minmer_size == 0 || minmer_size > kmer.1 and minmer_size >= 32 exponentiation.
  2. Tests:
    • Corrected test_minimizer assertion for "AGT" to expect canonical minimizer (0b0001, 2) ("AC").
    • Added test_minimizer_matches_sequence_minimizer, verifying exhaustive parity between bitkmer::minimizer and sequence::minimizer across all window lengths.

Are these changes tested?

Yes:

  • cargo test --lib: 42 passed (0 failed).
  • All integration and doc tests passed cleanly.

Comment thread src/bitkmer.rs Outdated
/// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably just be an error. @IgnatiusPang can you change this to an assert + add a test

@IgnatiusPang

Copy link
Copy Markdown
Contributor Author

Thanks @audy! Updated to an assert!(minmer_size > 0 && minmer_size <= kmer.1, ...) and added #[should_panic] unit tests for both minmer_size == 0 and minmer_size > kmer.1.

This also matches sequence::minimizer behavior where invalid window lengths panic. Pushed in commit 16bbca7!

(PR performed with assistance of AI)

@IgnatiusPang
IgnatiusPang requested a review from audy October 3, 2026 05:47
@audy
audy merged commit 5abc78d into onecodex:master Oct 6, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants