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}