use super::{OccEncoding, OccTable, BLOCK_SIZE, SUPERBLOCK_SIZE};
use crate::alphabet::ALPHABET_SIZE;
use crate::bwt::Bwt;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
pub fn build_occ_table(bwt: &Bwt, encoding: OccEncoding) -> OccTable {
let n = bwt.len() as u32;
let num_blocks = n.div_ceil(BLOCK_SIZE) as usize;
let num_superblocks = n.div_ceil(SUPERBLOCK_SIZE) as usize;
let blocks_per_sb = (SUPERBLOCK_SIZE / BLOCK_SIZE) as usize;
let chars: Vec<u8> = bwt.iter_chars().collect();
#[cfg(not(target_arch = "wasm32"))]
let blocks: Vec<([u64; ALPHABET_SIZE], [u32; ALPHABET_SIZE])> = (0..num_blocks)
.into_par_iter()
.map(|block_idx| compute_block(&chars, block_idx, n))
.collect();
#[cfg(target_arch = "wasm32")]
let blocks: Vec<([u64; ALPHABET_SIZE], [u32; ALPHABET_SIZE])> = (0..num_blocks)
.map(|block_idx| compute_block(&chars, block_idx, n))
.collect();
let mut totals = [0u32; ALPHABET_SIZE];
for (_, counts) in &blocks {
for (t, c) in totals.iter_mut().zip(counts.iter()) {
*t += c;
}
}
let mut symbol_to_lane = [u8::MAX; ALPHABET_SIZE];
let mut num_lanes: u8 = 0;
for (c, &total) in totals.iter().enumerate() {
if total > 0 {
symbol_to_lane[c] = num_lanes;
num_lanes += 1;
}
}
let num_lanes_usize = num_lanes as usize;
let num_planes = if num_lanes <= 1 {
0
} else {
(u8::BITS - (num_lanes - 1).leading_zeros()) as usize
};
let lane_data_width = match encoding {
OccEncoding::Bitplane => num_planes,
OccEncoding::OneHot => num_lanes_usize,
};
let mut superblock_checkpoints = Vec::with_capacity(num_superblocks * num_lanes_usize);
let mut block_deltas = Vec::with_capacity(num_blocks * num_lanes_usize);
let mut lane_data = Vec::with_capacity(num_blocks * lane_data_width);
let mut cumulative = vec![0u32; num_lanes_usize];
let mut sb_base = vec![0u32; num_lanes_usize];
for (b, (block_bits, block_counts)) in blocks.iter().enumerate() {
if b % blocks_per_sb == 0 {
superblock_checkpoints.extend_from_slice(&cumulative);
sb_base.copy_from_slice(&cumulative);
}
let mut block_lane_data = vec![0u64; lane_data_width];
for c in 0..ALPHABET_SIZE {
let lane = symbol_to_lane[c];
if lane == u8::MAX {
continue;
}
let lane = lane as usize;
block_deltas.push((cumulative[lane] - sb_base[lane]) as u16);
match encoding {
OccEncoding::Bitplane => {
for (p, plane) in block_lane_data.iter_mut().enumerate() {
if (lane >> p) & 1 == 1 {
*plane |= block_bits[c];
}
}
}
OccEncoding::OneHot => {
block_lane_data[lane] = block_bits[c];
}
}
cumulative[lane] += block_counts[c];
}
lane_data.extend_from_slice(&block_lane_data);
}
OccTable::from_parts(
num_lanes,
symbol_to_lane,
superblock_checkpoints,
block_deltas,
lane_data,
n,
encoding,
)
}
#[inline]
fn compute_block(
chars: &[u8],
block_idx: usize,
n: u32,
) -> ([u64; ALPHABET_SIZE], [u32; ALPHABET_SIZE]) {
let start = block_idx as u32 * BLOCK_SIZE;
let end = std::cmp::min(start + BLOCK_SIZE, n);
let mut block_bits = [0u64; ALPHABET_SIZE];
let mut block_counts = [0u32; ALPHABET_SIZE];
for pos in start..end {
let ch = chars[pos as usize] as usize;
let bit_pos = pos - start;
if ch < ALPHABET_SIZE {
block_bits[ch] |= 1u64 << bit_pos;
block_counts[ch] += 1;
}
}
(block_bits, block_counts)
}
pub fn naive_rank(bwt: &Bwt, c: u8, i: u32) -> u32 {
(0..i as usize).filter(|&j| bwt.get(j) == c).count() as u32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alphabet::*;
use crate::bwt::cpu::build_bwt;
use crate::suffix_array::cpu::build_suffix_array;
fn encode(s: &str) -> Vec<u8> {
let mut v: Vec<u8> = s.chars().map(|c| encode_char(c).unwrap()).collect();
if v.last() != Some(&SENTINEL) {
v.push(SENTINEL);
}
v
}
#[test]
fn test_occ_matches_naive() {
let text = encode("ACGTACGTACGT");
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let occ = build_occ_table(&bwt, OccEncoding::Bitplane);
let n = bwt.len() as u32;
for c in 0..ALPHABET_SIZE as u8 {
for i in 0..=n {
let expected = naive_rank(&bwt, c, i);
let actual = occ.rank(c, i);
assert_eq!(
actual, expected,
"Occ({}, {}) = {} but expected {}",
c, i, actual, expected
);
}
}
}
#[test]
fn test_occ_long_text() {
let s = "ACGT".repeat(20); let text = encode(&s);
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let occ = build_occ_table(&bwt, OccEncoding::Bitplane);
let n = bwt.len() as u32;
for c in 0..ALPHABET_SIZE as u8 {
for i in 0..=n {
let expected = naive_rank(&bwt, c, i);
let actual = occ.rank(c, i);
assert_eq!(actual, expected, "Occ({}, {}) mismatch", c, i);
}
}
}
#[test]
fn test_occ_boundary_values() {
let text = encode("ACGTACGTACGT");
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let occ = build_occ_table(&bwt, OccEncoding::Bitplane);
for c in 0..ALPHABET_SIZE as u8 {
assert_eq!(occ.rank(c, 0), 0);
}
let n = bwt.len() as u32;
for c in 0..ALPHABET_SIZE as u8 {
let total = bwt.iter_chars().filter(|&ch| ch == c).count() as u32;
assert_eq!(occ.rank(c, n), total);
}
}
#[test]
fn test_lf_step_matches_symbol_at_and_rank() {
let s = "ACGT".repeat(50); let text = encode(&s);
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let occ = build_occ_table(&bwt, OccEncoding::Bitplane);
let n = bwt.len() as u32;
for pos in 0..n {
let (sym, rank) = occ.lf_step(pos);
assert_eq!(sym, occ.symbol_at(pos), "symbol mismatch at pos {pos}");
assert_eq!(rank, occ.rank(sym, pos), "rank mismatch at pos {pos}");
}
}
#[test]
fn test_occ_compacts_to_effective_alphabet() {
let text = encode("ACGTACGTACGT");
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let occ = build_occ_table(&bwt, OccEncoding::Bitplane);
assert_eq!(occ.num_lanes(), 5, "expected exactly {{$,A,C,G,T}} lanes");
let n = bwt.len() as u32;
for c in 0..ALPHABET_SIZE as u8 {
for i in 0..=n {
assert_eq!(
occ.rank(c, i),
naive_rank(&bwt, c, i),
"Occ({c}, {i}) mismatch"
);
}
}
}
#[test]
fn test_bitplane_and_onehot_encodings_agree() {
let s = "ACGT".repeat(50); let text = encode(&s);
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let bitplane = build_occ_table(&bwt, OccEncoding::Bitplane);
let onehot = build_occ_table(&bwt, OccEncoding::OneHot);
assert_eq!(bitplane.num_lanes(), onehot.num_lanes());
let n = bwt.len() as u32;
for c in 0..ALPHABET_SIZE as u8 {
for i in 0..=n {
assert_eq!(
bitplane.rank(c, i),
onehot.rank(c, i),
"rank({c}, {i}) mismatch between encodings"
);
}
}
for pos in 0..n {
assert_eq!(
bitplane.symbol_at(pos),
onehot.symbol_at(pos),
"symbol_at({pos}) mismatch"
);
assert_eq!(
bitplane.lf_step(pos),
onehot.lf_step(pos),
"lf_step({pos}) mismatch"
);
}
assert_eq!(
bitplane.reconstruct_bwt_u32(),
onehot.reconstruct_bwt_u32(),
"reconstruct_bwt_u32 mismatch"
);
}
#[test]
fn rank_pair_matches_scalar_rank() {
let text = encode(&"ACGTTGCAACGT".repeat(30));
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let n = bwt.len() as u32;
for enc in [OccEncoding::Bitplane, OccEncoding::OneHot] {
let occ = build_occ_table(&bwt, enc);
for c in 0..ALPHABET_SIZE as u8 {
for lo in (0..=n).step_by(7) {
for hi in (lo..=n).step_by(11) {
assert_eq!(
occ.rank_pair(c, lo, hi),
(occ.rank(c, lo), occ.rank(c, hi)),
"rank_pair({c}, {lo}, {hi}) mismatch ({enc:?})"
);
}
}
}
}
}
#[test]
fn rank_many_matches_scalar_rank() {
let text = encode(&"GATTACAGATTACA".repeat(20));
let sa = build_suffix_array(&text);
let bwt = build_bwt(&text, &sa);
let n = bwt.len() as u32;
for enc in [OccEncoding::Bitplane, OccEncoding::OneHot] {
let occ = build_occ_table(&bwt, enc);
let mut queries: Vec<(u8, u32)> = Vec::new();
for c in 0..ALPHABET_SIZE as u8 {
for i in (0..=n).step_by(5) {
queries.push((c, i));
}
}
let mut out = vec![0u32; queries.len()];
occ.rank_many(&queries, &mut out);
for (&(c, i), &got) in queries.iter().zip(&out) {
assert_eq!(
got,
occ.rank(c, i),
"rank_many({c}, {i}) mismatch ({enc:?})"
);
}
}
}
}