Skip to main content

bloq_vm/
decoder.rs

1//! Seeded mock streaming decoder for dynamic VM runs.
2//!
3//! This is deliberately a policy model, not a decoder. Each causal solve samples
4//! an acceptance decision and a residual output-bit error. The raw parity is
5//! treated as nominal truth; this model does not infer a physical correction
6//! from syndrome data. Accepted solves default to a 0.1% residual error and
7//! rejected solves to 10%. The confidence GAP is sampled above or below the
8//! configured threshold to match that decision.
9//! A measurement-timed solve becomes ready after the configured number of local
10//! memory rounds following its last input measurement. Factory solves use the
11//! explicit deadline reached after the runtime executes the configured physical
12//! GAP rounds. Repeated requests with the same [`DecodeKey`] return the cached
13//! decision, keeping corrected and flip outputs causally identical. Confidence
14//! acceptance does not itself restart a region; its explicit predicate owns that decision.
15
16use std::collections::HashMap;
17
18use rand::{RngExt, SeedableRng, rngs::StdRng};
19use serde::{Deserialize, Serialize};
20
21/// Identifies one observable solve in one dynamic attempt.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct DecodeKey {
24    /// Dense, globally scoped VM observable task id.
25    pub observable_task: u32,
26    /// Runtime attempt epoch. Zero is the top-level execution.
27    pub attempt: u64,
28}
29
30/// Tunable assumptions for the mock decoder.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct MockDecoderConfig {
33    /// Probability that the confidence policy accepts a solve.
34    pub acceptance_probability: f64,
35    /// Probability that an accepted solve leaves no synthetic residual error.
36    pub accepted_accuracy: f64,
37    /// Probability that a rejected solve leaves no synthetic residual error.
38    pub rejected_accuracy: f64,
39    /// Confidence GAP at which policy acceptance begins.
40    pub gap_threshold: f64,
41    /// Optional global deterministic solve prefix for retry tests and examples.
42    /// Unlike seeded random draws, this deliberate override follows execution
43    /// order across factories.
44    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/// One inspectable mock-decoder result.
78#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
79pub struct DecoderDecision {
80    /// Observable and attempt sharing this causal solve.
81    pub key: DecodeKey,
82    /// Raw observable parity supplied to the decoder.
83    pub raw: bool,
84    /// Synthetic residual decoder error. XOR this with `raw`.
85    pub flip: bool,
86    /// Sampled confidence-policy acceptance; diagnostic, not a retry trigger.
87    pub accepted: bool,
88    /// Sampled confidence GAP in `[0, 1]`.
89    pub confidence_gap: f64,
90    /// Time of the last contributing measurement.
91    pub measurements_ready_at: Option<f64>,
92    /// Time at which this decision may be consumed.
93    pub ready_at: f64,
94}
95
96/// Failures at the mock-decoder boundary.
97#[derive(Debug, Clone, PartialEq, thiserror::Error)]
98pub enum DecoderError {
99    /// Probability-like configuration must be finite and in `[0, 1]`.
100    #[error("{name} must be in [0, 1], got {value}")]
101    InvalidProbability {
102        /// Invalid field name.
103        name: &'static str,
104        /// Invalid value.
105        value: f64,
106    },
107    /// Confidence threshold must separate accepted and rejected GAP ranges.
108    #[error("gap_threshold must be strictly between 0 and 1, got {0}")]
109    InvalidGapThreshold(f64),
110    /// A duration was negative, non-finite, or overflowed the VM clock.
111    #[error("invalid decoder timing")]
112    InvalidTiming,
113    /// One causal key was requested with different immutable inputs.
114    #[error("decoder solve {key:?} was requested with inconsistent inputs")]
115    InconsistentRequest {
116        /// Reused causal key.
117        key: DecodeKey,
118    },
119}
120
121/// Seeded streaming policy model with one cached result per causal solve.
122#[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    /// Build a reproducible mock decoder.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`DecoderError::InvalidProbability`] or
137    /// [`DecoderError::InvalidGapThreshold`] for invalid assumptions.
138    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    /// Request or reuse one causal solve.
154    ///
155    /// `round_duration` is the local seam memory-round duration, rather than a
156    /// global gate unit. A measurement-backed solve is ready at
157    /// `measurements_ready_at + latency_rounds * round_duration`, or immediately
158    /// when a later request arrives after that deadline. A constant solve with
159    /// no contributing measurement is ready immediately at `requested_at`.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`DecoderError::InvalidTiming`] if the ready deadline is invalid,
164    /// or [`DecoderError::InconsistentRequest`] if `key` is reused for a
165    /// different raw parity or deadline.
166    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    /// Request a solve with an explicit causal deadline.
195    ///
196    /// Factory GAP decisions use this after their required physical QEC rounds;
197    /// ordinary readouts should use [`Self::request`] to derive the deadline
198    /// from the measurement cut.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`DecoderError::InvalidTiming`] for a negative or non-finite
203    /// timestamp, or a deadline before its measurement cut, or
204    /// [`DecoderError::InconsistentRequest`] when a cached causal solve is
205    /// requested with different inputs.
206    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        // One causal solve owns one random stream, so a retry never advances
231        // unrelated decoder randomness.
232        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}