Skip to main content

ciri_toolkit/
index_compare.rs

1//! Splice-signal lookup used by Scan1.
2//!
3//! The functions in this module search short flanking windows for canonical and
4//! semi-canonical splice motifs and return Java-compatible payload strings.
5
6use std::collections::BTreeMap;
7
8/// Splice-signal comparator for BSJ flanking sequences.
9///
10/// A `BTreeMap` is used deliberately so shift offsets are iterated in ascending
11/// order, matching the ordering assumptions in later validator logic.
12pub struct IndexCompare;
13
14impl IndexCompare {
15    /// Compares flanking sequences for nuclear-chromosome splice motifs.
16    ///
17    /// - `end_string1`: Sequence flanking the predicted upstream site (acceptor side).
18    /// - `end_string2`: Sequence flanking the predicted downstream site (donor side).
19    ///
20    /// Returns a map of shift offsets to a formatted string containing:
21    /// `offset, strand, donor_signal, acceptor_signal`.
22    pub fn index_compare(end_string1: &str, end_string2: &str) -> BTreeMap<i32, String> {
23        let mut index_strand_map = BTreeMap::new();
24        let up_s1 = end_string1.to_uppercase();
25        let up_s2 = end_string2.to_uppercase();
26
27        // Canonical splicing motifs:
28        // For '+' strand: Donor is GT, Acceptor is AG.
29        // For '-' strand: Donor (complement) is CT, Acceptor (complement) is AC.
30        // bibases[0] contains upstream motifs, bibases[1] contains downstream motifs.
31        let bibases = [
32            ["AC", "AG"], // motifs for '-' and '+' strands respectively
33            ["CT", "GT"],
34        ];
35        let strand_index = ["-", "+"];
36
37        for i in 0..=1 {
38            let mut pre_index: i32 = -1;
39            loop {
40                let target = bibases[0][i];
41                let search_start = (pre_index + 1) as usize;
42                if search_start >= up_s1.len() {
43                    break;
44                }
45
46                // Search for the first motif in the upstream flanking sequence.
47                if let Some(index) = up_s1[search_start..].find(target) {
48                    let actual_index = (search_start + index) as i32;
49                    // Verify if the matching second motif exists in the downstream sequence at the same relative position.
50                    if up_s2.len() >= (actual_index + 2) as usize {
51                        let s2_part = &up_s2[actual_index as usize..(actual_index + 2) as usize];
52                        if s2_part.eq_ignore_ascii_case(bibases[1][i]) {
53                            // Motif pair found! Store it with the relative shift (offset).
54                            index_strand_map.insert(
55                                actual_index,
56                                format!(
57                                    "{}\t{}\t{}\t{}",
58                                    actual_index, strand_index[i], target, bibases[1][i]
59                                ),
60                            );
61                        }
62                    } else {
63                        break;
64                    }
65                    pre_index = actual_index;
66                } else {
67                    break;
68                }
69            }
70        }
71        index_strand_map
72    }
73
74    /// Compares flanking sequences for mitochondrial or otherwise relaxed motif sets.
75    ///
76    /// Mitochondrial handling intentionally accepts a wider motif palette to match
77    /// the original CIRI3 logic.
78    pub fn index_compare_chrm(end_string1: &str, end_string2: &str) -> BTreeMap<i32, String> {
79        let mut index_strand_map = BTreeMap::new();
80        let up_s1 = end_string1.to_uppercase();
81        let up_s2 = end_string2.to_uppercase();
82
83        // Expanded set of splicing motifs for mitochondrial DNA or specialized cases.
84        // Includes non-canonical pairs like GC/AG, AT/AC, etc.
85        let bibases_mut = [
86            ["AC", "AG", "GC", "AG", "AT", "AC", "AT", "AG"],
87            ["CT", "GT", "CT", "GC", "GT", "AT", "CT", "AT"],
88        ];
89        let strand_index = ["-", "+"];
90
91        for i in 0..=7 {
92            let mut pre_index: i32 = -1;
93            let strand = i % 2; // Alternates between '-' (even i) and '+' (odd i) strands in this array.
94            loop {
95                let target = bibases_mut[0][i];
96                let search_start = (pre_index + 1) as usize;
97                if search_start >= up_s1.len() {
98                    break;
99                }
100
101                if let Some(index) = up_s1[search_start..].find(target) {
102                    let actual_index = (search_start + index) as i32;
103                    if up_s2.len() >= (actual_index + 2) as usize {
104                        let s2_part = &up_s2[actual_index as usize..(actual_index + 2) as usize];
105                        if s2_part.eq_ignore_ascii_case(bibases_mut[1][i]) {
106                            index_strand_map.insert(
107                                actual_index,
108                                format!(
109                                    "{}\t{}\t{}\t{}",
110                                    actual_index, strand_index[strand], target, bibases_mut[1][i]
111                                ),
112                            );
113                        }
114                    } else {
115                        break;
116                    }
117                    pre_index = actual_index;
118                } else {
119                    break;
120                }
121            }
122        }
123        index_strand_map
124    }
125}