Skip to main content

chelae_lib/
mod.rs

1//! Crate-level library surface shared by `chelae` subcommands.
2//!
3//! Currently exposes:
4//! - [`IUPAC_MASKS`] — a 256-entry byte LUT mapping ASCII base characters (and the
5//!   subset of IUPAC ambiguity codes) onto 4-bit masks. Used by the adapter matcher
6//!   to evaluate IUPAC-aware base compatibility: two bases are compatible iff
7//!   `IUPAC_MASKS[a] & IUPAC_MASKS[b] != 0`.
8//! - [`adapter_db`] — built-in 3' adapter presets for common sequencing kits,
9//!   selectable via `--kit`.
10
11pub mod adapter_db;
12
13/// ASCII-indexed IUPAC ambiguity masks. Bit 0 = A, bit 1 = C, bit 2 = G, bit 3 = T
14/// (U is aliased to T). Case-insensitive: both uppercase and lowercase entries are
15/// populated. Non-IUPAC bytes map to 0 — callers compute compatibility via
16/// `MASKS[a] & MASKS[b] != 0`, and a 0 mask always yields "incompatible".
17pub const IUPAC_MASKS: [u8; 256] = {
18    let mut masks = [0u8; 256];
19    let (a, c, g, t) = (1, 2, 4, 8);
20    let pairs: &[(u8, u8)] = &[
21        (b'A', a),
22        (b'C', c),
23        (b'G', g),
24        (b'T', t),
25        (b'U', t),
26        (b'M', a | c),
27        (b'R', a | g),
28        (b'W', a | t),
29        (b'S', c | g),
30        (b'Y', c | t),
31        (b'K', g | t),
32        (b'V', a | c | g),
33        (b'H', a | c | t),
34        (b'D', a | g | t),
35        (b'B', c | g | t),
36        (b'N', a | c | g | t),
37    ];
38    let mut i = 0;
39    while i < pairs.len() {
40        let (upper, mask) = pairs[i];
41        masks[upper as usize] = mask;
42        // Case-fold to lowercase (ASCII: `| 0x20`).
43        masks[(upper | 0x20) as usize] = mask;
44        i += 1;
45    }
46    masks
47};
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn acgt_masks_are_one_hot() {
55        assert_eq!(IUPAC_MASKS[b'A' as usize], 0b0001);
56        assert_eq!(IUPAC_MASKS[b'C' as usize], 0b0010);
57        assert_eq!(IUPAC_MASKS[b'G' as usize], 0b0100);
58        assert_eq!(IUPAC_MASKS[b'T' as usize], 0b1000);
59        assert_eq!(IUPAC_MASKS[b'U' as usize], 0b1000, "U aliases T");
60    }
61
62    #[test]
63    fn lowercase_matches_uppercase() {
64        for b in b"ACGTUMRWSYKVHDBN" {
65            assert_eq!(
66                IUPAC_MASKS[*b as usize],
67                IUPAC_MASKS[(*b | 0x20) as usize],
68                "case mismatch for {:?}",
69                *b as char,
70            );
71        }
72    }
73
74    #[test]
75    fn n_matches_everything() {
76        for b in b"ACGT" {
77            assert_ne!(IUPAC_MASKS[*b as usize] & IUPAC_MASKS[b'N' as usize], 0);
78        }
79    }
80
81    #[test]
82    fn non_iupac_bytes_are_zero() {
83        assert_eq!(IUPAC_MASKS[b'Z' as usize], 0);
84        assert_eq!(IUPAC_MASKS[b'.' as usize], 0);
85        assert_eq!(IUPAC_MASKS[0_usize], 0);
86    }
87
88    #[test]
89    fn r_is_superset_of_a_and_g_only() {
90        let r = IUPAC_MASKS[b'R' as usize];
91        assert_ne!(r & IUPAC_MASKS[b'A' as usize], 0);
92        assert_ne!(r & IUPAC_MASKS[b'G' as usize], 0);
93        assert_eq!(r & IUPAC_MASKS[b'C' as usize], 0);
94        assert_eq!(r & IUPAC_MASKS[b'T' as usize], 0);
95    }
96}