Skip to main content

orphos_core/sequence/
processing.rs

1use super::*;
2use crate::constants::{
3    MAX_MOTIF_LENGTH, MAX_RIBOSOME_DISTANCE, MIN_CUMULATIVE_SCORE, MIN_DISTANCE_FROM_START,
4    MIN_MOTIF_LENGTH, READING_FRAMES, SLIDING_WINDOW_SIZE,
5};
6
7/// Calculate most GC-rich frame for each position
8pub fn calc_most_gc_frame(sequence: &[u8], sequence_length: usize) -> Vec<i32> {
9    if sequence_length < READING_FRAMES {
10        return vec![-1; sequence_length];
11    }
12
13    let forward_gc_counts = calculate_forward_gc_counts(sequence, sequence_length);
14    let backward_gc_counts = calculate_backward_gc_counts(sequence, sequence_length);
15    let total_gc_counts = calculate_total_gc_counts(
16        &forward_gc_counts,
17        &backward_gc_counts,
18        sequence,
19        sequence_length,
20    );
21
22    assign_gc_rich_frames(&total_gc_counts, sequence_length)
23}
24
25fn calculate_forward_gc_counts(sequence: &[u8], sequence_length: usize) -> Vec<i32> {
26    let mut counts = vec![0; sequence_length];
27
28    for reading_frame in 0..READING_FRAMES {
29        for position in (reading_frame..sequence_length).step_by(READING_FRAMES) {
30            counts[position] = if position < READING_FRAMES {
31                i32::from(is_gc(sequence, position))
32            } else {
33                counts[position - READING_FRAMES] + i32::from(is_gc(sequence, position))
34            };
35        }
36    }
37
38    counts
39}
40
41fn calculate_backward_gc_counts(sequence: &[u8], sequence_length: usize) -> Vec<i32> {
42    let mut counts = vec![0; sequence_length];
43
44    for reading_frame in 0..READING_FRAMES {
45        for position in (reading_frame..sequence_length).step_by(READING_FRAMES) {
46            let reverse_position = sequence_length - position - 1;
47            counts[reverse_position] = if position < READING_FRAMES {
48                i32::from(is_gc(sequence, reverse_position))
49            } else {
50                counts[reverse_position + READING_FRAMES]
51                    + i32::from(is_gc(sequence, reverse_position))
52            };
53        }
54    }
55
56    counts
57}
58
59fn calculate_total_gc_counts(
60    forward_counts: &[i32],
61    backward_counts: &[i32],
62    sequence: &[u8],
63    sequence_length: usize,
64) -> Vec<i32> {
65    (0..sequence_length)
66        .map(|position| {
67            let mut total = forward_counts[position] + backward_counts[position]
68                - i32::from(is_gc(sequence, position));
69
70            if position >= SLIDING_WINDOW_SIZE / 2 {
71                total -= forward_counts[position - SLIDING_WINDOW_SIZE / 2];
72            }
73
74            if position + SLIDING_WINDOW_SIZE / 2 < sequence_length {
75                total -= backward_counts[position + SLIDING_WINDOW_SIZE / 2];
76            }
77
78            total
79        })
80        .collect()
81}
82
83fn assign_gc_rich_frames(total_gc_counts: &[i32], sequence_length: usize) -> Vec<i32> {
84    let mut gc_rich_frames = vec![-1; sequence_length];
85
86    for triplet_start in (0..sequence_length.saturating_sub(2)).step_by(READING_FRAMES) {
87        let counts = [
88            total_gc_counts[triplet_start],
89            total_gc_counts.get(triplet_start + 1).copied().unwrap_or(0),
90            total_gc_counts.get(triplet_start + 2).copied().unwrap_or(0),
91        ];
92
93        let max_gc_frame = find_max_reading_frame(counts[0], counts[1], counts[2]) as i32;
94
95        for frame_offset in 0..READING_FRAMES.min(sequence_length - triplet_start) {
96            gc_rich_frames[triplet_start + frame_offset] = max_gc_frame;
97        }
98    }
99
100    gc_rich_frames
101}
102
103/// Find exact Shine-Dalgarno motifs
104#[must_use]
105pub fn shine_dalgarno_exact(
106    sequence: &[u8],
107    search_position: usize,
108    start_codon_position: usize,
109    ribosome_weights: &[f64],
110) -> usize {
111    if start_codon_position <= search_position + MIN_DISTANCE_FROM_START {
112        return 0;
113    }
114
115    let search_limit =
116        MAX_MOTIF_LENGTH.min(start_codon_position - MIN_DISTANCE_FROM_START - search_position);
117    let base_scores = calculate_exact_base_scores(sequence, search_position, search_limit);
118
119    find_best_exact_motif(
120        &base_scores,
121        search_limit,
122        search_position,
123        start_codon_position,
124        ribosome_weights,
125    )
126}
127
128fn calculate_exact_base_scores(
129    sequence: &[u8],
130    search_position: usize,
131    search_limit: usize,
132) -> [f64; MAX_MOTIF_LENGTH] {
133    let mut base_scores = [0.0; MAX_MOTIF_LENGTH];
134
135    for (pattern_index, score) in base_scores.iter_mut().take(search_limit).enumerate() {
136        let sequence_position = search_position + pattern_index;
137        *score = match pattern_index % 3 {
138            0 if is_a(sequence, sequence_position) => 2.0,
139            1 | 2 if is_g(sequence, sequence_position) => 3.0,
140            _ => -10.0,
141        };
142    }
143
144    base_scores
145}
146
147fn find_best_exact_motif(
148    base_scores: &[f64],
149    search_limit: usize,
150    search_position: usize,
151    start_codon_position: usize,
152    ribosome_weights: &[f64],
153) -> usize {
154    let mut best_motif_index = 0;
155
156    for motif_length in (MIN_MOTIF_LENGTH..=search_limit).rev() {
157        for motif_start_offset in 0..=(search_limit - motif_length) {
158            if let Some(motif_index) = evaluate_exact_motif(
159                base_scores,
160                motif_start_offset,
161                motif_length,
162                search_position,
163                start_codon_position,
164            ) && is_better_motif(motif_index, best_motif_index, ribosome_weights)
165            {
166                best_motif_index = motif_index;
167            }
168        }
169    }
170
171    best_motif_index
172}
173
174fn evaluate_exact_motif(
175    base_scores: &[f64],
176    motif_start_offset: usize,
177    motif_length: usize,
178    search_position: usize,
179    start_codon_position: usize,
180) -> Option<usize> {
181    let start = motif_start_offset;
182    let end = motif_start_offset + motif_length;
183    let window = &base_scores[start..end];
184
185    if window.iter().any(|&score| score < 0.0) {
186        return None;
187    }
188
189    let cumulative_score: f64 = window.iter().copied().sum::<f64>() - 2.0;
190    let ribosome_distance =
191        start_codon_position - (search_position + motif_start_offset + motif_length);
192
193    if ribosome_distance > MAX_RIBOSOME_DISTANCE || cumulative_score < MIN_CUMULATIVE_SCORE {
194        return None;
195    }
196
197    let distance_category = categorize_distance(ribosome_distance, motif_length);
198    Some(map_score_to_motif_index(
199        cumulative_score as i32,
200        distance_category,
201    ))
202}
203
204/// Find Shine-Dalgarno motifs with single mismatch
205#[must_use]
206pub fn shine_dalgarno_mm(
207    sequence: &[u8],
208    search_position: usize,
209    start_codon_position: usize,
210    ribosome_weights: &[f64],
211) -> usize {
212    if start_codon_position <= search_position + MIN_DISTANCE_FROM_START {
213        return 0;
214    }
215
216    let search_limit =
217        MAX_MOTIF_LENGTH.min(start_codon_position - MIN_DISTANCE_FROM_START - search_position);
218    let base_scores = calculate_mismatch_base_scores(sequence, search_position, search_limit);
219
220    find_best_mismatch_motif(
221        &base_scores,
222        search_limit,
223        search_position,
224        start_codon_position,
225        ribosome_weights,
226    )
227}
228
229fn calculate_mismatch_base_scores(
230    sequence: &[u8],
231    search_position: usize,
232    search_limit: usize,
233) -> [f64; MAX_MOTIF_LENGTH] {
234    let mut base_scores = [0.0; MAX_MOTIF_LENGTH];
235
236    for (pattern_index, score) in base_scores.iter_mut().take(search_limit).enumerate() {
237        let sequence_position = search_position + pattern_index;
238        *score = match pattern_index % 3 {
239            0 if is_a(sequence, sequence_position) => 2.0,
240            0 => -3.0,
241            _ if is_g(sequence, sequence_position) => 3.0,
242            _ => -2.0,
243        };
244    }
245
246    base_scores
247}
248
249fn find_best_mismatch_motif(
250    base_scores: &[f64],
251    search_limit: usize,
252    search_position: usize,
253    start_codon_position: usize,
254    ribosome_weights: &[f64],
255) -> usize {
256    let mut best_motif_index = 0;
257
258    for motif_length in (5..=search_limit).rev() {
259        for motif_start_offset in 0..=(search_limit - motif_length) {
260            if let Some(motif_index) = evaluate_mismatch_motif(
261                base_scores,
262                motif_start_offset,
263                motif_length,
264                search_position,
265                start_codon_position,
266            ) && is_better_motif(motif_index, best_motif_index, ribosome_weights)
267            {
268                best_motif_index = motif_index;
269            }
270        }
271    }
272
273    best_motif_index
274}
275
276fn evaluate_mismatch_motif(
277    base_scores: &[f64],
278    motif_start_offset: usize,
279    motif_length: usize,
280    search_position: usize,
281    start_codon_position: usize,
282) -> Option<usize> {
283    let mut cumulative_score = -2.0;
284    let mut mismatch_count = 0;
285
286    let start = motif_start_offset;
287    let end = motif_start_offset + motif_length;
288    for (pos_in_motif, &score) in base_scores[start..end].iter().enumerate() {
289        cumulative_score += score;
290        if score < 0.0 {
291            mismatch_count += 1;
292            if pos_in_motif <= 1 || pos_in_motif >= motif_length - 2 {
293                cumulative_score -= 10.0;
294            }
295        }
296    }
297
298    if mismatch_count != 1 {
299        return None;
300    }
301
302    let ribosome_distance =
303        start_codon_position - (search_position + motif_start_offset + motif_length);
304
305    if ribosome_distance > MAX_RIBOSOME_DISTANCE || cumulative_score < MIN_CUMULATIVE_SCORE {
306        return None;
307    }
308
309    let distance_category = categorize_mismatch_distance(ribosome_distance);
310    Some(map_mismatch_score_to_motif_index(
311        cumulative_score as i32,
312        distance_category,
313    ))
314}
315
316const fn categorize_distance(ribosome_distance: usize, motif_length: usize) -> usize {
317    match ribosome_distance {
318        0..=4 => {
319            if motif_length < 5 {
320                2
321            } else {
322                1
323            }
324        }
325        5..=10 => 0,
326        11..=12 => {
327            if motif_length < 5 {
328                1
329            } else {
330                2
331            }
332        }
333        _ => 3,
334    }
335}
336
337const fn categorize_mismatch_distance(ribosome_distance: usize) -> usize {
338    match ribosome_distance {
339        0..=4 => 1,
340        5..=10 => 0,
341        11..=12 => 2,
342        _ => 3,
343    }
344}
345
346const fn map_score_to_motif_index(score: i32, distance_category: usize) -> usize {
347    match (score, distance_category) {
348        (6, 2) => 1,
349        (6, 3) => 2,
350        (8 | 9, 3) => 3,
351        (6, 1) => 6,
352        (11 | 12 | 14, 3) => 10,
353        (8 | 9, 2) => 11,
354        (8 | 9, 1) => 12,
355        (6, 0) => 13,
356        (8, 0) => 15,
357        (9, 0) => 16,
358        (11 | 12, 2) => 20,
359        (11, 1) => 21,
360        (11, 0) => 22,
361        (12, 1) => 23,
362        (12, 0) => 24,
363        (14, 2) => 25,
364        (14, 1) => 26,
365        (14, 0) => 27,
366        _ => 0,
367    }
368}
369
370const fn map_mismatch_score_to_motif_index(score: i32, distance_category: usize) -> usize {
371    match (score, distance_category) {
372        (6 | 7, 3) => 2,
373        (9, 3) => 3,
374        (6, 2) => 4,
375        (6, 1) => 5,
376        (6, 0) => 9,
377        (7, 2) => 7,
378        (7, 1) => 8,
379        (7, 0) => 14,
380        (9, 2) => 17,
381        (9, 1) => 18,
382        (9, 0) => 19,
383        _ => 0,
384    }
385}
386
387fn is_better_motif(current_index: usize, best_index: usize, ribosome_weights: &[f64]) -> bool {
388    ribosome_weights[current_index] > ribosome_weights[best_index]
389        || (ribosome_weights[current_index] == ribosome_weights[best_index]
390            && current_index > best_index)
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::sequence::encoded::EncodedSequence;
397
398    #[test]
399    fn test_calc_most_gc_frame_basic() {
400        let sequence = b"ATCGGCGCGCTAATCGGCGC";
401        let result = calc_most_gc_frame(sequence, sequence.len());
402        assert_eq!(result.len(), sequence.len());
403        for &frame in &result {
404            assert!((-1..=2).contains(&frame));
405        }
406    }
407
408    #[test]
409    fn test_calc_most_gc_frame_encoded_sequence() {
410        let encoded = EncodedSequence::without_masking(b"ATCGGCGCGCTAATCGGCGC");
411        let result = calc_most_gc_frame(&encoded.forward_sequence, encoded.sequence_length);
412
413        assert_eq!(result.len(), encoded.sequence_length);
414        for &frame in &result {
415            assert!((-1..=2).contains(&frame));
416        }
417    }
418
419    #[test]
420    fn test_calc_most_gc_frame_empty() {
421        let sequence = b"";
422        let result = calc_most_gc_frame(sequence, 0);
423        assert_eq!(result.len(), 0);
424    }
425
426    #[test]
427    fn test_shine_dalgarno_exact_basic() {
428        let ribosome_weights = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
429        let sequence = b"AGGAGGTATGATCGGC";
430
431        let result = shine_dalgarno_exact(sequence, 5, 12, &ribosome_weights);
432        assert!(result < ribosome_weights.len());
433    }
434
435    #[test]
436    fn test_shine_dalgarno_exact_encoded_sequence() {
437        let ribosome_weights = vec![1.0; 28];
438        let encoded = EncodedSequence::without_masking(b"AGGAGGTATGATCGGC");
439
440        let result = shine_dalgarno_exact(&encoded.forward_sequence, 0, 12, &ribosome_weights);
441        assert!(result < ribosome_weights.len());
442    }
443
444    #[test]
445    fn test_shine_dalgarno_mm_basic() {
446        let ribosome_weights = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
447        let sequence = b"AGGAGGTATGATCGGC";
448
449        let result = shine_dalgarno_mm(sequence, 0, 8, &ribosome_weights);
450        assert!(result < ribosome_weights.len());
451    }
452
453    #[test]
454    fn test_shine_dalgarno_mm_encoded_sequence() {
455        let ribosome_weights = vec![1.0; 28];
456        let encoded = EncodedSequence::without_masking(b"AGGAGGTATGATCGGC");
457
458        let result = shine_dalgarno_mm(&encoded.forward_sequence, 0, 12, &ribosome_weights);
459        assert!(result < ribosome_weights.len());
460    }
461
462    #[test]
463    fn test_assign_gc_rich_frames_basic() {
464        let total_gc_counts = vec![5, 3, 8, 2, 7, 1, 9, 4];
465        let result = assign_gc_rich_frames(&total_gc_counts, 8);
466        assert_eq!(result.len(), 8);
467
468        for &frame in &result {
469            assert!((-1..=2).contains(&frame));
470        }
471    }
472}