Introduce the siblingannex module in obicompactvec to store per-slot minorant flags and sibling counts in a memory-mapped annex file. Add a scatter-gather pipeline in obikindex to compute these values across index layers and write them to .psib files. Implement central_canonical_neighbors in obikseq for generating strand-aware k-mer variants around the middle base. Expose rolling statistics in obiskbuilder and update dependency graphs accordingly.
256 lines
8.3 KiB
Rust
256 lines
8.3 KiB
Rust
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);
|
|
}
|
|
|
|
// ── central_canonical_neighbors ─────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn central_canonical_neighbors_hand_checked_k3() {
|
|
// k=3, centre = index 1. For "ACG", every one of the 4 central
|
|
// substitutions ("AAG","ACG","AGG","ATG") happens to stay in forward
|
|
// orientation when canonicalised (verified by hand: each is already
|
|
// lexicographically <= its own reverse complement), so this case
|
|
// exercises the substitution logic without the RC-flip edge case.
|
|
let ck = KmerOf::<ConstLen<3>>::from_ascii(b"ACG").unwrap().canonical();
|
|
let neighbours = ck.central_canonical_neighbors();
|
|
let ascii: Vec<Vec<u8>> = neighbours.iter().map(|n| n.to_ascii()).collect();
|
|
assert_eq!(ascii, vec![b"AAG".to_vec(), b"ACG".to_vec(), b"AGG".to_vec(), b"ATG".to_vec()]);
|
|
// The identity substitution (centre unchanged) must reproduce `ck`.
|
|
assert!(neighbours.contains(&ck));
|
|
}
|
|
|
|
#[test]
|
|
fn central_canonical_neighbors_identity_present_for_various_k() {
|
|
macro_rules! check {
|
|
($n:expr) => {{
|
|
let ck = KmerOf::<ConstLen<$n>>::from_ascii(&make_seq::<$n>())
|
|
.unwrap()
|
|
.canonical();
|
|
let neighbours = ck.central_canonical_neighbors();
|
|
assert!(
|
|
neighbours.contains(&ck),
|
|
"identity substitution missing from central_canonical_neighbors for k={}",
|
|
$n
|
|
);
|
|
// Every returned neighbour must itself already be canonical.
|
|
for n in &neighbours {
|
|
assert_eq!(n.into_kmer().canonical(), *n, "neighbour not canonical for k={}", $n);
|
|
}
|
|
}};
|
|
}
|
|
check!(1);
|
|
check!(3);
|
|
check!(5);
|
|
check!(31);
|
|
}
|
|
}
|