refactor: centralize k-mer config and introduce packed sequences
Centralize k-mer and minimizer configuration using a thread-safe global module, and replace manual bit-packing with a memory-efficient `PackedSeq` type. Refactor core sequence and k-mer types to use compile-time length enforcement and centralized hashing. Introduce a new De Bruijn graph implementation with compact node encoding and traversal iterators. Update I/O, partitioning, and builder modules to align with the new architecture, and add the `xxhash-rust` dependency.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Tests use ConstLen<N> — no dependency on global params singletons.
|
||||
type K1 = KmerOf<ConstLen<1>>;
|
||||
type K4 = KmerOf<ConstLen<4>>;
|
||||
|
||||
fn ascii_revcomp(seq: &[u8]) -> Vec<u8> {
|
||||
seq.iter()
|
||||
.rev()
|
||||
.map(|&b| match b {
|
||||
b'A' => b'T',
|
||||
b'T' => b'A',
|
||||
b'C' => b'G',
|
||||
b'G' => b'C',
|
||||
_ => b'A',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn make_seq<const N: usize>() -> Vec<u8> {
|
||||
(0..N).map(|i| b"ACGT"[i % 4]).collect()
|
||||
}
|
||||
|
||||
// ── from_ascii / to_ascii ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn ascii_roundtrip() {
|
||||
macro_rules! check {
|
||||
($n:expr) => {{
|
||||
let ascii = make_seq::<$n>();
|
||||
let kmer = KmerOf::<ConstLen<$n>>::from_ascii(&ascii).unwrap();
|
||||
assert_eq!(kmer.to_ascii(), ascii, "roundtrip failed for k={}", $n);
|
||||
}};
|
||||
}
|
||||
check!(1);
|
||||
check!(2);
|
||||
check!(3);
|
||||
check!(4);
|
||||
check!(8);
|
||||
check!(11);
|
||||
check!(16);
|
||||
check!(31);
|
||||
check!(32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ascii_all_bases() {
|
||||
for (base, expected) in [(b'A', b'A'), (b'C', b'C'), (b'G', b'G'), (b'T', b'T')] {
|
||||
let kmer = K1::from_ascii(&[base]).unwrap();
|
||||
assert_eq!(kmer.to_ascii(), vec![expected]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ascii_invalid_k() {
|
||||
assert!(KmerOf::<ConstLen<0>>::from_ascii(b"A").is_err());
|
||||
assert!(KmerOf::<ConstLen<33>>::from_ascii(b"ACGT").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ascii_too_short() {
|
||||
assert!(KmerOf::<ConstLen<4>>::from_ascii(b"ACG").is_err());
|
||||
}
|
||||
|
||||
// ── nucleotide ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn nucleotide_extraction() {
|
||||
let kmer = K4::from_ascii(b"ACGT").unwrap();
|
||||
assert_eq!(kmer.nucleotide(0), 0b00); // A
|
||||
assert_eq!(kmer.nucleotide(1), 0b01); // C
|
||||
assert_eq!(kmer.nucleotide(2), 0b10); // G
|
||||
assert_eq!(kmer.nucleotide(3), 0b11); // T
|
||||
}
|
||||
|
||||
// ── revcomp ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn revcomp_known_values() {
|
||||
let cases: &[(&[u8], &[u8])] = &[
|
||||
(b"A", b"T"),
|
||||
(b"AC", b"GT"),
|
||||
(b"ACG", b"CGT"),
|
||||
(b"ACGT", b"ACGT"),
|
||||
(b"AAAA", b"TTTT"),
|
||||
(b"TTTT", b"AAAA"),
|
||||
];
|
||||
for (seq, expected) in cases {
|
||||
macro_rules! check_len {
|
||||
($n:expr) => {
|
||||
if seq.len() == $n {
|
||||
let kmer = KmerOf::<ConstLen<$n>>::from_ascii(seq).unwrap();
|
||||
assert_eq!(
|
||||
kmer.revcomp().to_ascii(),
|
||||
*expected,
|
||||
"revcomp wrong for \"{}\"",
|
||||
std::str::from_utf8(seq).unwrap()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
check_len!(1);
|
||||
check_len!(2);
|
||||
check_len!(3);
|
||||
check_len!(4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revcomp_vs_reference() {
|
||||
macro_rules! check {
|
||||
($n:expr) => {{
|
||||
let ascii = make_seq::<$n>();
|
||||
let expected = ascii_revcomp(&ascii);
|
||||
let rc = KmerOf::<ConstLen<$n>>::from_ascii(&ascii)
|
||||
.unwrap()
|
||||
.revcomp();
|
||||
assert_eq!(rc.to_ascii(), expected, "revcomp wrong for k={}", $n);
|
||||
}};
|
||||
}
|
||||
check!(1);
|
||||
check!(4);
|
||||
check!(8);
|
||||
check!(11);
|
||||
check!(16);
|
||||
check!(31);
|
||||
check!(32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revcomp_involution() {
|
||||
macro_rules! check {
|
||||
($n:expr) => {{
|
||||
let ascii = make_seq::<$n>();
|
||||
let kmer = KmerOf::<ConstLen<$n>>::from_ascii(&ascii).unwrap();
|
||||
assert_eq!(
|
||||
kmer.revcomp().revcomp(),
|
||||
kmer,
|
||||
"revcomp∘revcomp≠id for k={}",
|
||||
$n
|
||||
);
|
||||
}};
|
||||
}
|
||||
check!(1);
|
||||
check!(4);
|
||||
check!(8);
|
||||
check!(16);
|
||||
check!(31);
|
||||
check!(32);
|
||||
}
|
||||
|
||||
// ── canonical ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn canonical_palindrome() {
|
||||
let kmer = K4::from_ascii(b"ACGT").unwrap();
|
||||
assert_eq!(kmer.canonical().into_kmer(), kmer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_chooses_lesser() {
|
||||
let kmer = K4::from_ascii(b"TTTT").unwrap();
|
||||
let expected = K4::from_ascii(b"AAAA").unwrap();
|
||||
assert_eq!(kmer.canonical().into_kmer(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_is_minimal() {
|
||||
macro_rules! check {
|
||||
($n:expr) => {{
|
||||
let ascii = make_seq::<$n>();
|
||||
let ck = KmerOf::<ConstLen<$n>>::from_ascii(&ascii)
|
||||
.unwrap()
|
||||
.canonical();
|
||||
let rc = ck.revcomp();
|
||||
assert!(ck.raw() <= rc.raw(), "canonical not minimal for k={}", $n);
|
||||
}};
|
||||
}
|
||||
check!(1);
|
||||
check!(4);
|
||||
check!(8);
|
||||
check!(16);
|
||||
check!(31);
|
||||
check!(32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_idempotent() {
|
||||
macro_rules! check {
|
||||
($n:expr) => {{
|
||||
let ck = KmerOf::<ConstLen<$n>>::from_ascii(&make_seq::<$n>())
|
||||
.unwrap()
|
||||
.canonical();
|
||||
assert_eq!(
|
||||
ck.into_kmer().canonical(),
|
||||
ck,
|
||||
"canonical not idempotent for k={}",
|
||||
$n
|
||||
);
|
||||
}};
|
||||
}
|
||||
check!(1);
|
||||
check!(4);
|
||||
check!(8);
|
||||
check!(16);
|
||||
check!(31);
|
||||
check!(32);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user