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
7pub 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#[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#[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}