use haystackfm::{BidirFmIndex, DnaSequence, FmIndex, FmIndexConfig};
fn encode(s: &str) -> Vec<u8> {
use haystackfm::alphabet::encode_char;
s.chars().map(|c| encode_char(c).unwrap()).collect()
}
fn bidir_single(s: &str) -> BidirFmIndex {
let config = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
..Default::default()
};
BidirFmIndex::build_cpu(&[DnaSequence::from_str(s).unwrap()], &config).unwrap()
}
fn bidir_multi(seqs: &[&str]) -> BidirFmIndex {
let dna: Vec<DnaSequence> = seqs
.iter()
.map(|s| DnaSequence::from_str(s).unwrap())
.collect();
let config = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
..Default::default()
};
BidirFmIndex::build_cpu(&dna, &config).unwrap()
}
fn uni(s: &str) -> FmIndex {
let config = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
..Default::default()
};
FmIndex::build_cpu(&[DnaSequence::from_str(s).unwrap()], &config).unwrap()
}
fn naive_count(text: &str, pattern: &str) -> u32 {
if pattern.is_empty() || pattern.len() > text.len() {
return 0;
}
(0..=text.len() - pattern.len())
.filter(|&i| &text[i..i + pattern.len()] == pattern)
.count() as u32
}
fn brute_force_mems(reference: &str, query: &str, min_len: usize) -> Vec<(usize, usize)> {
let n = query.len();
let mut set: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
for start in 0..n {
for end in (start + min_len)..=n {
let sub = &query[start..end];
if !reference.contains(sub) {
continue;
}
let right_max = end == n || !reference.contains(&query[start..end + 1]);
let left_max = start == 0 || !reference.contains(&query[start - 1..end]);
if right_max && left_max {
set.insert((start, end));
}
}
}
let mut v: Vec<_> = set.into_iter().collect();
v.sort();
v
}
#[test]
fn extend_right_count_equals_forward_index() {
for text in &[
"ACGTACGT",
"AAACCCGGGTTT",
"ACGT",
"TTTTTTTT",
"ACGTTAGCCAGTACGT",
] {
let idx = bidir_single(text);
let uni_idx = uni(text);
for pattern_str in &["A", "AC", "ACG", "ACGT", "TT", "GGG", "XXXXXX"] {
let valid: String = pattern_str
.chars()
.filter(|c| "ACGT".contains(*c))
.collect();
if valid.is_empty() {
continue;
}
let pattern = encode(&valid);
let mut iv = idx.full_interval();
let mut ok = true;
for &c in &pattern {
match idx.extend_right(iv, c) {
Some(next) => iv = next,
None => {
ok = false;
break;
}
}
}
let bidir_count = if ok { iv.size() } else { 0 };
let uni_count = uni_idx.count(&pattern);
assert_eq!(
bidir_count, uni_count,
"count mismatch for pattern '{}' in text '{}'",
valid, text
);
}
}
}
#[test]
fn extend_right_size_invariant() {
let idx = bidir_single("ACGTACGTACGT");
let mut iv = idx.full_interval();
assert_eq!(iv.fwd_hi - iv.fwd_lo, iv.rev_hi - iv.rev_lo);
for c in encode("ACGT") {
if let Some(next) = idx.extend_right(iv, c) {
assert_eq!(
next.fwd_hi - next.fwd_lo,
next.rev_hi - next.rev_lo,
"size invariant broken"
);
iv = next;
}
}
}
#[test]
fn extend_left_size_invariant() {
let idx = bidir_single("ACGTACGTACGT");
let mut iv = idx.full_interval();
for c in encode("ACGT") {
iv = idx.extend_right(iv, c).unwrap();
}
assert_eq!(iv.fwd_hi - iv.fwd_lo, iv.rev_hi - iv.rev_lo);
use haystackfm::alphabet::T;
if let Some(next) = idx.extend_left(iv, T) {
assert_eq!(
next.fwd_hi - next.fwd_lo,
next.rev_hi - next.rev_lo,
"size invariant broken after extend_left"
);
}
}
#[test]
fn bidirectional_locate_positions_are_correct() {
let text = "ACGTACGT";
let query_str = "ACG";
let idx = bidir_single(text);
let pattern = encode(query_str);
let mut iv = idx.full_interval();
for &c in &pattern {
iv = idx.extend_right(iv, c).unwrap();
}
let mut positions = idx.locate_interval(&iv);
positions.sort();
for (_, pos) in &positions {
let pos = *pos as usize;
assert!(
pos + query_str.len() <= text.len(),
"position {} out of bounds",
pos
);
assert_eq!(
&text[pos..pos + query_str.len()],
query_str,
"wrong match at position {}",
pos
);
}
assert_eq!(positions.len() as u32, naive_count(text, query_str));
}
#[test]
fn smems_match_brute_force_basic() {
let cases = [
("ACGTACGT", "ACGT"),
("ACGTTAGCCAGTACGT", "CGTTAGC"),
("AAAACCCCC", "AACCC"),
("ACGT", "TGCA"),
("ACGT", "ACGT"),
("TTTTTTTT", "TTTTT"),
];
for (reference, query_str) in &cases {
let idx = bidir_single(reference);
let query = encode(query_str);
let smems = idx.find_smems(&query, 1, false);
let smem_pairs: Vec<(usize, usize)> =
smems.iter().map(|m| (m.query_start, m.query_end)).collect();
let expected = brute_force_mems(reference, query_str, 1);
assert_eq!(
smem_pairs, expected,
"\nreference='{}' query='{}'\nGot: {:?}\nExpected: {:?}",
reference, query_str, smem_pairs, expected
);
}
}
#[test]
fn smem_count_matches_forward_index() {
let text = "ACGTACGTACGT";
let idx = bidir_single(text);
let uni_idx = uni(text);
let query = encode("ACGT");
let smems = idx.find_smems(&query, 1, false);
assert_eq!(smems.len(), 1);
assert_eq!(smems[0].match_count, uni_idx.count(&query));
}
#[test]
fn smems_min_len_filtering() {
let idx = bidir_single("AACCGGTT");
let query = encode("AACCGGTT");
let smems_1 = idx.find_smems(&query, 1, false);
let smems_4 = idx.find_smems(&query, 4, false);
let smems_100 = idx.find_smems(&query, 100, false);
for m in &smems_4 {
assert!(m.len() >= 4, "SMEM shorter than min_len=4: {:?}", m);
}
assert!(smems_100.is_empty());
assert!(smems_1.len() >= smems_4.len());
}
#[test]
fn smem_positions_are_valid() {
let reference = "ACGTTAGCCAGTACGT";
let query_str = "AGTACGT";
let idx = bidir_single(reference);
let query = encode(query_str);
let smems = idx.find_smems(&query, 1, true);
for mem in &smems {
let pattern = &query_str[mem.query_start..mem.query_end];
for (_, pos) in &mem.positions {
let pos = *pos as usize;
assert!(
pos + pattern.len() <= reference.len(),
"position {} out of bounds (ref len {})",
pos,
reference.len()
);
assert_eq!(
&reference[pos..pos + pattern.len()],
pattern,
"wrong match at pos {}",
pos
);
}
assert_eq!(
mem.match_count as usize,
mem.positions.len(),
"match_count != positions.len() for pattern '{}'",
pattern
);
}
}
#[test]
fn smems_on_multi_sequence_index() {
let idx = bidir_multi(&["ACGTACGT", "TGCATGCA"]);
let query = encode("ACGT");
let smems = idx.find_smems(&query, 1, false);
assert!(!smems.is_empty());
for m in &smems {
assert!(m.match_count > 0);
}
}
#[test]
fn find_mems_contains_all_smems() {
let reference = "ACGTTAGCCAGTACGT";
let query_str = "CGTTAGC";
let idx = bidir_single(reference);
let query = encode(query_str);
let smems = idx.find_smems(&query, 1, false);
let mems = idx.find_mems(&query, 1, false);
for smem in &smems {
assert!(
mems.iter()
.any(|m| m.query_start == smem.query_start && m.query_end == smem.query_end),
"SMEM ({},{}) not in MEM list",
smem.query_start,
smem.query_end
);
}
}
#[test]
fn bidir_serialization_preserves_smems() {
let reference = "ACGTTAGCCAGTACGT";
let original = bidir_single(reference);
let bytes = original.to_bytes().unwrap();
let restored = BidirFmIndex::from_bytes(&bytes).unwrap();
let query = encode("CGTTAGC");
let orig_smems = original.find_smems(&query, 1, false);
let rest_smems = restored.find_smems(&query, 1, false);
assert_eq!(orig_smems, rest_smems);
}