1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! This module contains some constants/matrices for curvature calculation.
use std::fmt;

/// The number of nucleotides in a triplet, which is also the number of dimensions in the
/// nucleotide matrices used for triplet -> value lookup.
pub const TRIPLET_SIZE: usize = 3;

/// A type alias for a 3D matrix sized 4x4x4 of f64 values. The first dimension is the
/// first nucleotide in a triplet, the second dimension is the second nucleotide in a triplet,
/// and the third dimension is the third nucleotide in a triplet.
pub type NucMatrix = [[[f64; 4]; 4]; 4];

/// The TWIST matrix is used to calculate the twist angle in three nucleotides of DNA.
/// The values are all 0.598647428 for all combinations of nucleotide triplets.
pub const TWIST: NucMatrix = [[[0.598647428; 4]; 4]; 4];

/// The TILT matrix is not really used in the current implementation, but is included here
/// for completeness. The values are all 0.0 for all combinations of nucleotide triplets.
pub const TILT: NucMatrix = [[[0.0; 4]; 4]; 4];

/// The simple version fo the ROLL matrix is used to calculate the roll angle in three nucleotides
/// of DNA.
pub const ROLL_ACTIVE: NucMatrix = [
    [
        [0.0633, 0.3500, 4.6709, 2.64115],
        [6.2734, 0.3500, 7.7171, 4.44325],
        [4.8884, 3.9232, 5.0523, 6.8829],
        [5.4903, 3.9232, 5.3055, 5.3055],
    ],
    [
        [4.6709, 6.2734, 5.00295, 5.0673],
        [4.6709, 0.0633, 4.7618, 4.0633],
        [7.7000, 5.4903, 3.05865, 6.75525],
        [7.7000, 4.8884, 7.07195, 4.9907],
    ],
    [
        [4.0633, 4.44325, 5.9806, 5.51645],
        [5.0673, 2.64115, 6.62555, 5.51645],
        [4.9907, 5.3055, 5.89135, 9.0823],
        [6.75525, 6.8829, 5.89135, 9.0823],
    ],
    [
        [4.7618, 7.7171, 6.8996, 6.62555],
        [5.00295, 4.6709, 6.8996, 5.9806],
        [7.07195, 5.3055, 3.869, 5.9000],
        [3.05865, 5.0523, 3.869, 5.827],
    ],
];

/// The "activated" version of the ROLL matrix is used to calculate the roll angle in three
/// nucleotides of DNA. This matrix differs from the simple matrix in that the angle
/// values represent a more activated state of the nucleosomes bound to the DNA.
pub const ROLL_SIMPLE: NucMatrix = [
    [
        [0.1, 0.0, 4.2, 1.6],
        [9.7, 0.0, 8.7, 3.6],
        [6.5, 2.0, 4.7, 6.3],
        [5.8, 2.0, 5.2, 5.2],
    ],
    [
        [7.3, 9.7, 7.8, 6.4],
        [7.3, 0.1, 6.2, 5.1],
        [10.0, 5.8, 0.7, 7.5],
        [10.0, 6.5, 5.8, 6.2],
    ],
    [
        [5.1, 3.6, 6.6, 5.6],
        [6.4, 1.6, 6.8, 5.6],
        [6.2, 5.2, 5.7, 8.2],
        [7.5, 6.3, 4.3, 8.2],
    ],
    [
        [6.2, 8.7, 9.6, 6.8],
        [7.8, 4.2, 9.6, 6.6],
        [5.8, 5.2, 3.0, 4.3],
        [0.7, 4.7, 3.0, 5.7],
    ],
];

#[derive(Debug)]
pub struct MatrixLookupError {
    details: String,
}

impl fmt::Display for MatrixLookupError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Error: {}", self.details)
    }
}

#[derive(Debug, Clone)]
pub(crate) enum RollType {
    Simple,
    Active,
}

/// Looks up a value in a nucleotide matrix based on a triplet of nucleotides.
///
/// This function takes a triplet of nucleotides and a nucleotide matrix, and returns the value
/// at the corresponding position in the matrix. The triplet is expected to contain the ASCII
/// values of 'A', 'C', 'G', or 'T'.  
///
/// # Arguments
///
/// * `triplet` - A slice of u8 representing a triplet of nucleotides. Each u8 should be the ASCII
/// value of 'A', 'C', 'G', or 'T'.
/// * `matrix` - A reference to a `NucMatrix` to look up the value in.
///
/// # Returns
///
/// If the triplet is valid and of length 3, this function returns a `Result` containing the value
/// at the corresponding position in the matrix. If the triplet is not valid or not of length 3,
/// it returns a `Result` containing a `MatrixLookupError`.
///
/// # Errors
///
/// Returns a `MatrixLookupError` if the triplet is not of length 3.  An unrecognized nucleotide
/// will cause this error because the triplet will not be of length 3.
pub(crate) fn matrix_lookup(triplet: &[u8], matrix: &NucMatrix) -> Result<f64, MatrixLookupError> {
    let ixs: Vec<usize> = triplet
        .iter()
        .map(|&x| match x {
            b'A' => Some(0),
            b'T' => Some(1),
            b'G' => Some(2),
            b'C' => Some(3),
            _ => None,
        })
        .flatten()
        .collect();
    if ixs.len() != 3 {
        return Err(MatrixLookupError {
            details: "triplet must be of length 3".to_string(),
        });
    }
    Ok(matrix[ixs[0]][ixs[1]][ixs[2]])
}

#[cfg(test)]
mod tests {
    extern crate approx;
    use approx::assert_relative_eq;

    use super::*;

    #[test]
    fn test_spot_check_indexing() {
        assert_relative_eq!(TWIST[0][0][0], 0.598647428, epsilon = 1e-4);
        assert_relative_eq!(TWIST[1][1][1], 0.598647428, epsilon = 1e-4);
        assert_relative_eq!(ROLL_ACTIVE[1][2][0], 7.7, epsilon = 1e-4);
        assert_relative_eq!(
            matrix_lookup(b"AAA", &TWIST).unwrap(),
            0.598647428,
            epsilon = 1e-4
        );
        assert_relative_eq!(
            matrix_lookup(b"CCC", &TWIST).unwrap(),
            0.598647428,
            epsilon = 1e-4
        );
        assert_relative_eq!(
            matrix_lookup(b"CCA", &ROLL_SIMPLE).unwrap(),
            0.7,
            epsilon = 1e-4
        );
        assert!(matrix_lookup(b"AA", &ROLL_ACTIVE).is_err());
        assert!(matrix_lookup(b"AAAA", &ROLL_ACTIVE).is_err());
        assert!(matrix_lookup(b"AAN", &ROLL_ACTIVE).is_err());
    }

    #[test]
    fn test_matrix_lookup_error_display() {
        let error = MatrixLookupError {
            details: "Test error details".to_string(),
        };
        assert_eq!(format!("{}", error), "Error: Test error details");
    }
}