use crate::c_array::CArray;
use crate::occ::OccTable;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LookupTable {
pub depth: u32,
core_symbols: Vec<u8>,
intervals: Vec<(u32, u32)>,
}
impl LookupTable {
pub fn build(
depth: u32,
text_len: u32,
c_array: &CArray,
occ: &OccTable,
core_symbols: &[u8],
) -> Self {
assert!(depth > 0, "lookup depth must be ≥ 1");
assert!(!core_symbols.is_empty(), "core_symbols must not be empty");
let radix = core_symbols.len();
let table_size = radix.pow(depth);
let mut intervals = vec![(0u32, 0u32); table_size];
let mut current: Vec<(usize, u32, u32)> = vec![(0, 0, text_len)];
for level in 1..=depth as usize {
let mut next: Vec<(usize, u32, u32)> = Vec::with_capacity(current.len() * radix);
let is_last = level == depth as usize;
for &(parent_idx, lo, hi) in ¤t {
for (digit, &sym) in core_symbols.iter().enumerate() {
let child_idx = parent_idx * radix + digit;
if c_array.symbol_count(sym, text_len) == 0 {
continue;
}
let cv = c_array.get(sym);
let new_lo = cv + occ.rank(sym, lo);
let new_hi = cv + occ.rank(sym, hi);
if is_last {
intervals[child_idx] = (new_lo, new_hi);
} else if new_lo < new_hi {
next.push((child_idx, new_lo, new_hi));
}
}
}
if !is_last {
current = next;
}
}
Self {
depth,
core_symbols: core_symbols.to_vec(),
intervals,
}
}
#[inline]
pub fn get(&self, codes: &[u8]) -> Option<(u32, u32)> {
debug_assert_eq!(codes.len(), self.depth as usize);
let radix = self.core_symbols.len();
let mut idx = 0usize;
for &c in codes.iter().rev() {
let d = self.core_symbols.iter().position(|&s| s == c)?;
idx = idx * radix + d;
}
Some(self.intervals[idx])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alphabet::ExactDna;
use crate::alphabet::{encode_char, DnaSequence};
use crate::fm_index::{FmIndex, FmIndexConfig};
fn make_index_with_lookup(s: &str, depth: u32) -> FmIndex {
let seq = DnaSequence::from_str(s).unwrap();
let config = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
lookup_depth: depth,
build_threads: 1,
occ_encoding: Default::default(),
};
FmIndex::build_cpu(&[seq], &config).unwrap()
}
fn make_exact_index_with_lookup(s: &str, depth: u32) -> FmIndex {
let seq = DnaSequence::from_str(s).unwrap();
let config = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
lookup_depth: depth,
build_threads: 1,
occ_encoding: Default::default(),
};
FmIndex::build_cpu_with::<ExactDna>(&[seq], &config).unwrap()
}
fn encode(s: &str) -> Vec<u8> {
s.chars().map(|c| encode_char(c).unwrap()).collect()
}
#[test]
fn lookup_count_matches_full_search() {
let text = "ACGTTAGCCAGTACGT";
let idx_with = make_index_with_lookup(text, 3);
let idx_no = {
let seq = DnaSequence::from_str(text).unwrap();
let cfg = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
lookup_depth: 0,
build_threads: 1,
occ_encoding: Default::default(),
};
FmIndex::build_cpu(&[seq], &cfg).unwrap()
};
for pat in &["ACG", "CGT", "GCC", "TAG", "ACGT", "TTA", "AAA"] {
let enc = encode(pat);
assert_eq!(
idx_with.count(&enc),
idx_no.count(&enc),
"count mismatch for '{}' with lookup vs without",
pat
);
}
}
#[test]
fn lookup_locate_matches_full_search() {
let text = "ACGTACGTACGT";
let idx_with = make_index_with_lookup(text, 4);
let idx_no = {
let seq = DnaSequence::from_str(text).unwrap();
let cfg = FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
lookup_depth: 0,
build_threads: 1,
occ_encoding: Default::default(),
};
FmIndex::build_cpu(&[seq], &cfg).unwrap()
};
let enc = encode("ACGT");
let mut pos_with = idx_with.locate_positions(&enc);
let mut pos_no = idx_no.locate_positions(&enc);
pos_with.sort();
pos_no.sort();
assert_eq!(pos_with, pos_no);
}
#[test]
fn lookup_short_pattern_falls_back() {
let idx = make_index_with_lookup("ACGTACGT", 4);
let enc = encode("ACG"); assert_eq!(idx.count(&enc), 2);
}
#[test]
fn exact_dna_query_n_returns_zero_hits() {
let text = "ACGTNACGT";
let idx_exact = make_exact_index_with_lookup(text, 3);
let enc_n = encode("N");
assert_eq!(
idx_exact.count(&enc_n),
0,
"ExactDna: query N should return 0 hits"
);
let idx_iupac = make_index_with_lookup(text, 3);
assert!(
idx_iupac.count(&enc_n) > 0,
"IupacDna: query N should match something"
);
}
#[test]
fn exact_dna_acgt_counts_match_iupac() {
let text = "ACGTTAGCCAGTACGT";
let idx_exact = make_exact_index_with_lookup(text, 3);
let idx_iupac = make_index_with_lookup(text, 3);
for pat in &["ACG", "CGT", "GCC", "TAG", "ACGT"] {
let enc = encode(pat);
assert_eq!(
idx_exact.count(&enc),
idx_iupac.count(&enc),
"ExactDna vs IupacDna count mismatch for '{}'",
pat
);
}
}
#[test]
fn lookup_iupac_consistency_with_n_in_text() {
let text = "ACGTNACGT";
let idx_with = make_index_with_lookup(text, 3);
let idx_no = {
let seq = DnaSequence::from_str(text).unwrap();
FmIndex::build_cpu(
&[seq],
&FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
lookup_depth: 0,
build_threads: 1,
occ_encoding: Default::default(),
},
)
.unwrap()
};
for pat in &["ACG", "CGT", "N", "ACGT"] {
let enc = encode(pat);
assert_eq!(
idx_with.count(&enc),
idx_no.count(&enc),
"IupacDna lookup/no-lookup mismatch for '{}'",
pat
);
}
}
#[test]
fn lookup_exact_consistency_with_n_in_text() {
let text = "ACGTNACGT";
let idx_with = make_exact_index_with_lookup(text, 3);
let idx_no = {
let seq = DnaSequence::from_str(text).unwrap();
FmIndex::build_cpu_with::<ExactDna>(
&[seq],
&FmIndexConfig {
sa_sample_rate: 1,
use_gpu: false,
lookup_depth: 0,
build_threads: 1,
occ_encoding: Default::default(),
},
)
.unwrap()
};
for pat in &["ACG", "CGT", "N", "ACGT"] {
let enc = encode(pat);
assert_eq!(
idx_with.count(&enc),
idx_no.count(&enc),
"ExactDna lookup/no-lookup mismatch for '{}'",
pat
);
}
}
}