Skip to main content

locus_core/
strategy.rs

1//! Pluggable decoding strategies for different SNR conditions.
2//!
3//! This module abstracts the process of converting sampled pixel intensities
4//! into tag IDs. It provides:
5//! - **Hard-Decision**: Fastest mode using binary thresholds and Hamming distance.
6//! - **Soft-Decision**: High-recall mode using Log-Likelihood Ratios (LLRs).
7
8use crate::decoder::TagDecoder;
9
10/// Trait abstracting the decoding strategy (Hard vs Soft).
11pub trait DecodingStrategy: Send + Sync + 'static {
12    /// The type of code extracted from the image (e.g., u64 bits or `Vec<i16>` LLRs).
13    type Code: Clone + std::fmt::Debug + Send + Sync;
14
15    /// Convert intensities and thresholds into a code.
16    fn from_intensities(intensities: &[f64], thresholds: &[f64]) -> Self::Code;
17
18    /// Compute the "distance" between the extracted code and a dictionary target.
19    ///
20    /// For Hard decoding, this is Hamming distance.
21    /// For Soft decoding, this is the accumulated penalty of mismatching LLRs.
22    fn distance(code: &Self::Code, target: u64) -> u32;
23
24    /// Decode the code into an ID using the provided decoder.
25    fn decode(
26        code: &Self::Code,
27        decoder: &(impl TagDecoder + ?Sized),
28        max_error: u32,
29    ) -> Option<(u32, u32, u8)>;
30
31    /// Convert the code to a debug bitstream (u64).
32    fn to_debug_bits(code: &Self::Code) -> u64;
33}
34
35/// Hard-decision strategy (Hamming distance).
36pub struct HardStrategy;
37
38impl DecodingStrategy for HardStrategy {
39    type Code = u64;
40
41    fn from_intensities(intensities: &[f64], thresholds: &[f64]) -> Self::Code {
42        let mut bits = 0u64;
43        for (i, (&val, &thresh)) in intensities.iter().zip(thresholds.iter()).enumerate() {
44            if val > thresh {
45                bits |= 1 << i;
46            }
47        }
48        bits
49    }
50
51    fn distance(code: &Self::Code, target: u64) -> u32 {
52        (*code ^ target).count_ones()
53    }
54
55    fn decode(
56        code: &Self::Code,
57        decoder: &(impl TagDecoder + ?Sized),
58        max_error: u32,
59    ) -> Option<(u32, u32, u8)> {
60        decoder.decode_full(*code, max_error)
61    }
62
63    fn to_debug_bits(code: &Self::Code) -> u64 {
64        *code
65    }
66}
67
68/// Soft-decision strategy (Log-Likelihood Ratios).
69pub struct SoftStrategy;
70
71/// A stack-allocated buffer for Log-Likelihood Ratios (LLRs).
72#[derive(Clone, Debug)]
73pub struct SoftCode {
74    /// The LLR values for each sample point.
75    pub llrs: [i16; 64],
76    /// The number of valid LLRs (usually dimension^2).
77    pub len: usize,
78}
79
80impl SoftStrategy {
81    #[inline]
82    fn distance_with_limit(code: &SoftCode, target: u64, limit: u32) -> u32 {
83        let mut penalty = 0u32;
84        let n = code.len;
85
86        for i in 0..n {
87            let target_bit = (target >> i) & 1;
88            let llr = code.llrs[i];
89
90            if target_bit == 1 {
91                if llr < 0 {
92                    penalty += u32::from(llr.unsigned_abs());
93                }
94            } else if llr > 0 {
95                penalty += u32::from(llr.unsigned_abs());
96            }
97
98            if penalty >= limit {
99                return limit;
100            }
101        }
102        penalty
103    }
104}
105
106impl DecodingStrategy for SoftStrategy {
107    type Code = SoftCode;
108
109    fn from_intensities(intensities: &[f64], thresholds: &[f64]) -> Self::Code {
110        let n = intensities.len().min(64);
111        let mut llrs = [0i16; 64];
112        for i in 0..n {
113            llrs[i] = (intensities[i] - thresholds[i]) as i16;
114        }
115        SoftCode { llrs, len: n }
116    }
117
118    fn distance(code: &Self::Code, target: u64) -> u32 {
119        Self::distance_with_limit(code, target, u32::MAX)
120    }
121
122    fn decode(
123        code: &Self::Code,
124        decoder: &(impl TagDecoder + ?Sized),
125        max_error: u32,
126    ) -> Option<(u32, u32, u8)> {
127        // Fast Path: Try hard-decoding first
128        let bits = Self::to_debug_bits(code);
129        if let Some((id, hamming, rot)) = decoder.decode_full(bits, 0) {
130            return Some((id, hamming, rot));
131        }
132
133        let _codes_count = decoder.num_codes();
134        let soft_threshold = max_error.max(1) * 60;
135
136        let mut best_id = None;
137        let mut best_dist = soft_threshold;
138        let mut best_rot = 0;
139
140        for &(target_code, id, rot) in decoder.rotated_codes() {
141            let dist = Self::distance_with_limit(code, target_code, best_dist);
142            if dist < best_dist {
143                best_dist = dist;
144                best_id = Some(u32::from(id));
145                best_rot = rot;
146
147                if best_dist == 0 {
148                    return Some((u32::from(id), 0, rot));
149                }
150            }
151        }
152
153        if best_dist < soft_threshold {
154            return best_id.map(|id| {
155                let equiv_hamming = best_dist / 60;
156                (id, equiv_hamming, best_rot)
157            });
158        }
159
160        None
161    }
162
163    fn to_debug_bits(code: &Self::Code) -> u64 {
164        let mut bits = 0u64;
165        for i in 0..code.len {
166            if code.llrs[i] > 0 {
167                bits |= 1 << i;
168            }
169        }
170        bits
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_hard_distance() {
180        let code = 0b1010;
181        let target = 0b1100;
182        // Diff: 0b0110 -> 2 bits
183        assert_eq!(HardStrategy::distance(&code, target), 2);
184    }
185
186    #[test]
187    fn test_soft_from_intensities() {
188        let intensities = vec![100.0, 50.0, 200.0];
189        let thresholds = vec![80.0, 60.0, 150.0];
190        // LLR: 20, -10, 50
191        let code = SoftStrategy::from_intensities(&intensities, &thresholds);
192        assert_eq!(code.llrs[0], 20);
193        assert_eq!(code.llrs[1], -10);
194        assert_eq!(code.llrs[2], 50);
195        assert_eq!(code.len, 3);
196    }
197
198    #[test]
199    fn test_soft_distance() {
200        // Code: [20, -10, 50] (Strong 1, Weak 0, Strong 1)
201        let code = SoftCode {
202            llrs: {
203                let mut l = [0i16; 64];
204                l[0] = 20;
205                l[1] = -10;
206                l[2] = 50;
207                l
208            },
209            len: 3,
210        };
211
212        // Target 1: 1 0 1 (binary 5) -> 0b101
213        // i=0 (20) -> target 1 -> Penalty 0 (match)
214        // i=1 (-10) -> target 0 -> Penalty 0 (match)
215        // i=2 (50) -> target 1 -> Penalty 0 (match)
216        assert_eq!(SoftStrategy::distance(&code, 0b101), 0);
217
218        // Target 2: 0 1 0 (binary 2) -> 0b010
219        // i=0 (20) -> target 0 -> Penalty 20 (mismatch)
220        // i=1 (-10) -> target 1 -> Penalty 10 (mismatch)
221        // i=2 (50) -> target 0 -> Penalty 50 (mismatch)
222        // Total: 80
223        assert_eq!(SoftStrategy::distance(&code, 0b010), 80);
224    }
225
226    #[test]
227    fn test_to_debug_bits() {
228        let code = SoftCode {
229            llrs: {
230                let mut l = [0i16; 64];
231                l[0] = 20;
232                l[1] = -10;
233                l[2] = 50;
234                l
235            },
236            len: 3,
237        };
238        // >0 -> 1, <=0 -> 0
239        // 1 0 1 -> 5
240        assert_eq!(SoftStrategy::to_debug_bits(&code), 5);
241    }
242}