1use std::fmt;
3
4pub const TRIPLET_SIZE: usize = 3;
7
8pub type NucMatrix = [[[f64; 4]; 4]; 4];
12
13pub const TWIST: NucMatrix = [[[0.598647428; 4]; 4]; 4];
16
17pub const TILT: NucMatrix = [[[0.0; 4]; 4]; 4];
20
21pub const ROLL_ACTIVE: NucMatrix = [
27 [
28 [0.0633, 0.3500, 4.6709, 2.64115],
29 [6.2734, 0.3500, 7.7171, 4.44325],
30 [4.8884, 3.9232, 5.0523, 6.8829],
31 [5.4903, 3.9232, 5.3055, 5.3055],
32 ],
33 [
34 [4.6709, 6.2734, 5.00295, 5.0673],
35 [4.6709, 0.0633, 4.7618, 4.0633],
36 [7.7000, 5.4903, 3.05865, 6.75525],
37 [7.7000, 4.8884, 7.07195, 4.9907],
38 ],
39 [
40 [4.0633, 4.44325, 5.9806, 5.51645],
41 [5.0673, 2.64115, 6.62555, 5.51645],
42 [4.9907, 5.3055, 5.89135, 9.0823],
43 [6.75525, 6.8829, 5.89135, 9.0823],
44 ],
45 [
46 [4.7618, 7.7171, 6.8996, 6.62555],
47 [5.00295, 4.6709, 6.8996, 5.9806],
48 [7.07195, 5.3055, 3.869, 5.9000],
49 [3.05865, 5.0523, 3.869, 5.827],
50 ],
51];
52
53pub const ROLL_SIMPLE: NucMatrix = [
58 [
59 [0.1, 0.0, 4.2, 1.6],
60 [9.7, 0.0, 8.7, 3.6],
61 [6.5, 2.0, 4.7, 6.3],
62 [5.8, 2.0, 5.2, 5.2],
63 ],
64 [
65 [7.3, 9.7, 7.8, 6.4],
66 [7.3, 0.1, 6.2, 5.1],
67 [10.0, 5.8, 0.7, 7.5],
68 [10.0, 6.5, 5.8, 6.2],
69 ],
70 [
71 [5.1, 3.6, 6.6, 5.6],
72 [6.4, 1.6, 6.8, 5.6],
73 [6.2, 5.2, 5.7, 8.2],
74 [7.5, 6.3, 4.3, 8.2],
75 ],
76 [
77 [6.2, 8.7, 9.6, 6.8],
78 [7.8, 4.2, 9.6, 6.6],
79 [5.8, 5.2, 3.0, 4.3],
80 [0.7, 4.7, 3.0, 5.7],
81 ],
82];
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum MatrixLookupError {
90 WrongLength(usize),
92 UnknownBase(u8),
94}
95
96impl fmt::Display for MatrixLookupError {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 Self::WrongLength(n) => write!(f, "triplet must be of length 3, got {n}"),
100 Self::UnknownBase(base) => {
101 write!(f, "unrecognized nucleotide {:?}", *base as char)
102 }
103 }
104 }
105}
106
107impl std::error::Error for MatrixLookupError {}
108
109#[derive(Debug, Clone, Copy)]
111pub enum RollType {
112 Simple,
113 Active,
114}
115
116const NOT_A_BASE: u8 = u8::MAX;
118
119const NUC_TABLE: [u8; 256] = {
129 let mut table = [NOT_A_BASE; 256];
130 table[b'A' as usize] = 0;
131 table[b'a' as usize] = 0;
132 table[b'T' as usize] = 1;
133 table[b't' as usize] = 1;
134 table[b'G' as usize] = 2;
135 table[b'g' as usize] = 2;
136 table[b'C' as usize] = 3;
137 table[b'c' as usize] = 3;
138 table
139};
140
141#[inline]
145pub fn nuc_index(base: u8) -> Option<usize> {
146 match NUC_TABLE[base as usize] {
147 NOT_A_BASE => None,
148 ix => Some(ix as usize),
149 }
150}
151
152pub fn triplet_indices(
163 triplet: &[u8; TRIPLET_SIZE],
164) -> Result<[usize; TRIPLET_SIZE], MatrixLookupError> {
165 let mut ixs = [0usize; TRIPLET_SIZE];
166 for (slot, &base) in ixs.iter_mut().zip(triplet.iter()) {
167 *slot = nuc_index(base).ok_or(MatrixLookupError::UnknownBase(base))?;
168 }
169 Ok(ixs)
170}
171
172#[inline]
174pub fn lookup_by_index(ixs: &[usize; TRIPLET_SIZE], matrix: &NucMatrix) -> f64 {
175 matrix[ixs[0]][ixs[1]][ixs[2]]
176}
177
178pub fn matrix_lookup(triplet: &[u8], matrix: &NucMatrix) -> Result<f64, MatrixLookupError> {
200 let triplet: &[u8; TRIPLET_SIZE] = triplet
201 .try_into()
202 .map_err(|_| MatrixLookupError::WrongLength(triplet.len()))?;
203 Ok(lookup_by_index(&triplet_indices(triplet)?, matrix))
204}
205
206#[cfg(test)]
207mod tests {
208 use approx::assert_relative_eq;
209
210 use super::*;
211
212 #[test]
213 fn test_spot_check_indexing() {
214 assert_relative_eq!(TWIST[0][0][0], 0.598647428, epsilon = 1e-4);
215 assert_relative_eq!(TWIST[1][1][1], 0.598647428, epsilon = 1e-4);
216 assert_relative_eq!(ROLL_ACTIVE[1][2][0], 7.7, epsilon = 1e-4);
217 assert_relative_eq!(
218 matrix_lookup(b"AAA", &TWIST).unwrap(),
219 0.598647428,
220 epsilon = 1e-4
221 );
222 assert_relative_eq!(
223 matrix_lookup(b"CCC", &TWIST).unwrap(),
224 0.598647428,
225 epsilon = 1e-4
226 );
227 assert_relative_eq!(
228 matrix_lookup(b"CCA", &ROLL_SIMPLE).unwrap(),
229 0.7,
230 epsilon = 1e-4
231 );
232 assert!(matrix_lookup(b"AA", &ROLL_ACTIVE).is_err());
233 assert!(matrix_lookup(b"AAAA", &ROLL_ACTIVE).is_err());
234 assert!(matrix_lookup(b"AAN", &ROLL_ACTIVE).is_err());
235 }
236
237 #[test]
238 fn test_nuc_index_over_the_whole_byte_range() {
239 for byte in 0u8..=255 {
242 let expected = match byte {
243 b'A' | b'a' => Some(0),
244 b'T' | b't' => Some(1),
245 b'G' | b'g' => Some(2),
246 b'C' | b'c' => Some(3),
247 _ => None,
248 };
249 assert_eq!(
250 nuc_index(byte),
251 expected,
252 "byte {byte:?} ({:?})",
253 byte as char
254 );
255 }
256 }
257
258 #[test]
259 fn test_triplet_indices_matches_per_base_decoding() {
260 let triplet = *b"CgA";
261 assert_eq!(triplet_indices(&triplet).unwrap(), [3, 2, 0]);
262 assert_relative_eq!(
264 lookup_by_index(&triplet_indices(&triplet).unwrap(), &ROLL_SIMPLE),
265 matrix_lookup(&triplet, &ROLL_SIMPLE).unwrap(),
266 epsilon = 1e-12
267 );
268 assert!(triplet_indices(b"CgN").is_err());
269 }
270
271 #[test]
272 fn test_matrix_lookup_error_display() {
273 assert_eq!(
274 MatrixLookupError::WrongLength(2).to_string(),
275 "triplet must be of length 3, got 2"
276 );
277 assert_eq!(
278 MatrixLookupError::UnknownBase(b'N').to_string(),
279 "unrecognized nucleotide 'N'"
280 );
281 }
282
283 #[test]
284 fn test_matrix_lookup_error_is_a_std_error() {
285 fn takes_error<E: std::error::Error>(_: E) {}
287 takes_error(MatrixLookupError::UnknownBase(b'N'));
288 let boxed: Box<dyn std::error::Error> = MatrixLookupError::WrongLength(4).into();
289 assert!(boxed.to_string().contains("length 3"));
290 }
291}