Skip to main content

rustalign_aligner/
result.rs

1//! Alignment result types
2
3use rustalign_common::{Nuc, Score, Strand};
4
5/// A single alignment result
6#[derive(Debug, Clone, Default)]
7pub struct Alignment {
8    /// BWT row position (unresolved genome coordinate)
9    pub bwt_row: u64,
10
11    /// Offset of the seed within the read
12    pub seed_offset: usize,
13
14    /// Starting position in reference
15    pub ref_start: usize,
16
17    /// Ending position in reference
18    pub ref_end: usize,
19
20    /// Alignment score
21    pub score: Score,
22
23    /// Strand (forward or reverse)
24    pub strand: Strand,
25
26    /// Number of edits (mismatches + indels)
27    pub edits: usize,
28
29    /// CIGAR string
30    pub cigar: String,
31
32    /// MD:Z tag (mismatch string)
33    pub md: String,
34
35    /// Edit operations
36    pub edit_ops: Vec<EditOp>,
37
38    /// Number of genome positions the seed matched (lower = more unique)
39    pub seed_hit_count: usize,
40}
41
42impl Alignment {
43    /// Create a new alignment
44    pub fn new(ref_start: usize, ref_end: usize, score: Score, strand: Strand) -> Self {
45        Self {
46            ref_start,
47            ref_end,
48            score,
49            strand,
50            ..Default::default()
51        }
52    }
53}
54
55/// Edit operation in alignment
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum EditOp {
58    /// Match
59    Match,
60    /// Mismatch
61    Mismatch { ref_nuc: Nuc, read_nuc: Nuc },
62    /// Insertion (relative to reference)
63    Insertion(Nuc),
64    /// Deletion (relative to reference)
65    Deletion(Nuc),
66}
67
68/// Result of aligning a read
69#[derive(Debug, Clone, Default)]
70pub struct AlignmentResult {
71    /// All alignments found
72    pub alignments: Vec<Alignment>,
73
74    /// Whether alignment was successful
75    pub success: bool,
76
77    /// Error message if alignment failed
78    pub error: Option<String>,
79}
80
81impl AlignmentResult {
82    /// Create a new empty result
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Get the best alignment (highest score, preferring more unique seeds,
88    /// then earlier seed position, then forward strand on ties)
89    pub fn best(&self) -> Option<&Alignment> {
90        // Use a stable comparison that prefers:
91        // 1. Higher score
92        // 2. Lower seed_hit_count (more unique seed)
93        // 3. Lower seed_offset (seed earlier in the read)
94        // 4. Forward strand on ties
95        self.alignments.iter().max_by(|a, b| {
96            match a.score.cmp(&b.score) {
97                std::cmp::Ordering::Equal => {
98                    // Prefer lower hit count (more unique seed)
99                    match b.seed_hit_count.cmp(&a.seed_hit_count) {
100                        std::cmp::Ordering::Equal => {
101                            // Prefer lower seed_offset (seed earlier in the read)
102                            match b.seed_offset.cmp(&a.seed_offset) {
103                                std::cmp::Ordering::Equal => {
104                                    // On tie, prefer forward strand
105                                    match (a.strand, b.strand) {
106                                        (Strand::Forward, Strand::Reverse) => {
107                                            std::cmp::Ordering::Greater
108                                        }
109                                        (Strand::Reverse, Strand::Forward) => {
110                                            std::cmp::Ordering::Less
111                                        }
112                                        _ => std::cmp::Ordering::Equal,
113                                    }
114                                }
115                                other => other,
116                            }
117                        }
118                        other => other,
119                    }
120                }
121                other => other,
122            }
123        })
124    }
125
126    /// Number of alignments
127    pub fn len(&self) -> usize {
128        self.alignments.len()
129    }
130
131    /// Check if no alignments were found
132    pub fn is_empty(&self) -> bool {
133        self.alignments.is_empty()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_alignment_new() {
143        let aln = Alignment::new(100, 120, 50, Strand::Forward);
144        assert_eq!(aln.ref_start, 100);
145        assert_eq!(aln.ref_end, 120);
146        assert_eq!(aln.score, 50);
147    }
148
149    #[test]
150    fn test_result_empty() {
151        let result = AlignmentResult::new();
152        assert!(result.is_empty());
153        assert!(result.best().is_none());
154    }
155}