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;
9use multiversion::multiversion;
10
11/// Trait abstracting the decoding strategy (Hard vs Soft).
12pub trait DecodingStrategy: Send + Sync + 'static {
13    /// The type of code extracted from the image (e.g., u64 bits or `Vec<i16>` LLRs).
14    type Code: Clone + std::fmt::Debug + Send + Sync;
15
16    /// Convert intensities and thresholds into a code.
17    fn from_intensities(intensities: &[f64], thresholds: &[f64]) -> Self::Code;
18
19    /// Compute the "distance" between the extracted code and a dictionary target.
20    ///
21    /// For Hard decoding, this is Hamming distance.
22    /// For Soft decoding, this is the accumulated penalty of mismatching LLRs.
23    fn distance(code: &Self::Code, target: u64) -> u32;
24
25    /// Decode the code into an ID using the provided decoder.
26    fn decode(
27        code: &Self::Code,
28        decoder: &(impl TagDecoder + ?Sized),
29        max_error: u32,
30    ) -> Option<(u32, u32, u8)>;
31
32    /// Convert the code to a debug bitstream (u64).
33    fn to_debug_bits(code: &Self::Code) -> u64;
34}
35
36/// Hard-decision strategy (Hamming distance).
37pub struct HardStrategy;
38
39impl DecodingStrategy for HardStrategy {
40    type Code = u64;
41
42    fn from_intensities(intensities: &[f64], thresholds: &[f64]) -> Self::Code {
43        let mut bits = 0u64;
44        for (i, (&val, &thresh)) in intensities.iter().zip(thresholds.iter()).enumerate() {
45            if val > thresh {
46                bits |= 1 << i;
47            }
48        }
49        bits
50    }
51
52    fn distance(code: &Self::Code, target: u64) -> u32 {
53        (*code ^ target).count_ones()
54    }
55
56    fn decode(
57        code: &Self::Code,
58        decoder: &(impl TagDecoder + ?Sized),
59        max_error: u32,
60    ) -> Option<(u32, u32, u8)> {
61        decoder.decode_full(*code, max_error)
62    }
63
64    fn to_debug_bits(code: &Self::Code) -> u64 {
65        *code
66    }
67}
68
69/// Soft-decision strategy (Log-Likelihood Ratios).
70pub struct SoftStrategy;
71
72/// A stack-allocated buffer for Log-Likelihood Ratios (LLRs).
73#[derive(Clone, Debug)]
74pub struct SoftCode {
75    /// The LLR values for each sample point.
76    pub llrs: [i16; 64],
77    /// The number of valid LLRs (usually dimension^2).
78    pub len: usize,
79}
80
81impl SoftStrategy {
82    #[multiversion(targets(
83        "x86_64+avx2+bmi1+bmi2+popcnt+lzcnt",
84        "x86_64+avx512f+avx512bw+avx512dq+avx512vl",
85        "aarch64+neon"
86    ))]
87    fn distance_with_limit(code: &SoftCode, target: u64, limit: u32) -> u32 {
88        let mut penalty = 0u32;
89        let n = code.len;
90
91        // SIMD branch: Process 16 LLRs at a time if possible
92        let chunks = code.llrs[..n].chunks_exact(16);
93        let processed = chunks.len() * 16;
94
95        for (chunk_idx, llr_chunk) in chunks.enumerate() {
96            let target_chunk = (target >> (chunk_idx * 16)) as u16;
97            for (i, &llr) in llr_chunk.iter().enumerate() {
98                let target_bit = (target_chunk >> i) & 1;
99                if target_bit == 1 {
100                    if llr < 0 {
101                        penalty += u32::from(llr.unsigned_abs());
102                    }
103                } else if llr > 0 {
104                    penalty += u32::from(llr.unsigned_abs());
105                }
106            }
107            if penalty >= limit {
108                return limit;
109            }
110        }
111
112        // Tail processing
113        for i in processed..n {
114            let target_bit = (target >> i) & 1;
115            let llr = code.llrs[i];
116
117            if target_bit == 1 {
118                if llr < 0 {
119                    penalty += u32::from(llr.unsigned_abs());
120                }
121            } else if llr > 0 {
122                penalty += u32::from(llr.unsigned_abs());
123            }
124
125            if penalty >= limit {
126                return limit;
127            }
128        }
129        penalty
130    }
131}
132
133impl DecodingStrategy for SoftStrategy {
134    type Code = SoftCode;
135
136    fn from_intensities(intensities: &[f64], thresholds: &[f64]) -> Self::Code {
137        let n = intensities.len().min(64);
138        let mut llrs = [0i16; 64];
139        for i in 0..n {
140            llrs[i] = (intensities[i] - thresholds[i]) as i16;
141        }
142        SoftCode { llrs, len: n }
143    }
144
145    fn distance(code: &Self::Code, target: u64) -> u32 {
146        Self::distance_with_limit(code, target, u32::MAX)
147    }
148
149    fn decode(
150        code: &Self::Code,
151        decoder: &(impl TagDecoder + ?Sized),
152        max_error: u32,
153    ) -> Option<(u32, u32, u8)> {
154        // Fast Path: Try hard-decoding first
155        let bits = Self::to_debug_bits(code);
156        if let Some((id, hamming, rot)) = decoder.decode_full(bits, 0) {
157            return Some((id, hamming, rot));
158        }
159
160        let _codes_count = decoder.num_codes();
161
162        // Scale factor mapping a Hamming distance (integer bit-flips) to the
163        // equivalent total LLR penalty. Derived from the typical saturated LLR
164        // magnitude (~60 per bit for 8-bit image gradients).
165        let llr_per_hamming_bit = 60_u32;
166
167        let soft_threshold = max_error.max(1) * llr_per_hamming_bit;
168
169        // Coarse rejection ratio: candidates beyond 2x the Hamming budget are
170        // pruned before the expensive soft distance computation.
171        let coarse_rejection_threshold = max_error * 2;
172        let mut best_id = None;
173        let mut best_dist = soft_threshold;
174        let mut best_rot = 0;
175
176        decoder.for_each_candidate_within_hamming(
177            bits,
178            coarse_rejection_threshold,
179            &mut |target_code, id, rot| {
180                let dist = Self::distance_with_limit(code, target_code, best_dist);
181                if dist < best_dist {
182                    best_dist = dist;
183                    best_id = Some(u32::from(id));
184                    best_rot = rot;
185                }
186            },
187        );
188
189        if best_dist < soft_threshold {
190            return best_id.map(|id| {
191                let equiv_hamming = best_dist / llr_per_hamming_bit;
192                (id, equiv_hamming, best_rot)
193            });
194        }
195
196        None
197    }
198
199    fn to_debug_bits(code: &Self::Code) -> u64 {
200        let mut bits = 0u64;
201        for i in 0..code.len {
202            if code.llrs[i] > 0 {
203                bits |= 1 << i;
204            }
205        }
206        bits
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_hard_distance() {
216        let code = 0b1010;
217        let target = 0b1100;
218        // Diff: 0b0110 -> 2 bits
219        assert_eq!(HardStrategy::distance(&code, target), 2);
220    }
221
222    #[test]
223    fn test_soft_from_intensities() {
224        let intensities = vec![100.0, 50.0, 200.0];
225        let thresholds = vec![80.0, 60.0, 150.0];
226        // LLR: 20, -10, 50
227        let code = SoftStrategy::from_intensities(&intensities, &thresholds);
228        assert_eq!(code.llrs[0], 20);
229        assert_eq!(code.llrs[1], -10);
230        assert_eq!(code.llrs[2], 50);
231        assert_eq!(code.len, 3);
232    }
233
234    #[test]
235    fn test_soft_distance() {
236        // Code: [20, -10, 50] (Strong 1, Weak 0, Strong 1)
237        let code = SoftCode {
238            llrs: {
239                let mut l = [0i16; 64];
240                l[0] = 20;
241                l[1] = -10;
242                l[2] = 50;
243                l
244            },
245            len: 3,
246        };
247
248        // Target 1: 1 0 1 (binary 5) -> 0b101
249        // i=0 (20) -> target 1 -> Penalty 0 (match)
250        // i=1 (-10) -> target 0 -> Penalty 0 (match)
251        // i=2 (50) -> target 1 -> Penalty 0 (match)
252        assert_eq!(SoftStrategy::distance(&code, 0b101), 0);
253
254        // Target 2: 0 1 0 (binary 2) -> 0b010
255        // i=0 (20) -> target 0 -> Penalty 20 (mismatch)
256        // i=1 (-10) -> target 1 -> Penalty 10 (mismatch)
257        // i=2 (50) -> target 0 -> Penalty 50 (mismatch)
258        // Total: 80
259        assert_eq!(SoftStrategy::distance(&code, 0b010), 80);
260    }
261
262    #[test]
263    fn test_to_debug_bits() {
264        let code = SoftCode {
265            llrs: {
266                let mut l = [0i16; 64];
267                l[0] = 20;
268                l[1] = -10;
269                l[2] = 50;
270                l
271            },
272            len: 3,
273        };
274        // >0 -> 1, <=0 -> 0
275        // 1 0 1 -> 5
276        assert_eq!(SoftStrategy::to_debug_bits(&code), 5);
277    }
278}