Skip to main content

mscore/chemistry/
utility.rs

1use std::sync::LazyLock;
2use regex::Regex;
3use crate::chemistry::unimod::unimod_modifications_mass;
4
5/// Convert a peptide sequence with UNIMOD annotations to a list of tokens
6///
7/// # Arguments
8///
9/// * `sequence` - a string slice of the peptide sequence
10/// * `group_modifications` - a boolean indicating whether to group the amino acid before the UNIMOD with the UNIMOD
11///
12/// # Returns
13///
14/// * `Vec<String>` - a vector of strings representing the tokens
15///
16/// # Example
17///
18/// ```
19/// use mscore::chemistry::utility::unimod_sequence_to_tokens;
20///
21/// let sequence = "PEPTIDE[UNIMOD:1]H";
22/// let tokens = unimod_sequence_to_tokens(sequence, false);
23/// assert_eq!(tokens, vec!["P", "E", "P", "T", "I", "D", "E", "[UNIMOD:1]", "H"]);
24/// let tokens = unimod_sequence_to_tokens(sequence, true);
25/// assert_eq!(tokens, vec!["P", "E", "P", "T", "I", "D", "E[UNIMOD:1]", "H"]);
26/// ```
27/// Compiled once: these run per peptide, and `Regex::new` on every call dominated the cost.
28static UNIMOD_BRACKETED: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[UNIMOD:\d+\]").unwrap());
29
30pub fn unimod_sequence_to_tokens(sequence: &str, group_modifications: bool) -> Vec<String> {
31    let pattern = &*UNIMOD_BRACKETED;
32    let mut tokens = Vec::new();
33    let mut last_index = 0;
34
35    for mat in pattern.find_iter(sequence) {
36        if group_modifications {
37            // When grouping, include the amino acid before the UNIMOD in the token
38            let pre_mod_sequence = &sequence[last_index..mat.start()];
39            let aa_sequence = if pre_mod_sequence.is_empty() {
40                ""
41            } else {
42                &pre_mod_sequence[..pre_mod_sequence.len() - 1]
43            };
44            tokens.extend(aa_sequence.chars().map(|c| c.to_string()));
45
46            // Group the last amino acid with the UNIMOD as one token
47            let grouped_mod = format!("{}{}", pre_mod_sequence.chars().last().unwrap_or_default().to_string(), &sequence[mat.start()..mat.end()]);
48            tokens.push(grouped_mod);
49        } else {
50            // Extract the amino acids before the current UNIMOD and add them as individual tokens
51            let aa_sequence = &sequence[last_index..mat.start()];
52            tokens.extend(aa_sequence.chars().map(|c| c.to_string()));
53
54            // Add the UNIMOD as its own token
55            let unimod = &sequence[mat.start()..mat.end()];
56            tokens.push(unimod.to_string());
57        }
58
59        // Update last_index to the end of the current UNIMOD
60        last_index = mat.end();
61    }
62
63    if !group_modifications || last_index < sequence.len() {
64        // Add the remaining amino acids after the last UNIMOD as individual tokens
65        let remaining_aa_sequence = &sequence[last_index..];
66        tokens.extend(remaining_aa_sequence.chars().map(|c| c.to_string()));
67    }
68
69    tokens
70}
71
72/// Convert a peptide sequence with UNIMOD annotations to a tuple of plain sequence and for each
73/// position in the sequence, the mass of the modification at that position (0 if no modification),
74/// which is the representation of sequence nad modifications used by SAGE
75///
76/// # Arguments
77///
78/// * `input_string` - a string slice of the peptide sequence
79///
80/// # Returns
81///
82/// * `(String, Vec<f64>)` - a tuple of the plain sequence and a vector of f64 representing the mass
83/// of the modification at each position in the sequence
84///
85/// # Example
86///
87/// ```
88/// use mscore::chemistry::utility::find_unimod_patterns;
89///
90/// let sequence = "PEPTIDE[UNIMOD:1]H";
91/// let (stripped_sequence, mods) = find_unimod_patterns(sequence);
92/// assert_eq!(stripped_sequence, "PEPTIDEH");
93/// assert_eq!(mods, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 42.010565, 0.0]);
94/// ```
95pub fn find_unimod_patterns(input_string: &str) -> (String, Vec<f64>) {
96    let results = extract_unimod_patterns(input_string);
97    let stripped_sequence = remove_unimod_annotation(input_string);
98    let index_list = generate_index_list(&results, input_string);
99    let mods = calculate_modifications(&index_list, &stripped_sequence);
100    (stripped_sequence, mods)
101}
102
103fn remove_unimod_annotation(sequence: &str) -> String {
104    let pattern = &*UNIMOD_BRACKETED;
105    pattern.replace_all(sequence, "").to_string()
106}
107
108fn extract_unimod_patterns(input_string: &str) -> Vec<(usize, usize, String)> {
109    let pattern = &*UNIMOD_BRACKETED;
110    pattern.find_iter(input_string)
111        .map(|mat| (mat.start(), mat.end(), mat.as_str().to_string()))
112        .collect()
113}
114
115fn generate_index_list(results: &[(usize, usize, String)], sequence: &str) -> Vec<(usize, String)> {
116    let mut index_list = Vec::new();
117    let mut chars_removed_counter = 0;
118
119    for (start, end, _) in results {
120        let num_chars_removed = end - start;
121        let mod_str = &sequence[*start..*end];
122
123        let later_aa_index = if *start != 0 {
124            start - 1 - chars_removed_counter
125        } else {
126            0
127        };
128
129        index_list.push((later_aa_index, mod_str.to_string()));
130        chars_removed_counter += num_chars_removed;
131    }
132
133    index_list
134}
135
136fn calculate_modifications(index_list: &[(usize, String)], stripped_sequence: &str) -> Vec<f64> {
137    let mut mods = vec![0.0; stripped_sequence.len()];
138    for (index, mod_str) in index_list {
139        if let Some(mass) = unimod_modifications_mass().get(mod_str.as_str()) {
140            mods[*index] += mass;
141        }
142    }
143    mods
144}
145
146/// Reshape the flat prosit array into a 3D array of shape (29, 2, 3)
147///
148/// # Arguments
149///
150/// * `flat_array` - a vector of f64 representing the flat prosit array
151///
152/// # Returns
153///
154/// * `Vec<Vec<Vec<f64>>>` - a 3D array of shape (29, 2, 3)
155///
156/// # Example
157///
158/// ```
159/// use mscore::chemistry::utility::reshape_prosit_array;
160///
161/// let flat_array = vec![0.0; 174];
162/// let reshaped_array = reshape_prosit_array(flat_array);
163/// assert_eq!(reshaped_array.len(), 29);
164/// assert_eq!(reshaped_array[0].len(), 2);
165/// assert_eq!(reshaped_array[0][0].len(), 3);
166/// ```
167pub fn reshape_prosit_array(flat_array: Vec<f64>) -> Vec<Vec<Vec<f64>>> {
168    let mut array_return: Vec<Vec<Vec<f64>>> = vec![vec![vec![0.0; 3]; 2]; 29];
169    let mut ptr = 0;
170
171    for c in 0..3 {
172        for row in 0..29 {
173            // Fill in the Y ion values
174            array_return[row][0][c] = flat_array[ptr];
175            ptr += 1;
176        }
177        for row in 0..29 {
178            // Fill in the B ion values
179            array_return[row][1][c] = flat_array[ptr];
180            ptr += 1;
181        }
182    }
183
184    array_return
185}