use crate::error::FmIndexError;
pub const ALPHABET_SIZE: usize = 16;
pub const SENTINEL: u8 = 0;
pub const A: u8 = 1;
pub const C: u8 = 2;
pub const G: u8 = 3;
pub const T: u8 = 4;
pub const N: u8 = 5;
pub const R: u8 = 6;
pub const Y: u8 = 7;
pub const S: u8 = 8;
pub const W: u8 = 9;
pub const K: u8 = 10;
pub const M: u8 = 11;
pub const B: u8 = 12;
pub const D: u8 = 13;
pub const H: u8 = 14;
pub const V: u8 = 15;
const fn build_encode_lut() -> [i8; 256] {
let mut lut = [-1i8; 256];
lut[b'$' as usize] = SENTINEL as i8;
lut[b'A' as usize] = A as i8;
lut[b'a' as usize] = A as i8;
lut[b'C' as usize] = C as i8;
lut[b'c' as usize] = C as i8;
lut[b'G' as usize] = G as i8;
lut[b'g' as usize] = G as i8;
lut[b'T' as usize] = T as i8;
lut[b't' as usize] = T as i8;
lut[b'U' as usize] = T as i8;
lut[b'u' as usize] = T as i8;
lut[b'N' as usize] = N as i8;
lut[b'n' as usize] = N as i8;
lut[b'R' as usize] = R as i8;
lut[b'r' as usize] = R as i8;
lut[b'Y' as usize] = Y as i8;
lut[b'y' as usize] = Y as i8;
lut[b'S' as usize] = S as i8;
lut[b's' as usize] = S as i8;
lut[b'W' as usize] = W as i8;
lut[b'w' as usize] = W as i8;
lut[b'K' as usize] = K as i8;
lut[b'k' as usize] = K as i8;
lut[b'M' as usize] = M as i8;
lut[b'm' as usize] = M as i8;
lut[b'B' as usize] = B as i8;
lut[b'b' as usize] = B as i8;
lut[b'D' as usize] = D as i8;
lut[b'd' as usize] = D as i8;
lut[b'H' as usize] = H as i8;
lut[b'h' as usize] = H as i8;
lut[b'V' as usize] = V as i8;
lut[b'v' as usize] = V as i8;
lut
}
static ENCODE_LUT: [i8; 256] = build_encode_lut();
#[inline]
pub fn encode_byte(b: u8) -> Option<u8> {
let v = ENCODE_LUT[b as usize];
if v < 0 {
None
} else {
Some(v as u8)
}
}
#[inline]
pub fn encode_char(ch: char) -> Option<u8> {
if ch.is_ascii() {
encode_byte(ch as u8)
} else {
None
}
}
pub fn decode_char(code: u8) -> Option<char> {
match code {
SENTINEL => Some('$'),
A => Some('A'),
C => Some('C'),
G => Some('G'),
T => Some('T'),
N => Some('N'),
R => Some('R'),
Y => Some('Y'),
S => Some('S'),
W => Some('W'),
K => Some('K'),
M => Some('M'),
B => Some('B'),
D => Some('D'),
H => Some('H'),
V => Some('V'),
_ => None,
}
}
pub fn iupac_bases(code: u8) -> &'static [u8] {
match code {
x if x == A => &[A],
x if x == C => &[C],
x if x == G => &[G],
x if x == T => &[T],
x if x == N => &[A, C, G, T],
x if x == R => &[A, G],
x if x == Y => &[C, T],
x if x == S => &[G, C],
x if x == W => &[A, T],
x if x == K => &[G, T],
x if x == M => &[A, C],
x if x == B => &[C, G, T],
x if x == D => &[A, G, T],
x if x == H => &[A, C, T],
x if x == V => &[A, C, G],
_ => &[],
}
}
pub fn compatible_symbols(code: u8) -> &'static [u8] {
match code {
x if x == A => &[A, N, R, W, M, D, H, V],
x if x == C => &[C, N, Y, S, M, B, H, V],
x if x == G => &[G, N, R, S, K, B, D, V],
x if x == T => &[T, N, Y, W, K, B, D, H],
x if x == N => &[A, C, G, T, N, R, Y, S, W, K, M, B, D, H, V],
x if x == R => &[A, G, N, R, S, W, K, M, B, D, H, V],
x if x == Y => &[C, T, N, Y, S, W, K, M, B, D, H, V],
x if x == S => &[C, G, N, R, Y, S, K, M, B, D, H, V],
x if x == W => &[A, T, N, R, Y, W, K, M, B, D, H, V],
x if x == K => &[G, T, N, R, Y, S, W, K, B, D, H, V],
x if x == M => &[A, C, N, R, Y, S, W, M, B, D, H, V],
x if x == B => &[C, G, T, N, R, Y, S, W, K, M, B, D, H, V],
x if x == D => &[A, G, T, N, R, Y, S, W, K, M, B, D, H, V],
x if x == H => &[A, C, T, N, R, Y, S, W, K, M, B, D, H, V],
x if x == V => &[A, C, G, N, R, Y, S, W, K, M, B, D, H, V],
_ => &[],
}
}
#[derive(Clone, Copy)]
pub struct AlphabetFns {
pub compatible_fn: fn(u8) -> &'static [u8],
pub core_symbols: &'static [u8],
pub tag: u8,
}
impl std::fmt::Debug for AlphabetFns {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AlphabetFns {{ tag: {} }}", self.tag)
}
}
pub trait Alphabet: Send + Sync + 'static {
fn fns() -> AlphabetFns;
}
pub fn alphabet_fns_from_tag(tag: u8) -> Option<AlphabetFns> {
match tag {
0 => Some(AlphabetFns {
compatible_fn: compatible_symbols,
core_symbols: &[A, C, G, T],
tag: 0,
}),
1 => Some(AlphabetFns {
compatible_fn: ExactDna::compatible,
core_symbols: &[A, C, G, T],
tag: 1,
}),
_ => None,
}
}
pub struct IupacDna;
impl Alphabet for IupacDna {
fn fns() -> AlphabetFns {
AlphabetFns {
compatible_fn: compatible_symbols,
core_symbols: &[A, C, G, T],
tag: 0,
}
}
}
pub struct ExactDna;
impl ExactDna {
pub fn compatible(code: u8) -> &'static [u8] {
match code {
x if x == A => &[A],
x if x == C => &[C],
x if x == G => &[G],
x if x == T => &[T],
_ => &[],
}
}
}
impl Alphabet for ExactDna {
fn fns() -> AlphabetFns {
AlphabetFns {
compatible_fn: ExactDna::compatible,
core_symbols: &[A, C, G, T],
tag: 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DnaSequence {
bases: Vec<u8>,
header: String,
}
impl DnaSequence {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, FmIndexError> {
if s.is_empty() {
return Err(FmIndexError::EmptySequence);
}
let mut bases = Vec::with_capacity(s.len());
for (i, b) in s.bytes().enumerate() {
match encode_byte(b) {
Some(SENTINEL) | None => {
let ch = s[i..].chars().next().unwrap_or(b as char);
return Err(FmIndexError::InvalidCharacter(ch, i));
}
Some(code) => bases.push(code),
}
}
Ok(Self {
bases,
header: String::new(),
})
}
pub fn from_str_with_header(s: &str, header: &str) -> Result<Self, FmIndexError> {
let mut seq = Self::from_str(s)?;
seq.header = header.to_string();
Ok(seq)
}
pub fn from_encoded(bases: Vec<u8>) -> Self {
Self {
bases,
header: String::new(),
}
}
pub fn len(&self) -> usize {
self.bases.len()
}
pub fn is_empty(&self) -> bool {
self.bases.is_empty()
}
pub fn header(&self) -> &str {
&self.header
}
pub fn as_slice(&self) -> &[u8] {
&self.bases
}
}
pub fn concatenate_sequences(
sequences: &[DnaSequence],
) -> Result<(Vec<u8>, Vec<u32>), FmIndexError> {
let total_len: usize = sequences.iter().map(|s| s.len() + 1).sum();
if total_len > u32::MAX as usize {
return Err(FmIndexError::TextTooLarge(total_len));
}
let mut text = Vec::with_capacity(total_len);
let mut cumulative_lengths = Vec::with_capacity(sequences.len());
for seq in sequences {
text.extend_from_slice(seq.as_slice());
text.push(SENTINEL);
cumulative_lengths.push(text.len() as u32);
}
Ok((text, cumulative_lengths))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let pairs = [
('A', A),
('C', C),
('G', G),
('T', T),
('N', N),
('R', R),
('Y', Y),
('S', S),
('W', W),
('K', K),
('M', M),
('B', B),
('D', D),
('H', H),
('V', V),
];
for (ch, code) in pairs {
assert_eq!(encode_char(ch), Some(code), "encode {ch}");
assert_eq!(decode_char(code), Some(ch), "decode {code}");
}
}
#[test]
fn test_case_insensitive() {
assert_eq!(encode_char('a'), Some(A));
assert_eq!(encode_char('r'), Some(R));
assert_eq!(encode_char('y'), Some(Y));
assert_eq!(encode_char('n'), Some(N));
}
#[test]
fn test_u_maps_to_t() {
assert_eq!(encode_char('U'), Some(T));
assert_eq!(encode_char('u'), Some(T));
}
#[test]
fn test_gap_chars_invalid() {
assert!(encode_char('-').is_none());
assert!(encode_char('.').is_none());
}
#[test]
fn test_invalid_char() {
assert!(encode_char('X').is_none());
assert!(encode_char('Z').is_none());
}
#[test]
fn test_dna_sequence_acgt() {
let seq = DnaSequence::from_str("ACGT").unwrap();
assert_eq!(seq.as_slice(), &[A, C, G, T]);
}
#[test]
fn test_dna_sequence_iupac() {
let seq = DnaSequence::from_str("ACGTRYNSWKMBDHV").unwrap();
assert_eq!(
seq.as_slice(),
&[A, C, G, T, R, Y, N, S, W, K, M, B, D, H, V]
);
}
#[test]
fn test_dna_sequence_invalid() {
assert!(DnaSequence::from_str("ACXGT").is_err());
assert!(DnaSequence::from_str("AC-GT").is_err());
}
#[test]
fn test_dna_sequence_empty() {
assert!(DnaSequence::from_str("").is_err());
}
#[test]
fn test_iupac_bases_correctness() {
assert_eq!(iupac_bases(A), &[A]);
assert_eq!(iupac_bases(N), &[A, C, G, T]);
assert_eq!(iupac_bases(R), &[A, G]);
assert_eq!(iupac_bases(Y), &[C, T]);
assert_eq!(iupac_bases(B), &[C, G, T]);
assert_eq!(iupac_bases(V), &[A, C, G]);
}
#[test]
fn test_compatible_symbols_a() {
let compat = compatible_symbols(A);
assert!(compat.contains(&A));
assert!(compat.contains(&N));
assert!(compat.contains(&R));
assert!(compat.contains(&W));
assert!(compat.contains(&M));
assert!(compat.contains(&D));
assert!(compat.contains(&H));
assert!(compat.contains(&V));
assert!(!compat.contains(&C));
assert!(!compat.contains(&G));
assert!(!compat.contains(&T));
assert!(!compat.contains(&Y)); assert!(!compat.contains(&S)); assert!(!compat.contains(&K)); assert!(!compat.contains(&B)); }
#[test]
fn test_compatible_symbols_n_is_universal() {
let compat = compatible_symbols(N);
for code in 1u8..=15 {
assert!(
compat.contains(&code),
"N should be compatible with code {code}"
);
}
}
#[test]
fn test_compatible_symbols_symmetric() {
for a in 1u8..=15 {
for &b in compatible_symbols(a) {
assert!(
compatible_symbols(b).contains(&a),
"compatible_symbols not symmetric: {a} ∈ compatible({b}) but {b} ∉ compatible({a})"
);
}
}
}
#[test]
fn test_concatenate() {
let s1 = DnaSequence::from_str("ACG").unwrap();
let s2 = DnaSequence::from_str("TT").unwrap();
let (text, cum) = concatenate_sequences(&[s1, s2]).unwrap();
assert_eq!(text, vec![A, C, G, SENTINEL, T, T, SENTINEL]);
assert_eq!(cum, vec![4, 7]);
}
#[test]
fn wgsl_compat_table_matches_compatible_symbols() {
let expected_len: [u8; 16] = [0, 8, 8, 8, 8, 15, 12, 12, 12, 12, 12, 12, 14, 14, 14, 14];
#[rustfmt::skip]
let expected_compat: [[u8; 16]; 16] = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 5, 6, 9, 11, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0],
[2, 5, 7, 8, 11, 12, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 5, 6, 8, 10, 12, 13, 15, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 5, 7, 9, 10, 12, 13, 14, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0],
[1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0],
[2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0],
[2, 3, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0],
[1, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0],
[3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 0, 0, 0, 0],
[1, 2, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 0, 0, 0, 0],
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0],
[1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0],
[1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0],
[1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0],
];
for code in 0u8..16 {
let syms = compatible_symbols(code);
let len = syms.len() as u8;
assert_eq!(
len, expected_len[code as usize],
"COMPAT_LEN mismatch for code {code}"
);
for (k, &sym) in syms.iter().enumerate() {
assert_eq!(
sym, expected_compat[code as usize][k],
"COMPAT mismatch for code {code} slot {k}: got {sym}, expected {}",
expected_compat[code as usize][k]
);
}
for k in syms.len()..16 {
assert_eq!(
expected_compat[code as usize][k], 0,
"COMPAT padding non-zero for code {code} slot {k}"
);
}
}
}
}