1use std::collections::HashMap;
17
18use rand::{RngExt, SeedableRng, rngs::StdRng};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct DecodeKey {
24 pub observable_task: u32,
26 pub attempt: u64,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct MockDecoderConfig {
33 pub acceptance_probability: f64,
35 pub accepted_accuracy: f64,
37 pub rejected_accuracy: f64,
39 pub gap_threshold: f64,
41 pub acceptance_script: Vec<bool>,
45}
46
47impl Default for MockDecoderConfig {
48 fn default() -> Self {
49 Self {
50 acceptance_probability: 0.8,
51 accepted_accuracy: 0.999,
52 rejected_accuracy: 0.9,
53 gap_threshold: 0.5,
54 acceptance_script: Vec::new(),
55 }
56 }
57}
58
59impl MockDecoderConfig {
60 fn validate(&self) -> Result<(), DecoderError> {
61 for (name, value) in [
62 ("acceptance_probability", self.acceptance_probability),
63 ("accepted_accuracy", self.accepted_accuracy),
64 ("rejected_accuracy", self.rejected_accuracy),
65 ] {
66 if !(0.0..=1.0).contains(&value) {
67 return Err(DecoderError::InvalidProbability { name, value });
68 }
69 }
70 if !(0.0 < self.gap_threshold && self.gap_threshold < 1.0) {
71 return Err(DecoderError::InvalidGapThreshold(self.gap_threshold));
72 }
73 Ok(())
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
79pub struct DecoderDecision {
80 pub key: DecodeKey,
82 pub raw: bool,
84 pub flip: bool,
86 pub accepted: bool,
88 pub confidence_gap: f64,
90 pub measurements_ready_at: Option<f64>,
92 pub ready_at: f64,
94}
95
96#[derive(Debug, Clone, PartialEq, thiserror::Error)]
98pub enum DecoderError {
99 #[error("{name} must be in [0, 1], got {value}")]
101 InvalidProbability {
102 name: &'static str,
104 value: f64,
106 },
107 #[error("gap_threshold must be strictly between 0 and 1, got {0}")]
109 InvalidGapThreshold(f64),
110 #[error("invalid decoder timing")]
112 InvalidTiming,
113 #[error("decoder solve {key:?} was requested with inconsistent inputs")]
115 InconsistentRequest {
116 key: DecodeKey,
118 },
119}
120
121#[derive(Debug)]
123pub struct MockStreamingDecoder {
124 config: MockDecoderConfig,
125 seed: u64,
126 latency_rounds: u32,
127 decisions: HashMap<DecodeKey, DecoderDecision>,
128 solves: usize,
129}
130
131impl MockStreamingDecoder {
132 pub fn new(
139 config: MockDecoderConfig,
140 seed: u64,
141 latency_rounds: u32,
142 ) -> Result<Self, DecoderError> {
143 config.validate()?;
144 Ok(Self {
145 config,
146 seed,
147 latency_rounds,
148 decisions: HashMap::new(),
149 solves: 0,
150 })
151 }
152
153 pub fn request(
167 &mut self,
168 key: DecodeKey,
169 raw: bool,
170 measurements_ready_at: Option<f64>,
171 requested_at: f64,
172 round_duration: f64,
173 ) -> Result<DecoderDecision, DecoderError> {
174 if !requested_at.is_finite()
175 || requested_at < 0.0
176 || !round_duration.is_finite()
177 || round_duration < 0.0
178 || measurements_ready_at.is_some_and(|time| !time.is_finite() || time < 0.0)
179 {
180 return Err(DecoderError::InvalidTiming);
181 }
182 let ready_at = self.decisions.get(&key).map_or_else(
183 || match measurements_ready_at {
184 Some(measured_at) => (measured_at
185 + round_duration * f64::from(self.latency_rounds))
186 .max(requested_at),
187 None => requested_at,
188 },
189 |decision| decision.ready_at,
190 );
191 self.request_at(key, raw, measurements_ready_at, ready_at)
192 }
193
194 pub fn request_at(
207 &mut self,
208 key: DecodeKey,
209 raw: bool,
210 measurements_ready_at: Option<f64>,
211 ready_at: f64,
212 ) -> Result<DecoderDecision, DecoderError> {
213 if !ready_at.is_finite()
214 || ready_at < 0.0
215 || measurements_ready_at.is_some_and(|time| !time.is_finite() || time < 0.0)
216 || ready_at < measurements_ready_at.unwrap_or(0.0)
217 {
218 return Err(DecoderError::InvalidTiming);
219 }
220 if let Some(&decision) = self.decisions.get(&key) {
221 if decision.raw != raw
222 || decision.measurements_ready_at != measurements_ready_at
223 || (measurements_ready_at.is_some() && decision.ready_at != ready_at)
224 {
225 return Err(DecoderError::InconsistentRequest { key });
226 }
227 return Ok(decision);
228 }
229
230 let mut rng = StdRng::seed_from_u64(
233 self.seed
234 ^ u64::from(key.observable_task).wrapping_mul(0x9e37_79b9_7f4a_7c15)
235 ^ key.attempt.wrapping_mul(0xbf58_476d_1ce4_e5b9),
236 );
237 let accepted = self
238 .config
239 .acceptance_script
240 .get(self.solves)
241 .copied()
242 .unwrap_or_else(|| rng.random::<f64>() < self.config.acceptance_probability);
243 self.solves += 1;
244 let confidence_gap = if accepted {
245 self.config.gap_threshold + rng.random::<f64>() * (1.0 - self.config.gap_threshold)
246 } else {
247 rng.random::<f64>() * self.config.gap_threshold
248 };
249 let accuracy = if accepted {
250 self.config.accepted_accuracy
251 } else {
252 self.config.rejected_accuracy
253 };
254 let decision = DecoderDecision {
255 key,
256 raw,
257 flip: rng.random::<f64>() >= accuracy,
258 accepted,
259 confidence_gap,
260 measurements_ready_at,
261 ready_at,
262 };
263 self.decisions.insert(key, decision);
264 Ok(decision)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn solve_is_cached_and_uses_configured_latency() {
274 let mut decoder = MockStreamingDecoder::new(
275 MockDecoderConfig {
276 acceptance_script: vec![false, true],
277 accepted_accuracy: 1.0,
278 rejected_accuracy: 1.0,
279 ..MockDecoderConfig::default()
280 },
281 7,
282 2,
283 )
284 .unwrap();
285 let first = decoder
286 .request(
287 DecodeKey {
288 observable_task: 3,
289 attempt: 1,
290 },
291 true,
292 Some(12.0),
293 12.0,
294 6.0,
295 )
296 .unwrap();
297 assert!(!first.accepted);
298 assert_eq!(first.ready_at, 24.0);
299 assert_eq!(
300 decoder
301 .request(first.key, true, Some(12.0), 20.0, 6.0)
302 .unwrap(),
303 first
304 );
305 assert_eq!(
306 decoder
307 .request(first.key, true, Some(12.0), 100.0, 6.0)
308 .unwrap(),
309 first
310 );
311 assert!(
312 decoder
313 .request(
314 DecodeKey {
315 observable_task: 3,
316 attempt: 2,
317 },
318 true,
319 Some(20.0),
320 20.0,
321 6.0,
322 )
323 .unwrap()
324 .accepted
325 );
326 assert_eq!(
327 decoder
328 .request_at(
329 DecodeKey {
330 observable_task: 4,
331 attempt: 9,
332 },
333 false,
334 Some(30.0),
335 90.0,
336 )
337 .unwrap()
338 .ready_at,
339 90.0
340 );
341 assert_eq!(
342 decoder
343 .request(
344 DecodeKey {
345 observable_task: 5,
346 attempt: 0,
347 },
348 false,
349 Some(12.0),
350 100.0,
351 6.0,
352 )
353 .unwrap()
354 .ready_at,
355 100.0
356 );
357 }
358
359 #[test]
360 fn zero_latency_is_immediate_and_timing_overflow_is_typed() {
361 let key = DecodeKey {
362 observable_task: 3,
363 attempt: 1,
364 };
365 let mut immediate = MockStreamingDecoder::new(MockDecoderConfig::default(), 7, 0).unwrap();
366 assert_eq!(
367 immediate
368 .request(key, true, Some(12.0), 12.0, 6.0)
369 .unwrap()
370 .ready_at,
371 12.0
372 );
373
374 let mut overflowing =
375 MockStreamingDecoder::new(MockDecoderConfig::default(), 7, 2).unwrap();
376 assert_eq!(
377 overflowing.request(key, true, Some(12.0), 12.0, f64::MAX),
378 Err(DecoderError::InvalidTiming)
379 );
380 }
381
382 #[test]
383 fn explicit_deadlines_reject_invalid_timestamps_before_sampling() {
384 let key = DecodeKey {
385 observable_task: 0,
386 attempt: 0,
387 };
388 let mut decoder = MockStreamingDecoder::new(
389 MockDecoderConfig {
390 acceptance_script: vec![false, true],
391 ..MockDecoderConfig::default()
392 },
393 7,
394 0,
395 )
396 .unwrap();
397 for invalid in [-1.0, f64::NAN, f64::NEG_INFINITY, f64::INFINITY] {
398 assert_eq!(
399 decoder.request_at(key, false, Some(invalid), 1.0),
400 Err(DecoderError::InvalidTiming)
401 );
402 assert_eq!(
403 decoder.request_at(key, false, None, invalid),
404 Err(DecoderError::InvalidTiming)
405 );
406 }
407 assert_eq!(
408 decoder.request_at(key, false, Some(2.0), 1.0),
409 Err(DecoderError::InvalidTiming)
410 );
411 let decision = decoder.request_at(key, false, Some(0.0), 0.0).unwrap();
412 assert!(!decision.accepted);
413 assert_eq!(
414 decoder.request_at(key, false, Some(0.0), 0.0).unwrap(),
415 decision
416 );
417 }
418}