Skip to main content

wai_quantum/
quantum_qec.rs

1//! Deterministic quantum LDPC DECODING — `wai.quantum.decode`
2//! (extensions/quantum-ops § Decoding).
3//!
4//! A fault-tolerant quantum computer runs a classical decoder in its innermost
5//! loop: every syndrome-measurement round produces a syndrome, and a decoder must
6//! turn it into a correction fast enough and cheaply enough to keep up with the
7//! code cycle. For quantum LDPC (qLDPC) codes — the high-rate codes that make
8//! fault tolerance affordable — the decoder of record is **belief propagation**,
9//! and plain BP *fails* on quantum codes: the stabilizer degeneracy and the short
10//! cycles in the Tanner graph trap it in symmetric non-solutions. A published fix is
11//! **Relay-BP**: an ensemble of BP passes with disordered per-node memory
12//! strengths, relayed (warm-started) from one another, which breaks the symmetry.
13//!
14//! This module is Relay-BP done the WAI way: **normalized min-sum** message
15//! passing in pure fixed-point integer arithmetic — no float, no `tanh`/`atanh` —
16//! so a decode is **byte-identical on every machine** and the emitted correction
17//! is a portable hash. Every decode seals a signed, joule-metered
18//! [`crate::quantum_ops::DecodeReceipt`] carrying **joules-per-decode**.
19//!
20//! Min-sum on a single uniform channel is scale-invariant (check node = min,
21//! variable node = sum, both homogeneous), so the channel prior is a fixed unit
22//! and the exact physical error rate never enters the decoder — only the
23//! deterministic error *sampler* uses it. That keeps the decode fully integer.
24//!
25//! # The codes verify their own claims
26//!
27//! A code is named `[[n, k, d]]`, and `d` is the half that matters: below it,
28//! correction is guaranteed rather than likely. [`x_distance`] computes it exactly
29//! by exhaustion, so `surface(3)` is *shown* to have distance 3 and `surface(5)`
30//! distance 5 — every lighter logical operator ruled out — rather than being
31//! assumed from the constructor's name. The decoder is held to the same standard:
32//! every error of weight below `d` is decoded and checked, exhaustively.
33//!
34//! # Circuit-level noise: faults that move
35//!
36//! Phenomenological noise treats a measurement as a black box that sometimes
37//! lies. A real extraction round is a *circuit*, and a fault inside it does not
38//! stay put: a fault on a stabilizer's ancilla rides the remaining CNOTs and
39//! lands as X errors on every data qubit that circuit has not yet touched — a
40//! **hook error**. One fault, several correlated data errors, and no edge in the
41//! matching graph that represents them.
42//!
43//! [`decode_circuit_level`] adds that channel. Surface code, `q = hook = p`,
44//! `d` rounds, and the same run with hooks switched off:
45//!
46//! | `p`   | d=3    | d=5    | d=7    | d=5, hooks off |
47//! |-------|--------|--------|--------|----------------|
48//! | 0.002 | 0.0008 | 0.0000 | 0.0000 | 0.0000         |
49//! | 0.005 | 0.0058 | 0.0033 | 0.0008 | 0.0008         |
50//!
51//! Hooks cost roughly a factor of four at `d = 5`, which is the point: they are
52//! why circuit-level thresholds sit below phenomenological ones. With `hook = 0`
53//! the model reduces to [`decode_phenomenological`] **bit for bit** — same
54//! stream, same failures — which is asserted, not merely hoped.
55//!
56//! # Handing the model to other tools
57//!
58//! [`to_dem`] exports the noise model as a **detector error model** in Stim's DEM
59//! syntax. A DEM says nothing about qubits or gates — it says here are the
60//! independent ways this experiment can fault, here is which detectors each one
61//! lights, and here is whether it flips the logical observable. That is exactly
62//! the interface every matching decoder wants, which is why it is the QEC
63//! ecosystem's interchange format, and it is to decoders what QIR is to compilers.
64//!
65//! The observable comes from [`logical_z_support`]: the Z checks detect X errors,
66//! so failure is not "an error remained" but "the residual chain crossed the
67//! code", and a minimum-weight logical Z is the ruler that measures it.
68//!
69//! The export is checked against the code's own algebra rather than eyeballed:
70//! the mechanisms of a minimum-weight logical operator must light **no** detectors
71//! yet flip the observable an **odd** number of times, a stabilizer's must light
72//! none and flip it an **even** number, and no mechanism anywhere may be
73//! undetectable while flipping the observable — that last one would be a logical
74//! error no decoder could see coming, and would make every external consumer
75//! silently mis-score.
76//!
77//! Ingest works too: [`from_dem`] parses a model — ours or somebody else's — and
78//! [`decode_dem`] decodes a round of detector data against it, answering the only
79//! question a QEC pipeline asks: did the logical observable flip? Decoding the
80//! same shots through the exported DEM matches the native decoder:
81//!
82//! | d | `p`   | native | via DEM |
83//! |---|-------|--------|---------|
84//! | 3 | 0.01  | 0.0127 | 0.0113  |
85//! | 5 | 0.005 | 0.0007 | 0.0007  |
86//! | 5 | 0.02  | 0.0440 | 0.0440  |
87//!
88//! (The two judge success slightly differently — stabilizer membership against
89//! observable parity — which coincide whenever the decoder fully explains the
90//! syndrome, and account for the small gap at `d = 3`.)
91//!
92//! Ingest refuses rather than approximates: an unimplemented instruction is an
93//! error, not a skipped line, because a DEM whose `repeat` block was ignored
94//! describes a different experiment; and a hyperedge mechanism is refused by the
95//! matching decoder instead of being dropped.
96//!
97//! # Two decoders, and which code each is for
98//!
99//! **Relay-BP** is a belief-propagation decoder and earns its name on the qLDPC
100//! family it was designed for: on the bivariate-bicycle code the relay legs beat
101//! plain BP (at `p = 0.06`, 0.30 → 0.27 logical failure), and more legs help.
102//!
103//! On **topological** codes it does not merely underperform — it inverts the
104//! property that makes a code family worth having. Measured across `p = 0.001`
105//! to `0.02`, a distance-5 surface code decodes *worse* than distance-3 under
106//! Relay-BP, and the toric code shows the same inversion. That is the known
107//! weakness of BP on degenerate codes with short cycles, not a defect in the code
108//! constructions (both verify as valid CSS codes with the expected `k`).
109//!
110//! [`union_find_correct`] is the answer for those codes — a matching decoder that
111//! pairs syndromes on the code's own graph. It restores the property BP loses
112//! (surface code, logical failure rate, 2000 trials):
113//!
114//! | `p`   | d=3 BP | d=3 UF | d=5 BP | d=5 UF | d=7 BP | d=7 UF |
115//! |-------|--------|--------|--------|--------|--------|--------|
116//! | 0.005 | 0.0205 | 0.0010 | 0.0410 | 0.0000 | 0.0605 | 0.0000 |
117//! | 0.01  | 0.0370 | 0.0020 | 0.0785 | 0.0000 | 0.1170 | 0.0000 |
118//! | 0.02  | 0.0715 | 0.0090 | 0.1535 | 0.0020 | 0.2365 | 0.0005 |
119//!
120//! # Faulty measurement: decoding in space-time
121//!
122//! Everything above assumes the syndrome is read perfectly, which no device does.
123//! [`decode_phenomenological`] runs `rounds` noisy measurement rounds plus a final
124//! perfect readout and decodes the block as a **space-time** matching problem:
125//! detectors are *differences* between consecutive rounds, so a data error lights
126//! two checks at the same time while a lying measurement lights one check at two
127//! adjacent times. Surface code, `q = p`, `d` rounds:
128//!
129//! | `p`   | d=3    | d=5    | d=7    |
130//! |-------|--------|--------|--------|
131//! | 0.003 | 0.0025 | 0.0000 | 0.0000 |
132//! | 0.005 | 0.0050 | 0.0008 | 0.0008 |
133//! | 0.01  | 0.0142 | 0.0042 | 0.0067 |
134//! | 0.02  | 0.0475 | 0.0458 | 0.0417 |
135//!
136//! Distance pays below `p ≈ 1%` and the curves converge by `2%` — a threshold in
137//! the region the literature puts the phenomenological surface code, which is the
138//! outcome that would have exposed a broken implementation.
139//!
140//! Edges are weighted by `−ln(rate)`, so the decoder blames the likelier fault
141//! rather than the nearer one. That matters more than it sounds: with data noise
142//! off and measurement noise on, an unweighted decoder happily "explains" a lying
143//! measurement by flipping data qubits, and enough of those in a line is a logical
144//! error invented out of nothing.
145//!
146//! The near-linear cost is measured, not just claimed: single-shot decoding runs
147//! about 1.2 µs per shot at `d = 3` and 7.5 µs at `d = 11`, so roughly six times
148//! the work for thirteen times the qubits. Threshold studies are Monte Carlo, and
149//! at those rates they are a coffee break rather than an overnight job.
150//!
151//! Distance now buys what it is supposed to buy, monotonically. Use union-find for
152//! surface and toric codes, Relay-BP for the higher-degree qLDPC families where a
153//! qubit touches more than two checks and the matching graph does not exist.
154//!
155//! Codes built in: the **rotated surface code** (`[[d², 1, d]]`, the planar code
156//! hardware actually targets), the **toric code** (its periodic-boundary cousin)
157//! and a
158//! **bivariate-bicycle** qLDPC code (the high-rate family; the \[\[72,12\]\] preset is
159//! the "gross"-style construction). Logical success — knowable only in simulation,
160//! since it needs the true error — is reported by the benchmark harness, never put
161//! in the receipt (a real decoder does not know it).
162
163use crate::quantum_ops::{content_hash, DecodeReceipt, GrantRef};
164use ed25519_dalek::SigningKey;
165
166/// Fixed-point fractional bits for messages/damping. `1.0` = `1 << QEC_FRAC`.
167pub const QEC_FRAC: u32 = 12;
168const SCALE: i64 = 1 << QEC_FRAC;
169/// Channel prior (uniform): min-sum is scale-invariant, so any positive unit works.
170const L0: i64 = SCALE;
171/// Message saturation, keeps fixed-point sums bounded without changing decisions.
172const MSG_MAX: i64 = 64 * SCALE;
173/// Normalized-min-sum scaling α = 7/8 (the standard hardware value).
174const ALPHA_NUM: i64 = 7;
175/// Per-node relay memory strength range. Relay-BP draws *disordered* memory,
176/// including negative values (over-relaxation) that aggressively break the
177/// symmetric trapping sets plain BP gets stuck in. γ ∈ [-0.6, 0.9].
178const GAMMA_MIN: i64 = -(6 * SCALE) / 10;
179const GAMMA_MAX: i64 = (9 * SCALE) / 10;
180
181fn splitmix64(state: &mut u64) -> u64 {
182    *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
183    let mut z = *state;
184    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
185    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
186    z ^ (z >> 31)
187}
188
189// ===========================================================================
190// GF(2) linear algebra — row space membership (logical-triviality test)
191// ===========================================================================
192
193/// A reduced GF(2) basis of a set of binary row vectors, supporting membership
194/// testing. Rows are packed into `u64` words. Used to test whether a residual
195/// error is a product of stabilizer generators (i.e. logically trivial).
196#[derive(Clone, Debug)]
197pub struct Gf2Basis {
198    words: usize,
199    /// Reduced rows, each with a distinct pivot column.
200    rows: Vec<Vec<u64>>,
201    /// Pivot column of each reduced row (parallel to `rows`).
202    pivots: Vec<usize>,
203}
204
205fn bit_get(row: &[u64], i: usize) -> bool {
206    (row[i >> 6] >> (i & 63)) & 1 == 1
207}
208fn bit_set(row: &mut [u64], i: usize) {
209    row[i >> 6] |= 1u64 << (i & 63);
210}
211fn bit_xor(dst: &mut [u64], src: &[u64]) {
212    for (d, s) in dst.iter_mut().zip(src) {
213        *d ^= *s;
214    }
215}
216fn is_zero(row: &[u64]) -> bool {
217    row.iter().all(|&w| w == 0)
218}
219
220/// Fully reduced row-echelon form over GF(2), returned as `(pivot column, row)`.
221///
222/// [`Gf2Basis`] stops at row-echelon form — a pivot may still appear in earlier
223/// rows — which is enough to test membership but not to read a row off as an
224/// independent generator. Encoding needs the stronger form.
225#[cfg(feature = "quantum")]
226fn rref(rows_cols: &[Vec<usize>], n: usize) -> Vec<(usize, Vec<usize>)> {
227    let mut m: Vec<Vec<bool>> = rows_cols
228        .iter()
229        .map(|cols| {
230            let mut r = vec![false; n];
231            for &c in cols {
232                r[c] = true;
233            }
234            r
235        })
236        .collect();
237    let mut pivots: Vec<usize> = Vec::new();
238    let mut r = 0usize;
239    for c in 0..n {
240        let Some(sel) = (r..m.len()).find(|&i| m[i][c]) else { continue };
241        m.swap(r, sel);
242        for i in 0..m.len() {
243            if i != r && m[i][c] {
244                for j in 0..n {
245                    let v = m[r][j];
246                    m[i][j] ^= v;
247                }
248            }
249        }
250        pivots.push(c);
251        r += 1;
252        if r == m.len() {
253            break;
254        }
255    }
256    m.truncate(pivots.len());
257    pivots
258        .into_iter()
259        .zip(m)
260        .map(|(p, row)| (p, (0..n).filter(|&j| row[j]).collect()))
261        .collect()
262}
263
264impl Gf2Basis {
265    /// Whether a row lies in this basis's span — it reduces to zero against it.
266    pub fn reduces_to_zero(&self, cols: &[usize], ncols: usize) -> bool {
267        let mut v = vec![0u64; ncols.div_ceil(64).max(1)];
268        for &c in cols {
269            bit_set(&mut v, c);
270        }
271        for (bi, br) in self.rows.iter().enumerate() {
272            if bit_get(&v, self.pivots[bi]) {
273                bit_xor(&mut v, br);
274            }
275        }
276        is_zero(&v)
277    }
278
279    /// Build from a list of rows, each given as the sorted set of its 1-columns.
280    pub fn from_rows(rows_cols: &[Vec<usize>], ncols: usize) -> Gf2Basis {
281        let words = ncols.div_ceil(64).max(1);
282        let mut basis: Vec<Vec<u64>> = Vec::new();
283        let mut pivots: Vec<usize> = Vec::new();
284        for cols in rows_cols {
285            let mut v = vec![0u64; words];
286            for &c in cols {
287                bit_set(&mut v, c);
288            }
289            // reduce against current basis
290            for (bi, br) in basis.iter().enumerate() {
291                if bit_get(&v, pivots[bi]) {
292                    bit_xor(&mut v, br);
293                }
294            }
295            if let Some(p) = Self::lowest_set(&v) {
296                // eliminate this pivot from existing basis rows for a full RREF
297                for br in basis.iter_mut() {
298                    if bit_get(br, p) {
299                        bit_xor(br, &v);
300                    }
301                }
302                basis.push(v);
303                pivots.push(p);
304            }
305        }
306        Gf2Basis { words, rows: basis, pivots }
307    }
308
309    fn lowest_set(v: &[u64]) -> Option<usize> {
310        for (w, &word) in v.iter().enumerate() {
311            if word != 0 {
312                return Some(w * 64 + word.trailing_zeros() as usize);
313            }
314        }
315        None
316    }
317
318    /// Rank of the row space.
319    pub fn rank(&self) -> usize {
320        self.rows.len()
321    }
322
323    /// Is the vector (given as its 1-columns) in the row space? Reduce it against
324    /// the basis and check it vanishes.
325    pub fn contains(&self, cols: &[usize]) -> bool {
326        let mut v = vec![0u64; self.words];
327        for &c in cols {
328            bit_set(&mut v, c);
329        }
330        for (bi, br) in self.rows.iter().enumerate() {
331            if bit_get(&v, self.pivots[bi]) {
332                bit_xor(&mut v, br);
333            }
334        }
335        is_zero(&v)
336    }
337}
338
339// ===========================================================================
340// CSS code — check matrix (for decoding) + stabilizer basis (for success test)
341// ===========================================================================
342
343/// A CSS code prepared for decoding one error sector. `checks` are the parity
344/// checks whose syndrome we decode (rows as sorted qubit lists); `stab_basis` is
345/// the GF(2) row space of the complementary stabilizers, against which a residual
346/// error is tested for logical triviality.
347#[derive(Clone, Debug)]
348pub struct CssCode {
349    pub id: String,
350    pub n: usize,
351    /// Parity checks (syndrome producers), each a sorted list of qubit indices.
352    pub checks: Vec<Vec<usize>>,
353    /// The complementary stabilizer family, kept so callers can inspect the code
354    /// (a CSS code is defined by both families, not just the one that produces the
355    /// syndrome being decoded).
356    pub stabs: Vec<Vec<usize>>,
357    stab_basis: Gf2Basis,
358    /// Number of encoded logical qubits, `k = n − rank(checks) − rank(stab)`.
359    pub k: usize,
360}
361
362impl CssCode {
363    fn new(id: String, n: usize, checks: Vec<Vec<usize>>, stab: Vec<Vec<usize>>) -> CssCode {
364        let stab_basis = Gf2Basis::from_rows(&stab, n);
365        let check_basis = Gf2Basis::from_rows(&checks, n);
366        let k = n.saturating_sub(check_basis.rank() + stab_basis.rank());
367        CssCode { id, n, checks, stabs: stab, stab_basis, k }
368    }
369
370    /// Canonical bytes of the check matrix (content-addresses the code).
371    pub fn check_matrix_bytes(&self) -> Vec<u8> {
372        let mut b = Vec::new();
373        b.extend_from_slice(b"wai:qec-checks\x01");
374        b.extend_from_slice(&(self.n as u64).to_le_bytes());
375        for row in &self.checks {
376            b.extend_from_slice(&(row.len() as u32).to_le_bytes());
377            for &c in row {
378                b.extend_from_slice(&(c as u32).to_le_bytes());
379            }
380        }
381        b
382    }
383
384    /// A residual error (as its 1-qubits) is logically trivial iff it lies in the
385    /// stabilizer row space.
386    pub fn is_trivial(&self, residual_cols: &[usize]) -> bool {
387        self.stab_basis.contains(residual_cols)
388    }
389
390    // --- Toric code -------------------------------------------------------
391
392    /// The `L×L` toric code: `n = 2L²` qubits on edges, `L²` star (X) checks and
393    /// `L²` plaquette (Z) checks, `k = 2`. We decode X-errors: `checks = Z`
394    /// (plaquettes), triviality against the X-stabilizers (stars).
395    pub fn toric(l: usize) -> CssCode {
396        let n = 2 * l * l;
397        let h = |i: usize, j: usize| (i % l) * l + (j % l); // horizontal edge id
398        let v = |i: usize, j: usize| l * l + (i % l) * l + (j % l); // vertical edge id
399        let mut plaq = Vec::with_capacity(l * l); // Z checks
400        let mut star = Vec::with_capacity(l * l); // X stabilizers
401        for i in 0..l {
402            for j in 0..l {
403                // plaquette (i,j): top/bottom horizontals, left/right verticals
404                let mut p = vec![h(i, j), h(i + 1, j), v(i, j), v(i, j + 1)];
405                p.sort_unstable();
406                p.dedup();
407                plaq.push(p);
408                // star at vertex (i,j): incident horizontals + verticals
409                let mut s = vec![h(i, j), h(i, j + l - 1), v(i, j), v(i + l - 1, j)];
410                s.sort_unstable();
411                s.dedup();
412                star.push(s);
413            }
414        }
415        CssCode::new(format!("toric:L{l}"), n, plaq, star)
416    }
417
418    // --- Rotated surface code ---------------------------------------------
419
420    /// The distance-`d` **rotated surface code**, `[[d², 1, d]]` (`d` odd).
421    ///
422    /// The toric code above is this code's periodic-boundary cousin: elegant, but
423    /// it needs a torus. The rotated surface code is the planar one — nearest-
424    /// neighbour on a `d × d` patch with open boundaries — and it is what
425    /// essentially every hardware roadmap actually targets.
426    ///
427    /// Data qubit `(r, c)` is index `r·d + c`. Bulk plaquettes cover each 2×2
428    /// square and alternate in a checkerboard; the boundaries carry weight-2
429    /// stabilizers, X along the top and bottom, Z along the left and right. The
430    /// parities are the ones that make the two families commute — the neighbouring
431    /// choice does not, which the `css_commutation` test would catch.
432    pub fn surface(d: usize) -> CssCode {
433        assert!(d >= 3 && d % 2 == 1, "rotated surface code needs an odd distance >= 3");
434        let n = d * d;
435        let q = |r: usize, c: usize| r * d + c;
436        let mut z_checks: Vec<Vec<usize>> = Vec::new();
437        let mut x_stabs: Vec<Vec<usize>> = Vec::new();
438
439        // Bulk: every 2x2 square, checkerboarded.
440        for r in 0..d - 1 {
441            for c in 0..d - 1 {
442                let face = vec![q(r, c), q(r, c + 1), q(r + 1, c), q(r + 1, c + 1)];
443                if (r + c) % 2 == 0 {
444                    z_checks.push(face);
445                } else {
446                    x_stabs.push(face);
447                }
448            }
449        }
450        // X boundary: top row on even columns, bottom row on odd columns.
451        for c in (0..d - 1).step_by(2) {
452            x_stabs.push(vec![q(0, c), q(0, c + 1)]);
453        }
454        for c in (1..d - 1).step_by(2) {
455            x_stabs.push(vec![q(d - 1, c), q(d - 1, c + 1)]);
456        }
457        // Z boundary: left column on odd rows, right column on even rows.
458        for r in (1..d - 1).step_by(2) {
459            z_checks.push(vec![q(r, 0), q(r + 1, 0)]);
460        }
461        for r in (0..d - 1).step_by(2) {
462            z_checks.push(vec![q(r, d - 1), q(r + 1, d - 1)]);
463        }
464        for v in z_checks.iter_mut().chain(x_stabs.iter_mut()) {
465            v.sort_unstable();
466            v.dedup();
467        }
468        CssCode::new(format!("surface:d{d}"), n, z_checks, x_stabs)
469    }
470
471    // --- Colour codes ------------------------------------------------------
472
473    /// The `[[7,1,3]]` Steane code — the smallest 2D **colour code**.
474    ///
475    /// Its three checks are the rows of the Hamming `[7,4,3]` parity-check matrix,
476    /// used for *both* the X and the Z family. That coincidence is the whole point
477    /// of a colour code: the two stabilizer families sit on the same supports, so
478    /// the code is **self-dual** as a CSS code.
479    ///
480    /// The payoff is [`CssCode::has_transversal_hadamard`]. A surface code has to
481    /// implement a logical Hadamard with lattice surgery or code deformation; a
482    /// colour code gets it by applying `H` to every physical qubit, one round, no
483    /// ancillas. The trade is a lower threshold — this buys gates, not noise
484    /// tolerance.
485    pub fn color_steane() -> CssCode {
486        let faces = vec![vec![3, 4, 5, 6], vec![1, 2, 5, 6], vec![0, 2, 4, 6]];
487        CssCode::new("color:steane".into(), 7, faces.clone(), faces)
488    }
489
490    /// Whether the X and Z stabilizer families generate the **same** group.
491    ///
492    /// This is the algebraic content of "colour code". `H⊗n` exchanges X-type and
493    /// Z-type Paulis, so it maps the stabilizer group to itself exactly when the
494    /// two families coincide — and a map that preserves the stabilizer group
495    /// preserves the code space.
496    pub fn is_self_dual(&self) -> bool {
497        let a = Gf2Basis::from_rows(&self.checks, self.n);
498        let b = Gf2Basis::from_rows(&self.stabs, self.n);
499        if a.rank() != b.rank() {
500            return false;
501        }
502        // same rank and each family reduces to nothing against the other ⇒ same span
503        self.checks.iter().all(|r| b.reduces_to_zero(r, self.n))
504            && self.stabs.iter().all(|r| a.reduces_to_zero(r, self.n))
505    }
506
507    /// Whether applying `H` to every physical qubit is a logical operation.
508    ///
509    /// True exactly for a self-dual CSS code. Stated as its own question because
510    /// it is the property people actually want from a colour code.
511    pub fn has_transversal_hadamard(&self) -> bool {
512        self.is_self_dual()
513    }
514
515    /// A circuit preparing the logical `|0⟩` of this CSS code from `|0…0⟩`.
516    ///
517    /// `|0⟩_L` is the uniform superposition over the row space of the X-stabilizer
518    /// matrix. Reduce that matrix to *fully* reduced row-echelon form, put an `H`
519    /// on each pivot qubit, and fan a `CNOT` out to the rest of its row: each row
520    /// then contributes an independent branch, which is exactly that superposition.
521    ///
522    /// The full reduction matters — with pivots shared between rows the fan-out
523    /// would interfere with itself and prepare something else.
524    ///
525    /// Needs the `quantum` feature: the decoding layer above is deliberately
526    /// independent of the simulator, and this is the one place that borrows it.
527    #[cfg(feature = "quantum")]
528    pub fn encode_zero_circuit(&self) -> crate::quantum::Circuit {
529        let mut c = crate::quantum::Circuit::new(self.n as u8);
530        for (pivot, cols) in rref(&self.stabs, self.n) {
531            c.h(pivot as u8);
532            for col in cols {
533                if col != pivot {
534                    c.cx(pivot as u8, col as u8);
535                }
536            }
537        }
538        c
539    }
540
541    // --- Bivariate-bicycle qLDPC code -------------------------------------
542
543    /// A bivariate-bicycle code on `Z_l × Z_m`. `a` and `b` are lists of monomials
544    /// `(px, py)`; `A = Σ x^px y^py`, `B` likewise (mod-2 sums of shift matrices).
545    /// `H_X = [A | B]`, `H_Z = [Bᵀ | Aᵀ]`, `n = 2·l·m`. We decode X-errors:
546    /// `checks = H_Z`, triviality against `H_X`.
547    pub fn bivariate_bicycle(l: usize, m: usize, a: &[(usize, usize)], b: &[(usize, usize)]) -> CssCode {
548        let lm = l * m;
549        let n = 2 * lm;
550        let cell = |i: usize, j: usize| (i % l) * m + (j % m);
551        // A[row][col]: monomial (px,py) sends col-cell (i,j) → row-cell (i+px, j+py).
552        // Build row-wise supports for A and B (over the lm cells).
553        let build = |monos: &[(usize, usize)]| -> Vec<Vec<usize>> {
554            let mut rows = vec![Vec::new(); lm];
555            for i in 0..l {
556                for j in 0..m {
557                    let col = cell(i, j);
558                    for &(px, py) in monos {
559                        let row = cell(i + px, j + py);
560                        rows[row].push(col);
561                    }
562                }
563            }
564            for r in rows.iter_mut() {
565                r.sort_unstable();
566                r.dedup();
567            }
568            rows
569        };
570        let a_rows = build(a);
571        let b_rows = build(b);
572        // transpose helper (for Bᵀ, Aᵀ)
573        let transpose = |rows: &[Vec<usize>]| -> Vec<Vec<usize>> {
574            let mut t = vec![Vec::new(); lm];
575            for (r, cols) in rows.iter().enumerate() {
576                for &c in cols {
577                    t[c].push(r);
578                }
579            }
580            for x in t.iter_mut() {
581                x.sort_unstable();
582            }
583            t
584        };
585        let at = transpose(&a_rows);
586        let bt = transpose(&b_rows);
587
588        // H_X = [A | B]  (columns 0..lm = block L, lm..2lm = block R)
589        let hx: Vec<Vec<usize>> = (0..lm)
590            .map(|r| {
591                let mut row: Vec<usize> = a_rows[r].clone();
592                row.extend(b_rows[r].iter().map(|&c| c + lm));
593                row.sort_unstable();
594                row
595            })
596            .collect();
597        // H_Z = [Bᵀ | Aᵀ]
598        let hz: Vec<Vec<usize>> = (0..lm)
599            .map(|r| {
600                let mut row: Vec<usize> = bt[r].clone();
601                row.extend(at[r].iter().map(|&c| c + lm));
602                row.sort_unstable();
603                row
604            })
605            .collect();
606
607        // decode X-errors: checks = H_Z, triviality against H_X
608        let code = CssCode::new(String::new(), n, hz, hx);
609        CssCode { id: format!("bb:[[{},{}]]", code.n, code.k), ..code }
610    }
611
612    /// The `[[72,12]]` bivariate-bicycle preset (l=m=6, A=x³+y+y², B=y³+x+x²).
613    pub fn gross() -> CssCode {
614        CssCode::bivariate_bicycle(6, 6, &[(3, 0), (0, 1), (0, 2)], &[(0, 3), (1, 0), (2, 0)])
615    }
616}
617
618// ===========================================================================
619// Tanner graph + normalized min-sum belief propagation
620// ===========================================================================
621
622struct Tanner {
623    n: usize,
624    n_edges: usize,
625    e_var: Vec<u32>,          // variable (qubit) of each edge
626    chk_range: Vec<(u32, u32)>, // contiguous edge range per check
627    var_edges: Vec<Vec<u32>>, // edges incident to each variable
628}
629
630impl Tanner {
631    fn build(checks: &[Vec<usize>], n: usize) -> Tanner {
632        let mut e_var = Vec::new();
633        let mut chk_range = Vec::with_capacity(checks.len());
634        let mut var_edges = vec![Vec::new(); n];
635        for cols in checks {
636            let start = e_var.len() as u32;
637            for &v in cols {
638                var_edges[v].push(e_var.len() as u32);
639                e_var.push(v as u32);
640            }
641            chk_range.push((start, e_var.len() as u32));
642        }
643        let n_edges = e_var.len();
644        Tanner { n, n_edges, e_var, chk_range, var_edges }
645    }
646}
647
648/// One min-sum BP leg over the current messages. `gamma` is the per-variable relay
649/// memory strength (fixed-point, 0 = no memory). Returns `(hard_decision,
650/// converged, iters, messages)`. Messages are warm-started from `mu_v2c`.
651#[allow(clippy::too_many_arguments)]
652fn bp_leg(
653    t: &Tanner,
654    syndrome: &[bool],
655    gamma: &[i64],
656    max_iter: u32,
657    mu_v2c: &mut [i64],
658    mu_c2v: &mut [i64],
659) -> (Vec<bool>, bool, u32, u64) {
660    let mut hard = vec![false; t.n];
661    let mut messages = 0u64;
662    let mut iters = 0;
663    for _ in 0..max_iter {
664        iters += 1;
665        // --- check update (min-sum) ---
666        for (&(a, b), &s) in t.chk_range.iter().zip(syndrome) {
667            let (mut min1, mut min2) = (i64::MAX, i64::MAX);
668            let mut arg = a;
669            let mut neg_parity = false;
670            for e in a..b {
671                let x = mu_v2c[e as usize];
672                if x < 0 {
673                    neg_parity = !neg_parity;
674                }
675                let mag = x.abs();
676                if mag < min1 {
677                    min2 = min1;
678                    min1 = mag;
679                    arg = e;
680                } else if mag < min2 {
681                    min2 = mag;
682                }
683            }
684            for e in a..b {
685                let x = mu_v2c[e as usize];
686                let base = if e == arg { min2 } else { min1 };
687                let mag = (base * ALPHA_NUM) >> 3;
688                let is_neg = neg_parity ^ (x < 0) ^ s;
689                mu_c2v[e as usize] = if is_neg { -mag } else { mag };
690            }
691        }
692        // --- variable update + hard decision ---
693        for v in 0..t.n {
694            let mut total = L0;
695            for &e in &t.var_edges[v] {
696                total += mu_c2v[e as usize];
697            }
698            hard[v] = total < 0;
699            let g = gamma[v];
700            for &e in &t.var_edges[v] {
701                let target = total - mu_c2v[e as usize];
702                let nv = if g == 0 {
703                    target
704                } else {
705                    (g * mu_v2c[e as usize] + (SCALE - g) * target) >> QEC_FRAC
706                };
707                mu_v2c[e as usize] = nv.clamp(-MSG_MAX, MSG_MAX);
708            }
709        }
710        messages += 2 * t.n_edges as u64;
711        // --- convergence: H·hard == syndrome ---
712        let mut ok = true;
713        for (&(a, b), &s) in t.chk_range.iter().zip(syndrome) {
714            let mut par = false;
715            for e in a..b {
716                par ^= hard[t.e_var[e as usize] as usize];
717            }
718            if par != s {
719                ok = false;
720                break;
721            }
722        }
723        if ok {
724            return (hard, true, iters, messages);
725        }
726    }
727    (hard, false, iters, messages)
728}
729
730/// Relay-BP: leg 0 is plain min-sum; each subsequent leg draws disordered
731/// per-variable memory strengths (deterministic from `seed`) and warm-starts
732/// (relays) from the previous leg. Stops at the first leg that satisfies the
733/// syndrome. `max_legs = 1` recovers plain BP.
734struct DecodeCore {
735    correction: Vec<bool>,
736    converged: bool,
737    legs: u32,
738    iters: u32,
739    messages: u64,
740}
741
742fn relay_bp(
743    t: &Tanner,
744    syndrome: &[bool],
745    seed: u64,
746    max_iter: u32,
747    max_legs: u32,
748) -> DecodeCore {
749    let mut mu_v2c = vec![L0; t.n_edges];
750    let mut mu_c2v = vec![0i64; t.n_edges];
751    let gamma0 = vec![0i64; t.n];
752    let (mut hard, mut conv, mut it, mut msg) =
753        bp_leg(t, syndrome, &gamma0, max_iter, &mut mu_v2c, &mut mu_c2v);
754    let mut legs = 1u32;
755    let (mut tot_it, mut tot_msg) = (it, msg);
756    while !conv && legs < max_legs {
757        let mut gamma = vec![0i64; t.n];
758        let mut st = seed
759            .wrapping_mul(0x100_0001)
760            .wrapping_add(legs as u64)
761            .wrapping_add(0xD15EA5E);
762        let span = (GAMMA_MAX - GAMMA_MIN) as u64 + 1;
763        for gv in gamma.iter_mut() {
764            let r = splitmix64(&mut st);
765            *gv = GAMMA_MIN + (r % span) as i64;
766        }
767        let out = bp_leg(t, syndrome, &gamma, max_iter, &mut mu_v2c, &mut mu_c2v);
768        hard = out.0;
769        conv = out.1;
770        it = out.2;
771        msg = out.3;
772        tot_it += it;
773        tot_msg += msg;
774        legs += 1;
775    }
776    let _ = it;
777    let _ = msg;
778    DecodeCore { correction: hard, converged: conv, legs, iters: tot_it, messages: tot_msg }
779}
780
781// ===========================================================================
782// Simulation front-end + decode result
783// ===========================================================================
784
785/// Deterministic decoder configuration.
786#[derive(Clone, Copy, Debug)]
787pub struct DecodeConfig {
788    /// Physical error rate per qubit (fixed-point, scale 2^12).
789    pub p_fx: i64,
790    /// Max BP iterations per leg.
791    pub max_iter: u32,
792    /// Max relay legs (1 = plain BP).
793    pub max_legs: u32,
794}
795
796impl DecodeConfig {
797    pub fn from_p(p: f64) -> DecodeConfig {
798        DecodeConfig { p_fx: (p * SCALE as f64) as i64, max_iter: 32, max_legs: 12 }
799    }
800}
801
802/// One decode: sample an error at rate `p`, decode its syndrome, and check
803/// logical success against the true error. Deterministic given `(code, cfg, seed)`.
804#[derive(Clone, Debug)]
805pub struct DecodeResult {
806    pub code_id: String,
807    pub n: usize,
808    pub error: Vec<u32>,      // true error qubits (simulation only)
809    pub correction: Vec<u32>, // decoder output qubits
810    pub syndrome: Vec<u32>,   // unsatisfied check indices
811    pub converged: bool,
812    pub logical_success: bool,
813    pub legs: u32,
814    pub iters: u32,
815    pub messages: u64,
816    syndrome_bits: Vec<bool>,
817    correction_bits: Vec<bool>,
818    check_matrix_hash: [u8; 32],
819}
820
821/// Sample a bit-flip error deterministically at rate `p_fx`.
822fn sample_error(n: usize, p_fx: i64, seed: u64) -> Vec<bool> {
823    let mut e = vec![false; n];
824    let mut st = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
825    for ei in e.iter_mut() {
826        let r = (splitmix64(&mut st) >> (64 - QEC_FRAC)) as i64; // [0, SCALE)
827        if r < p_fx {
828            *ei = true;
829        }
830    }
831    e
832}
833
834fn syndrome_of(checks: &[Vec<usize>], e: &[bool]) -> Vec<bool> {
835    checks
836        .iter()
837        .map(|row| row.iter().fold(false, |acc, &q| acc ^ e[q]))
838        .collect()
839}
840
841fn bits_to_ids(bits: &[bool]) -> Vec<u32> {
842    bits.iter()
843        .enumerate()
844        .filter(|&(_, &b)| b)
845        .map(|(i, _)| i as u32)
846        .collect()
847}
848
849fn canonical_bits(tag: &[u8], bits: &[bool]) -> Vec<u8> {
850    let mut out = tag.to_vec();
851    out.extend_from_slice(&(bits.len() as u64).to_le_bytes());
852    for chunk in bits.chunks(8) {
853        let mut byte = 0u8;
854        for (i, &b) in chunk.iter().enumerate() {
855            if b {
856                byte |= 1 << i;
857            }
858        }
859        out.push(byte);
860    }
861    out
862}
863
864/// Run one full decode on `code` with `cfg`, sampling error stream `seed`.
865pub fn decode_once(code: &CssCode, cfg: &DecodeConfig, seed: u64) -> DecodeResult {
866    let t = Tanner::build(&code.checks, code.n);
867    let error = sample_error(code.n, cfg.p_fx, seed);
868    let syndrome_bits = syndrome_of(&code.checks, &error);
869    let core = relay_bp(&t, &syndrome_bits, seed, cfg.max_iter, cfg.max_legs);
870    // residual = error XOR correction
871    let residual: Vec<usize> = (0..code.n)
872        .filter(|&i| error[i] ^ core.correction[i])
873        .collect();
874    let logical_success = core.converged && code.is_trivial(&residual);
875    DecodeResult {
876        code_id: code.id.clone(),
877        n: code.n,
878        error: bits_to_ids(&error),
879        correction: bits_to_ids(&core.correction),
880        syndrome: bits_to_ids(&syndrome_bits),
881        converged: core.converged,
882        logical_success,
883        legs: core.legs,
884        iters: core.iters,
885        messages: core.messages,
886        syndrome_bits,
887        correction_bits: core.correction.clone(),
888        check_matrix_hash: content_hash(&code.check_matrix_bytes()),
889    }
890}
891
892impl DecodeResult {
893    /// Seal this decode into a signed, joule-metered receipt. `joules_micro` is the
894    /// decode's energy: on real hardware a RAPL/NVML meter; here a caller-supplied
895    /// modeled figure (the `messages` work is the exact, deterministic basis).
896    pub fn seal(
897        &self,
898        signer: &SigningKey,
899        signer_id: impl Into<String>,
900        joules_micro: u64,
901        grant: GrantRef,
902    ) -> DecodeReceipt {
903        DecodeReceipt::seal(
904            signer,
905            signer_id,
906            self.code_id.clone(),
907            "relay-bp.min-sum",
908            self.check_matrix_hash,
909            content_hash(&canonical_bits(b"wai:qec-syndrome\x01", &self.syndrome_bits)),
910            content_hash(&canonical_bits(b"wai:qec-correction\x01", &self.correction_bits)),
911            self.converged,
912            self.legs,
913            self.messages,
914            joules_micro,
915            grant,
916            None,
917        )
918    }
919
920    /// Canonical correction bytes (what the receipt's `correction_hash` binds).
921    pub fn correction_bytes(&self) -> Vec<u8> {
922        canonical_bits(b"wai:qec-correction\x01", &self.correction_bits)
923    }
924}
925
926/// A Monte-Carlo comparison of plain BP vs Relay-BP on the same error samples —
927/// the demonstration that relay is what makes BP work on quantum codes.
928#[derive(Clone, Debug, PartialEq, Eq)]
929pub struct BenchResult {
930    pub trials: u32,
931    pub plain_logical_failures: u32,
932    pub relay_logical_failures: u32,
933    pub relay_total_messages: u64,
934}
935
936/// Run `trials` decodes at rate `p`, each with plain BP (`max_legs=1`) and
937/// Relay-BP, over identical error samples, counting logical failures of each.
938/// A matching-style **union-find decoder** (Delfosse–Nickerson).
939///
940/// Belief propagation struggles on topological codes: they are degenerate and
941/// their Tanner graphs are full of short cycles, and the measurements in this
942/// module's tests show the consequence — under Relay-BP a distance-5 surface code
943/// decodes *worse* than distance-3. Union-find decodes the syndrome the way the
944/// code's structure actually wants: as a matching problem on a graph whose nodes
945/// are checks and whose edges are qubits.
946///
947/// Grow every odd cluster until no odd cluster remains, then peel each cluster's
948/// spanning forest from the leaves inward to read off a correction. Almost-linear
949/// time, and unlike BP it makes the distance count.
950///
951/// # Applicability
952///
953/// This only makes sense when a qubit touches at most two checks — true for
954/// surface and toric codes, false for the higher-degree qLDPC families (a
955/// bivariate-bicycle qubit sits in three). Returns `None` rather than guessing
956/// when the code is not a matching graph; use Relay-BP for those.
957/// A minimum-weight **logical Z** representative: the observable that decides
958/// whether an X-error chain flipped the encoded qubit.
959///
960/// The Z checks detect X errors, so failure is not "an error remained" but "the
961/// residual X chain crossed the code". A logical Z operator is exactly the ruler
962/// that measures that: it commutes with every X stabilizer, is not itself a
963/// product of Z checks, and its overlap with the residual has odd parity precisely
964/// when the logical qubit flipped.
965///
966/// Returned as the qubit support. Exhaustive by increasing weight, so it is a
967/// minimum-weight representative, not just some representative.
968pub fn logical_z_support(code: &CssCode, max_weight: usize) -> Option<Vec<usize>> {
969    let n = code.n;
970    let check_basis = Gf2Basis::from_rows(&code.checks, n);
971    for w in 1..=max_weight.min(n) {
972        let mut combo: Vec<usize> = (0..w).collect();
973        loop {
974            let mut bits = vec![false; n];
975            for &q in &combo {
976                bits[q] = true;
977            }
978            // commutes with every X stabilizer, and is not a product of Z checks
979            if syndrome_of(&code.stabs, &bits).iter().all(|b| !b) && !check_basis.contains(&combo) {
980                return Some(combo);
981            }
982            if !next_combination(&mut combo, n) {
983                break;
984            }
985        }
986    }
987    None
988}
989
990/// Advance `c` to the next `w`-combination of `0..n` in lexicographic order.
991/// Returns `false` once the last combination has been passed.
992fn next_combination(c: &mut [usize], n: usize) -> bool {
993    let w = c.len();
994    let mut i = w;
995    while i > 0 {
996        i -= 1;
997        if c[i] < n - (w - i) {
998            c[i] += 1;
999            for j in i + 1..w {
1000                c[j] = c[j - 1] + 1;
1001            }
1002            return true;
1003        }
1004    }
1005    false
1006}
1007
1008/// The code's **X-distance**: the least weight of an undetectable error that is
1009/// not a stabilizer — a logical operator.
1010///
1011/// This is the `d` in `[[n, k, d]]`, and it is the claim a code family actually
1012/// rests on: any error of weight below `d` is correctable, because no error that
1013/// small can be logical. Asserting `k` while leaving `d` unchecked verifies the
1014/// less interesting half of the name.
1015///
1016/// Searched exactly by increasing weight, so a returned `d` is a proof: every
1017/// lighter operator has been ruled out by exhaustion. `None` means no logical of
1018/// weight `≤ max_weight` exists. The search is exponential in the cap — `d ≤ 5`
1019/// is instant, `d = 7` is not.
1020pub fn x_distance(code: &CssCode, max_weight: usize) -> Option<usize> {
1021    let n = code.n;
1022    for w in 1..=max_weight.min(n) {
1023        let mut combo: Vec<usize> = (0..w).collect();
1024        loop {
1025            let mut bits = vec![false; n];
1026            for &q in &combo {
1027                bits[q] = true;
1028            }
1029            // undetectable (zero syndrome) and not a stabilizer => logical
1030            if syndrome_of(&code.checks, &bits).iter().all(|b| !b) && !code.is_trivial(&combo) {
1031                return Some(w);
1032            }
1033            if !next_combination(&mut combo, n) {
1034                break;
1035            }
1036        }
1037    }
1038    None
1039}
1040
1041/// The union-find core, on an abstract graph.
1042///
1043/// `edges` are `(u, v)` node pairs; `boundary` is the node that may absorb any
1044/// leftover parity (open boundaries, and the time boundary of a space-time graph).
1045/// Returns which edges the decoder selected.
1046///
1047/// Grow every odd cluster until none remain, then peel each cluster's spanning
1048/// forest from the leaves inward. Growth adds a **half-edge from each odd
1049/// endpoint**, so an edge joining two syndromes closes in one round while an edge
1050/// to a neutral node needs two — that asymmetry is what makes the decoder pair
1051/// syndromes with each other rather than draining them all to the boundary.
1052///
1053/// `weights` are per-edge costs: an edge closes at `2 × weight`, so cheap edges
1054/// close first. Uniform weights recover plain union-find; deriving them from the
1055/// error rates is what stops the decoder explaining a lying measurement with a
1056/// data correction when data errors are the less likely cause.
1057fn uf_decode_graph(
1058    nodes: usize,
1059    boundary: usize,
1060    edges: &[(usize, usize)],
1061    weights: &[u32],
1062    syndrome: &[bool],
1063) -> Vec<bool> {
1064    let mut parent: Vec<usize> = (0..nodes).collect();
1065    fn find(parent: &mut Vec<usize>, mut x: usize) -> usize {
1066        while parent[x] != x {
1067            parent[x] = parent[parent[x]];
1068            x = parent[x];
1069        }
1070        x
1071    }
1072    let mut syn: Vec<bool> = (0..nodes).map(|i| syndrome.get(i).copied().unwrap_or(false)).collect();
1073    syn[boundary] = false;
1074    let mut grown: Vec<u32> = vec![0; edges.len()];
1075    let maxw = weights.iter().copied().max().unwrap_or(1).max(1) as usize;
1076
1077    for _ in 0..(4 * nodes * maxw + 8) {
1078        let mut parity = vec![false; nodes];
1079        let mut touches = vec![false; nodes];
1080        for v in 0..nodes {
1081            let r = find(&mut parent, v);
1082            if syn[v] {
1083                parity[r] ^= true;
1084            }
1085            if v == boundary {
1086                touches[r] = true;
1087            }
1088        }
1089        if !(0..nodes).any(|v| find(&mut parent, v) == v && parity[v] && !touches[v]) {
1090            break;
1091        }
1092        let mut to_union: Vec<usize> = Vec::new();
1093        for (ei, &(a, b)) in edges.iter().enumerate() {
1094            if grown[ei] >= 2 * weights.get(ei).copied().unwrap_or(1).max(1) {
1095                continue;
1096            }
1097            let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
1098            let odd_a = parity[ra] && !touches[ra];
1099            let odd_b = parity[rb] && !touches[rb];
1100            let inc = if ra == rb { u8::from(odd_a) } else { u8::from(odd_a) + u8::from(odd_b) };
1101            if inc > 0 {
1102                let cap = 2 * weights.get(ei).copied().unwrap_or(1).max(1);
1103                grown[ei] = grown[ei].saturating_add(inc as u32);
1104                if grown[ei] >= cap {
1105                    to_union.push(ei);
1106                }
1107            }
1108        }
1109        for ei in to_union {
1110            let (a, b) = edges[ei];
1111            let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
1112            if ra != rb {
1113                parent[ra] = rb;
1114            }
1115        }
1116    }
1117
1118    let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); nodes];
1119    for (ei, &(a, b)) in edges.iter().enumerate() {
1120        if grown[ei] >= 2 * weights.get(ei).copied().unwrap_or(1).max(1) {
1121            adj[a].push((b, ei));
1122            adj[b].push((a, ei));
1123        }
1124    }
1125    let mut picked = vec![false; edges.len()];
1126    let mut seen = vec![false; nodes];
1127    for root in std::iter::once(boundary).chain(0..nodes) {
1128        if seen[root] {
1129            continue;
1130        }
1131        let mut order = vec![root];
1132        let mut parent_edge: Vec<Option<(usize, usize)>> = vec![None; nodes];
1133        seen[root] = true;
1134        let mut i = 0;
1135        while i < order.len() {
1136            let v = order[i];
1137            i += 1;
1138            for &(w, ei) in &adj[v] {
1139                if !seen[w] {
1140                    seen[w] = true;
1141                    parent_edge[w] = Some((v, ei));
1142                    order.push(w);
1143                }
1144            }
1145        }
1146        for &v in order.iter().rev() {
1147            if v == root || !syn[v] {
1148                continue;
1149            }
1150            if let Some((u, ei)) = parent_edge[v] {
1151                picked[ei] ^= true;
1152                syn[v] = false;
1153                syn[u] ^= true;
1154            }
1155        }
1156    }
1157    picked
1158}
1159
1160/// A matching-style **union-find decoder** (Delfosse–Nickerson) for one shot of
1161/// syndrome, assuming perfect measurement.
1162///
1163/// Belief propagation struggles on topological codes: they are degenerate and
1164/// their Tanner graphs are full of short cycles. Union-find decodes the syndrome
1165/// the way the code's structure wants — as matching on a graph whose nodes are
1166/// checks and whose edges are qubits — and unlike BP it makes the distance count.
1167///
1168/// # Applicability
1169///
1170/// Only meaningful when a qubit touches at most two checks: true for surface and
1171/// toric codes, false for higher-degree qLDPC (a bivariate-bicycle qubit sits in
1172/// three). Returns `None` rather than guessing; use Relay-BP for those.
1173pub fn union_find_correct(code: &CssCode, syndrome: &[bool]) -> Option<Vec<bool>> {
1174    let m = code.checks.len();
1175    let boundary = m;
1176    let mut owners: Vec<Vec<usize>> = vec![Vec::new(); code.n];
1177    for (ci, chk) in code.checks.iter().enumerate() {
1178        for &q in chk {
1179            owners[q].push(ci);
1180        }
1181    }
1182    let mut edges: Vec<(usize, usize)> = Vec::new();
1183    let mut edge_qubit: Vec<usize> = Vec::new();
1184    for (q, own) in owners.iter().enumerate() {
1185        match own.len() {
1186            0 => {}
1187            1 => {
1188                edges.push((own[0], boundary));
1189                edge_qubit.push(q);
1190            }
1191            2 => {
1192                edges.push((own[0], own[1]));
1193                edge_qubit.push(q);
1194            }
1195            _ => return None,
1196        }
1197    }
1198    let weights = vec![1u32; edges.len()];
1199    let picked = uf_decode_graph(m + 1, boundary, &edges, &weights, syndrome);
1200    let mut correction = vec![false; code.n];
1201    for (ei, &on) in picked.iter().enumerate() {
1202        if on {
1203            correction[edge_qubit[ei]] ^= true;
1204        }
1205    }
1206    Some(correction)
1207}
1208
1209/// Build the space-time matching graph for a block and decode it to a data-qubit
1210/// correction. Shared by the phenomenological and circuit-level models so both
1211/// decode through identical code — only the fault simulation differs.
1212fn phenom_correction(code: &CssCode, cfg: &PhenomConfig, detector_bits: &[bool]) -> Option<Vec<bool>> {
1213    let m = code.checks.len();
1214    let t_rounds = cfg.rounds.max(1) as usize;
1215    let layers = t_rounds + 1;
1216    let mut owners: Vec<Vec<usize>> = vec![Vec::new(); code.n];
1217    for (ci, chk) in code.checks.iter().enumerate() {
1218        for &q in chk {
1219            owners[q].push(ci);
1220        }
1221    }
1222    if owners.iter().any(|o| o.len() > 2) {
1223        return None;
1224    }
1225    // --- space-time matching graph ---
1226    let boundary = layers * m;
1227    let node = |c: usize, t: usize| t * m + c;
1228    // Cost of an edge is -ln(rate): the less likely a fault, the more the decoder
1229    // must pay to blame it. A rate of zero means the fault cannot happen, so the
1230    // edge is omitted rather than merely made expensive.
1231    let cost = |rate_fx: i64| -> u32 {
1232        let r = rate_fx as f64 / 4096.0;
1233        if r <= 0.0 { 0 } else { (-r.ln()).round().clamp(1.0, 12.0) as u32 }
1234    };
1235    let w_space = cost(cfg.p_data_fx);
1236    let w_time = cost(cfg.q_meas_fx);
1237    let mut edges: Vec<(usize, usize)> = Vec::new();
1238    let mut weights: Vec<u32> = Vec::new();
1239    // space edges: a data error at layer t lights its two checks at layer t
1240    let mut space_qubit: Vec<Option<usize>> = Vec::new();
1241    if w_space > 0 {
1242        for t in 0..layers {
1243            for (q, own) in owners.iter().enumerate() {
1244                match own.len() {
1245                    1 => {
1246                        edges.push((node(own[0], t), boundary));
1247                        weights.push(w_space);
1248                        space_qubit.push(Some(q));
1249                    }
1250                    2 => {
1251                        edges.push((node(own[0], t), node(own[1], t)));
1252                        weights.push(w_space);
1253                        space_qubit.push(Some(q));
1254                    }
1255                    _ => {}
1256                }
1257            }
1258        }
1259    }
1260    // time edges: a measurement error at round t lights the same check at t and t+1
1261    if w_time > 0 {
1262        for t in 0..layers - 1 {
1263            for c in 0..m {
1264                edges.push((node(c, t), node(c, t + 1)));
1265                weights.push(w_time);
1266                space_qubit.push(None);
1267            }
1268        }
1269    }
1270
1271    let picked = uf_decode_graph(boundary + 1, boundary, &edges, &weights, detector_bits);
1272    let mut correction = vec![false; code.n];
1273    for (ei, &on) in picked.iter().enumerate() {
1274        if on && let Some(q) = space_qubit[ei] {
1275            correction[q] ^= true;
1276        }
1277    }
1278
1279    Some(correction)
1280}
1281
1282/// Repeated-round noise with **faulty syndrome measurement** — the
1283/// phenomenological model.
1284///
1285/// Single-shot decoding assumes the syndrome itself is read perfectly, which no
1286/// device does. Here each of `rounds` measurement rounds can misreport a check,
1287/// so a lone flipped bit is ambiguous: a data error, or a lying measurement?
1288/// The resolution is to decode in **space-time**. Detectors are *differences*
1289/// between consecutive rounds, so a measurement error lights two detectors in the
1290/// same check at adjacent times, while a data error lights two checks at the same
1291/// time. A final perfect round closes the block.
1292#[derive(Clone, Copy, Debug)]
1293pub struct PhenomConfig {
1294    /// Noisy measurement rounds (a perfect readout round is appended).
1295    pub rounds: u32,
1296    /// Data error per qubit per round (fixed-point, scale 2^12).
1297    pub p_data_fx: i64,
1298    /// Measurement error per check per noisy round (fixed-point, scale 2^12).
1299    pub q_meas_fx: i64,
1300}
1301
1302impl PhenomConfig {
1303    pub fn from_p(rounds: u32, p_data: f64, q_meas: f64) -> PhenomConfig {
1304        PhenomConfig {
1305            rounds,
1306            p_data_fx: (p_data * 4096.0) as i64,
1307            q_meas_fx: (q_meas * 4096.0) as i64,
1308        }
1309    }
1310}
1311
1312/// One space-time decode.
1313#[derive(Clone, Debug)]
1314pub struct PhenomResult {
1315    pub rounds: u32,
1316    /// Detectors that fired, as `round * n_checks + check`.
1317    pub detectors: Vec<u32>,
1318    /// Accumulated data error over the whole block.
1319    pub data_error: Vec<u32>,
1320    pub correction: Vec<u32>,
1321    pub logical_success: bool,
1322}
1323
1324/// Decode a phenomenological-noise block with union-find over the space-time
1325/// graph. Returns `None` when the code is not a matching graph.
1326pub fn decode_phenomenological(
1327    code: &CssCode,
1328    cfg: &PhenomConfig,
1329    seed: u64,
1330) -> Option<PhenomResult> {
1331    let m = code.checks.len();
1332    let t_rounds = cfg.rounds.max(1) as usize;
1333    let layers = t_rounds + 1; // + the final perfect readout
1334
1335    // qubit -> its checks (matching graph precondition)
1336    let mut owners: Vec<Vec<usize>> = vec![Vec::new(); code.n];
1337    for (ci, chk) in code.checks.iter().enumerate() {
1338        for &q in chk {
1339            owners[q].push(ci);
1340        }
1341    }
1342    if owners.iter().any(|o| o.len() > 2) {
1343        return None;
1344    }
1345
1346    // --- simulate the block ---
1347    let mut st = seed ^ 0x51ED_2701_A55A_1234;
1348    let draw = |rate: i64, st: &mut u64| -> bool {
1349        *st = st
1350            .wrapping_mul(6364136223846793005)
1351            .wrapping_add(1442695040888963407);
1352        (((*st >> 40) & 0xFFF) as i64) < rate
1353    };
1354    let mut cum_err = vec![false; code.n];
1355    let mut s_prev = vec![false; m];
1356    let mut detector_bits = vec![false; layers * m];
1357    for t in 0..layers {
1358        if t < t_rounds {
1359            for q in 0..code.n {
1360                if draw(cfg.p_data_fx, &mut st) {
1361                    cum_err[q] ^= true;
1362                }
1363            }
1364        }
1365        let s_true = syndrome_of(&code.checks, &cum_err);
1366        let mut s_obs = s_true.clone();
1367        if t < t_rounds {
1368            for c in 0..m {
1369                if draw(cfg.q_meas_fx, &mut st) {
1370                    s_obs[c] ^= true;
1371                }
1372            }
1373        }
1374        for c in 0..m {
1375            detector_bits[t * m + c] = s_obs[c] ^ s_prev[c];
1376        }
1377        s_prev = s_obs;
1378    }
1379
1380    let correction = phenom_correction(code, cfg, &detector_bits)?;
1381    let residual: Vec<usize> = (0..code.n).filter(|&i| cum_err[i] ^ correction[i]).collect();
1382    Some(PhenomResult {
1383        rounds: cfg.rounds,
1384        detectors: bits_to_ids(&detector_bits),
1385        data_error: bits_to_ids(&cum_err),
1386        correction: bits_to_ids(&correction),
1387        logical_success: code.is_trivial(&residual),
1388    })
1389}
1390
1391/// Circuit-level noise: syndrome extraction that can fault mid-circuit.
1392///
1393/// Phenomenological noise treats a measurement as a black box that occasionally
1394/// lies. A real extraction round is a *circuit* — an ancilla and a sequence of
1395/// CNOTs — and a fault inside it does not stay put. A fault on a stabilizer's
1396/// ancilla propagates through every CNOT still to come, landing as X errors on
1397/// all the data qubits that circuit has not touched yet: a **hook error**.
1398///
1399/// Hook errors are why circuit-level thresholds sit well below phenomenological
1400/// ones. They are *correlated* — one fault, several data qubits, always adjacent
1401/// in the code — and a matching graph has no edge for them, so the decoder must
1402/// explain one fault with several, and a weight-`⌈d/2⌉` hook can already reach
1403/// across a distance-`d` code.
1404///
1405/// # Scope, stated plainly
1406///
1407/// This models the fault mechanism that distinguishes circuit-level noise —
1408/// ancilla faults propagating along a fixed CNOT order — on top of the data and
1409/// measurement faults of [`decode_phenomenological`]. It is not a full
1410/// gate-by-gate simulation of both stabilizer families with every propagation
1411/// path; `hook_fx = 0` reduces it exactly to the phenomenological model.
1412#[derive(Clone, Copy, Debug)]
1413pub struct CircuitConfig {
1414    pub rounds: u32,
1415    /// Data error per qubit per round.
1416    pub p_data_fx: i64,
1417    /// Ancilla measurement flip per check per round.
1418    pub q_meas_fx: i64,
1419    /// Ancilla fault per stabilizer per round, propagating as a hook error.
1420    pub hook_fx: i64,
1421}
1422
1423impl CircuitConfig {
1424    pub fn from_p(rounds: u32, p_data: f64, q_meas: f64, hook: f64) -> CircuitConfig {
1425        CircuitConfig {
1426            rounds,
1427            p_data_fx: (p_data * 4096.0) as i64,
1428            q_meas_fx: (q_meas * 4096.0) as i64,
1429            hook_fx: (hook * 4096.0) as i64,
1430        }
1431    }
1432    fn phenom(&self) -> PhenomConfig {
1433        PhenomConfig { rounds: self.rounds, p_data_fx: self.p_data_fx, q_meas_fx: self.q_meas_fx }
1434    }
1435}
1436
1437/// One circuit-level decode. `hook_events` records how many ancilla faults fired
1438/// and the data weight each landed as — the mechanism, visible rather than buried.
1439#[derive(Clone, Debug)]
1440pub struct CircuitResult {
1441    pub rounds: u32,
1442    pub data_error: Vec<u32>,
1443    pub correction: Vec<u32>,
1444    pub hook_events: Vec<u32>,
1445    pub logical_success: bool,
1446}
1447
1448/// Decode a block under circuit-level noise. `None` when the code is not a
1449/// matching graph.
1450pub fn decode_circuit_level(
1451    code: &CssCode,
1452    cfg: &CircuitConfig,
1453    seed: u64,
1454) -> Option<CircuitResult> {
1455    let m = code.checks.len();
1456    let t_rounds = cfg.rounds.max(1) as usize;
1457    let layers = t_rounds + 1;
1458
1459    let mut owners: Vec<Vec<usize>> = vec![Vec::new(); code.n];
1460    for (ci, chk) in code.checks.iter().enumerate() {
1461        for &q in chk {
1462            owners[q].push(ci);
1463        }
1464    }
1465    if owners.iter().any(|o| o.len() > 2) {
1466        return None;
1467    }
1468
1469    // Same stream as decode_phenomenological, and the hook draws below are
1470    // guarded, so hook_fx = 0 reproduces that model bit-for-bit rather than merely
1471    // resembling it — a claim worth being able to test.
1472    let mut st = seed ^ 0x51ED_2701_A55A_1234;
1473    let draw = |rate: i64, st: &mut u64| -> bool {
1474        *st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1475        (((*st >> 40) & 0xFFF) as i64) < rate
1476    };
1477    let pick = |bound: usize, st: &mut u64| -> usize {
1478        *st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1479        if bound == 0 { 0 } else { ((*st >> 33) as usize) % bound }
1480    };
1481
1482    let mut cum_err = vec![false; code.n];
1483    let mut s_prev = vec![false; m];
1484    let mut detector_bits = vec![false; layers * m];
1485    let mut hook_events: Vec<u32> = Vec::new();
1486
1487    for t in 0..layers {
1488        if t < t_rounds {
1489            for q in 0..code.n {
1490                if draw(cfg.p_data_fx, &mut st) {
1491                    cum_err[q] ^= true;
1492                }
1493            }
1494            // Ancilla faults: propagate along the remaining CNOTs of that
1495            // stabilizer's extraction circuit.
1496            if cfg.hook_fx > 0 {
1497                for stab in &code.stabs {
1498                    if !stab.is_empty() && draw(cfg.hook_fx, &mut st) {
1499                        let j = pick(stab.len(), &mut st);
1500                        for &q in &stab[j..] {
1501                            cum_err[q] ^= true;
1502                        }
1503                        hook_events.push((stab.len() - j) as u32);
1504                    }
1505                }
1506            }
1507        }
1508        let s_true = syndrome_of(&code.checks, &cum_err);
1509        let mut s_obs = s_true.clone();
1510        if t < t_rounds {
1511            for c in 0..m {
1512                if draw(cfg.q_meas_fx, &mut st) {
1513                    s_obs[c] ^= true;
1514                }
1515            }
1516        }
1517        for c in 0..m {
1518            detector_bits[t * m + c] = s_obs[c] ^ s_prev[c];
1519        }
1520        s_prev = s_obs;
1521    }
1522
1523    let correction = phenom_correction(code, &cfg.phenom(), &detector_bits)?;
1524    let residual: Vec<usize> = (0..code.n).filter(|&i| cum_err[i] ^ correction[i]).collect();
1525    Some(CircuitResult {
1526        rounds: cfg.rounds,
1527        data_error: bits_to_ids(&cum_err),
1528        correction: bits_to_ids(&correction),
1529        hook_events,
1530        logical_success: code.is_trivial(&residual),
1531    })
1532}
1533
1534/// One independent error mechanism of a detector error model.
1535#[derive(Clone, Debug, PartialEq, Eq)]
1536pub struct DemMechanism {
1537    /// Detectors flipped, as `round * n_checks + check`, sorted.
1538    pub detectors: Vec<u32>,
1539    /// Whether the mechanism flips the logical observable.
1540    pub flips_observable: bool,
1541    /// Probability, in parts per million (fixed point keeps the model portable).
1542    pub probability_ppm: u32,
1543}
1544
1545/// Export the noise model as a **detector error model** in Stim's DEM syntax.
1546///
1547/// A DEM says nothing about qubits or gates. It says: here are the independent
1548/// ways this experiment can fault, here is which detectors each one lights, and
1549/// here is whether it flips the logical observable. That is precisely the
1550/// interface every matching decoder wants, which is why it is the interchange
1551/// format of the QEC ecosystem — the analogue of what QIR is for circuits.
1552///
1553/// Emits `detector(check, round) D#` coordinates and one `error(p) D… L0` line per
1554/// mechanism: data errors, measurement errors, and — when `hook_fx > 0` — one line
1555/// per ancilla-fault split.
1556///
1557/// Whether a hook becomes a *hyperedge* depends on the CNOT order, which is real
1558/// physics rather than an implementation detail: scheduling is how surface-code
1559/// circuits control hook damage. With the index order used here, a partial hook on
1560/// the rotated surface code lights at most two detectors, so the emitted model is
1561/// an ordinary graph. The hook still costs accuracy — it flips several *data*
1562/// qubits while presenting as one edge, and that mismatch is the damage — but a
1563/// consumer of this DEM should not assume hyperedges appear.
1564///
1565/// Returns `None` when the code is not a matching graph.
1566pub fn to_dem(code: &CssCode, cfg: &CircuitConfig, max_logical_weight: usize) -> Option<String> {
1567    let mechs = dem_mechanisms(code, cfg, max_logical_weight)?;
1568    let m = code.checks.len();
1569    let layers = cfg.rounds.max(1) as usize + 1;
1570    let mut s = String::new();
1571    s.push_str("# detector error model emitted by wai-quantum\n");
1572    s.push_str(&format!("# code={} rounds={} detectors={}\n", code.id, cfg.rounds, layers * m));
1573    for t in 0..layers {
1574        for c in 0..m {
1575            s.push_str(&format!("detector({c}, {t}) D{}\n", t * m + c));
1576        }
1577    }
1578    for mech in &mechs {
1579        let p = mech.probability_ppm as f64 / 1.0e6;
1580        let mut line = format!("error({p:.9})");
1581        for d in &mech.detectors {
1582            line.push_str(&format!(" D{d}"));
1583        }
1584        if mech.flips_observable {
1585            line.push_str(" L0");
1586        }
1587        s.push('\n');
1588        s.truncate(s.len() - 1);
1589        s.push_str(&line);
1590        s.push('\n');
1591    }
1592    Some(s)
1593}
1594
1595/// The mechanisms behind [`to_dem`], as data — so they can be compared against
1596/// the simulator rather than eyeballed as text.
1597pub fn dem_mechanisms(
1598    code: &CssCode,
1599    cfg: &CircuitConfig,
1600    max_logical_weight: usize,
1601) -> Option<Vec<DemMechanism>> {
1602    let m = code.checks.len();
1603    let t_rounds = cfg.rounds.max(1) as usize;
1604
1605    let mut owners: Vec<Vec<usize>> = vec![Vec::new(); code.n];
1606    for (ci, chk) in code.checks.iter().enumerate() {
1607        for &q in chk {
1608            owners[q].push(ci);
1609        }
1610    }
1611    if owners.iter().any(|o| o.len() > 2) {
1612        return None;
1613    }
1614    let logical = logical_z_support(code, max_logical_weight)?;
1615    let flips = |qs: &[usize]| qs.iter().filter(|q| logical.contains(q)).count() % 2 == 1;
1616    let ppm = |fx: i64| ((fx as f64 / 4096.0) * 1.0e6).round().max(0.0) as u32;
1617
1618    let mut out: Vec<DemMechanism> = Vec::new();
1619    // data errors: light this qubit's checks, at that round only
1620    if cfg.p_data_fx > 0 {
1621        for t in 0..t_rounds {
1622            for q in 0..code.n {
1623                let mut ds: Vec<u32> = owners[q].iter().map(|&c| (t * m + c) as u32).collect();
1624                ds.sort_unstable();
1625                out.push(DemMechanism {
1626                    detectors: ds,
1627                    flips_observable: flips(&[q]),
1628                    probability_ppm: ppm(cfg.p_data_fx),
1629                });
1630            }
1631        }
1632    }
1633    // measurement errors: same check, adjacent rounds; never touch the observable
1634    if cfg.q_meas_fx > 0 {
1635        for t in 0..t_rounds {
1636            for c in 0..m {
1637                out.push(DemMechanism {
1638                    detectors: vec![(t * m + c) as u32, ((t + 1) * m + c) as u32],
1639                    flips_observable: false,
1640                    probability_ppm: ppm(cfg.q_meas_fx),
1641                });
1642            }
1643        }
1644    }
1645    // hook errors: one ancilla fault, several data qubits — a hyperedge
1646    if cfg.hook_fx > 0 {
1647        for t in 0..t_rounds {
1648            for stab in &code.stabs {
1649                if stab.is_empty() {
1650                    continue;
1651                }
1652                let each = ppm(cfg.hook_fx) / stab.len() as u32;
1653                for j in 0..stab.len() {
1654                    let qs = &stab[j..];
1655                    let mut set: Vec<u32> = Vec::new();
1656                    for &q in qs {
1657                        for &c in &owners[q] {
1658                            let d = (t * m + c) as u32;
1659                            if let Some(pos) = set.iter().position(|&x| x == d) {
1660                                set.remove(pos); // XOR: two flips cancel
1661                            } else {
1662                                set.push(d);
1663                            }
1664                        }
1665                    }
1666                    set.sort_unstable();
1667                    // A hook that covers the whole stabilizer IS that stabilizer:
1668                    // undetectable and harmless. Emitting it would put a target-less
1669                    // `error(p)` in the DEM, which is not a mechanism at all.
1670                    let obs = flips(qs);
1671                    if set.is_empty() && !obs {
1672                        continue;
1673                    }
1674                    out.push(DemMechanism {
1675                        detectors: set,
1676                        flips_observable: obs,
1677                        probability_ppm: each,
1678                    });
1679                }
1680            }
1681        }
1682    }
1683    Some(out)
1684}
1685
1686/// A parsed detector error model.
1687#[derive(Clone, Debug, PartialEq, Eq)]
1688pub struct DemModel {
1689    pub num_detectors: usize,
1690    pub mechanisms: Vec<DemMechanism>,
1691}
1692
1693/// Parse a detector error model in Stim's DEM syntax.
1694///
1695/// Accepts what [`to_dem`] emits and, more usefully, a DEM produced elsewhere —
1696/// the point of a format is that both directions work. `detector(...)` lines carry
1697/// only coordinates and are read for the detector count; `error(p) D… L…` lines
1698/// are the mechanisms. Unknown instructions are refused rather than skipped: a DEM
1699/// whose `repeat` block was silently ignored would decode a different experiment
1700/// from the one it describes.
1701pub fn from_dem(src: &str) -> Result<DemModel, String> {
1702    let mut mechanisms = Vec::new();
1703    let mut max_det: i64 = -1;
1704    for (lineno, raw) in src.lines().enumerate() {
1705        let line = raw.split('#').next().unwrap_or("").trim();
1706        if line.is_empty() {
1707            continue;
1708        }
1709        let head = line.split(['(', ' ']).next().unwrap_or("");
1710        match head {
1711            "detector" | "shift_detectors" | "logical_observable" | "observable_include" => {
1712                for tok in line.split_whitespace() {
1713                    if let Some(d) = tok.strip_prefix('D')
1714                        && let Ok(i) = d.trim_end_matches(',').parse::<i64>()
1715                    {
1716                        max_det = max_det.max(i);
1717                    }
1718                }
1719            }
1720            "error" => {
1721                let open = line.find('(').ok_or_else(|| format!("line {}: error without probability", lineno + 1))?;
1722                let close = line[open..].find(')').ok_or_else(|| format!("line {}: unclosed probability", lineno + 1))? + open;
1723                let p: f64 = line[open + 1..close]
1724                    .trim()
1725                    .parse()
1726                    .map_err(|_| format!("line {}: bad probability", lineno + 1))?;
1727                if !(0.0..=1.0).contains(&p) {
1728                    return Err(format!("line {}: probability {p} outside [0,1]", lineno + 1));
1729                }
1730                let mut detectors = Vec::new();
1731                let mut flips = false;
1732                for tok in line[close + 1..].split_whitespace() {
1733                    if let Some(d) = tok.strip_prefix('D') {
1734                        let i: u32 = d.parse().map_err(|_| format!("line {}: bad detector {tok}", lineno + 1))?;
1735                        max_det = max_det.max(i as i64);
1736                        detectors.push(i);
1737                    } else if let Some(l) = tok.strip_prefix('L') {
1738                        let _: u32 = l.parse().map_err(|_| format!("line {}: bad observable {tok}", lineno + 1))?;
1739                        flips = !flips;
1740                    } else {
1741                        return Err(format!("line {}: unexpected target {tok}", lineno + 1));
1742                    }
1743                }
1744                detectors.sort_unstable();
1745                if detectors.is_empty() && !flips {
1746                    continue; // a mechanism that does nothing
1747                }
1748                mechanisms.push(DemMechanism {
1749                    detectors,
1750                    flips_observable: flips,
1751                    probability_ppm: (p * 1.0e6).round().clamp(0.0, 1.0e6) as u32,
1752                });
1753            }
1754            other => return Err(format!("line {}: unsupported DEM instruction `{other}`", lineno + 1)),
1755        }
1756    }
1757    Ok(DemModel { num_detectors: (max_det + 1).max(0) as usize, mechanisms })
1758}
1759
1760/// What a DEM decode concluded.
1761#[derive(Clone, Debug)]
1762pub struct DemDecode {
1763    /// Indices of the mechanisms the decoder blamed.
1764    pub mechanisms: Vec<u32>,
1765    /// Whether the logical observable is predicted to have flipped.
1766    pub observable_flipped: bool,
1767}
1768
1769/// Decode a syndrome against a detector error model.
1770///
1771/// This is the whole point of the format: given somebody else's noise model and a
1772/// round of detector data, say whether the logical observable flipped. The
1773/// mechanisms become graph edges — one detector means an edge to the boundary, two
1774/// means an ordinary edge — weighted by `−ln p`, and union-find matches them.
1775///
1776/// Mechanisms touching three or more detectors are hyperedges, which matching
1777/// cannot represent. Rather than drop them and quietly decode a different model,
1778/// this refuses.
1779pub fn decode_dem(model: &DemModel, detectors: &[bool]) -> Result<DemDecode, String> {
1780    let nd = model.num_detectors;
1781    let boundary = nd;
1782    let mut edges: Vec<(usize, usize)> = Vec::new();
1783    let mut weights: Vec<u32> = Vec::new();
1784    let mut index: Vec<usize> = Vec::new();
1785    for (i, m) in model.mechanisms.iter().enumerate() {
1786        let w = {
1787            let p = m.probability_ppm as f64 / 1.0e6;
1788            if p <= 0.0 { continue } else { (-p.ln()).round().clamp(1.0, 12.0) as u32 }
1789        };
1790        match m.detectors.len() {
1791            0 => {}
1792            1 => {
1793                edges.push((m.detectors[0] as usize, boundary));
1794                weights.push(w);
1795                index.push(i);
1796            }
1797            2 => {
1798                edges.push((m.detectors[0] as usize, m.detectors[1] as usize));
1799                weights.push(w);
1800                index.push(i);
1801            }
1802            k => {
1803                return Err(format!(
1804                    "mechanism {i} touches {k} detectors; matching needs at most two — \
1805                     decompose the hyperedge or use a decoder that accepts one"
1806                ));
1807            }
1808        }
1809    }
1810    let picked = uf_decode_graph(nd + 1, boundary, &edges, &weights, detectors);
1811    let mut used = Vec::new();
1812    let mut flipped = false;
1813    for (ei, &on) in picked.iter().enumerate() {
1814        if on {
1815            let mi = index[ei];
1816            used.push(mi as u32);
1817            flipped ^= model.mechanisms[mi].flips_observable;
1818        }
1819    }
1820    Ok(DemDecode { mechanisms: used, observable_flipped: flipped })
1821}
1822
1823/// Logical failure rate under circuit-level noise.
1824pub fn benchmark_circuit_level(
1825    code: &CssCode,
1826    cfg: &CircuitConfig,
1827    trials: u32,
1828    seed: u64,
1829) -> (u32, u32) {
1830    let mut fails = 0;
1831    for tix in 0..trials {
1832        let s = seed.wrapping_add(tix as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1833        match decode_circuit_level(code, cfg, s) {
1834            Some(r) if r.logical_success => {}
1835            _ => fails += 1,
1836        }
1837    }
1838    (trials, fails)
1839}
1840
1841/// Logical failure rate under phenomenological noise.
1842pub fn benchmark_phenomenological(
1843    code: &CssCode,
1844    cfg: &PhenomConfig,
1845    trials: u32,
1846    seed: u64,
1847) -> (u32, u32) {
1848    let mut fails = 0;
1849    for tix in 0..trials {
1850        let s = seed.wrapping_add(tix as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1851        match decode_phenomenological(code, cfg, s) {
1852            Some(r) if r.logical_success => {}
1853            _ => fails += 1,
1854        }
1855    }
1856    (trials, fails)
1857}
1858
1859/// One union-find decode of a sampled error — the matching counterpart to
1860/// [`decode_once`].
1861pub fn decode_once_uf(code: &CssCode, cfg: &DecodeConfig, seed: u64) -> DecodeResult {
1862    let error = sample_error(code.n, cfg.p_fx, seed);
1863    let syndrome_bits = syndrome_of(&code.checks, &error);
1864    let (correction, converged) = match union_find_correct(code, &syndrome_bits) {
1865        Some(c) => (c, true),
1866        None => (vec![false; code.n], false),
1867    };
1868    let residual: Vec<usize> = (0..code.n).filter(|&i| error[i] ^ correction[i]).collect();
1869    let logical_success = converged && code.is_trivial(&residual);
1870    DecodeResult {
1871        code_id: code.id.clone(),
1872        n: code.n,
1873        error: bits_to_ids(&error),
1874        correction: bits_to_ids(&correction),
1875        syndrome: bits_to_ids(&syndrome_bits),
1876        converged,
1877        logical_success,
1878        legs: 0,
1879        iters: 0,
1880        messages: 0,
1881        syndrome_bits,
1882        correction_bits: correction,
1883        check_matrix_hash: content_hash(&code.check_matrix_bytes()),
1884    }
1885}
1886
1887/// Union-find logical failure rate over `trials` sampled errors.
1888pub fn benchmark_uf(code: &CssCode, p: f64, trials: u32, seed: u64) -> (u32, u32) {
1889    let cfg = DecodeConfig::from_p(p);
1890    let mut fails = 0;
1891    for tix in 0..trials {
1892        let s = seed.wrapping_add(tix as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1893        if !decode_once_uf(code, &cfg, s).logical_success {
1894            fails += 1;
1895        }
1896    }
1897    (trials, fails)
1898}
1899
1900pub fn benchmark(code: &CssCode, p: f64, trials: u32, seed: u64, max_legs: u32) -> BenchResult {
1901    let base = DecodeConfig::from_p(p);
1902    let plain = DecodeConfig { max_legs: 1, ..base };
1903    let relay = DecodeConfig { max_legs, ..base };
1904    let mut pf = 0;
1905    let mut rf = 0;
1906    let mut msg = 0u64;
1907    for tix in 0..trials {
1908        let s = seed.wrapping_add(tix as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1909        if !decode_once(code, &plain, s).logical_success {
1910            pf += 1;
1911        }
1912        let r = decode_once(code, &relay, s);
1913        if !r.logical_success {
1914            rf += 1;
1915        }
1916        msg += r.messages;
1917    }
1918    BenchResult { trials, plain_logical_failures: pf, relay_logical_failures: rf, relay_total_messages: msg }
1919}
1920
1921#[cfg(test)]
1922mod tests {
1923    use super::*;
1924
1925    fn key(s: u8) -> SigningKey {
1926        SigningKey::from_bytes(&[s; 32])
1927    }
1928
1929    /// The `[[7,1,3]]` claim, with the distance found by exhaustive search rather
1930    /// than asserted — the same standard the other codes here are held to.
1931    #[test]
1932    fn steane_is_a_distance_three_colour_code() {
1933        let c = CssCode::color_steane();
1934        assert_eq!(c.n, 7);
1935        assert_eq!(c.k, 1, "one logical qubit");
1936        assert_eq!(x_distance(&c, 5), Some(3), "distance must be found, not claimed");
1937        assert!(c.is_self_dual(), "a colour code's two stabilizer families coincide");
1938        assert!(c.has_transversal_hadamard());
1939    }
1940
1941    /// The contrast that makes the point. A surface code is *not* self-dual, so the
1942    /// one-round transversal Hadamard is unavailable to it — that is why it needs
1943    /// lattice surgery or code deformation for a logical H.
1944    #[test]
1945    fn the_surface_code_has_no_transversal_hadamard() {
1946        let s = CssCode::surface(3);
1947        assert!(!s.is_self_dual(), "surface X and Z families sit on different supports");
1948        assert!(!s.has_transversal_hadamard());
1949    }
1950
1951    /// Encoding really does land in the code space: every stabilizer of the code
1952    /// is satisfied by the prepared state, checked by syndrome rather than by
1953    /// construction.
1954    #[cfg(feature = "quantum")]
1955    #[test]
1956    fn the_encoder_prepares_a_codeword() {
1957        for code in [CssCode::color_steane(), CssCode::surface(3)] {
1958            let sv = code.encode_zero_circuit().simulate().unwrap();
1959            // |0>_L is a superposition of X-stabilizer row-space vectors, so every
1960            // basis state with nonzero amplitude must satisfy every Z check.
1961            for (i, a) in sv.amps.iter().enumerate() {
1962                if a.norm2() == 0 {
1963                    continue;
1964                }
1965                for chk in &code.checks {
1966                    let par = chk.iter().filter(|&&q| (i >> q) & 1 == 1).count() % 2;
1967                    assert_eq!(par, 0, "{} left basis state {i:b} outside the code", code.id);
1968                }
1969            }
1970        }
1971    }
1972
1973    /// The headline, verified end-to-end on the byte-exact simulator rather than
1974    /// argued: applying `H` to all seven physical qubits turns the logical `|0⟩`
1975    /// into the logical `|+⟩`. That is a logical Hadamard, done in one round with
1976    /// no ancillas and no surgery.
1977    #[cfg(feature = "quantum_info")]
1978    #[test]
1979    fn transversal_hadamard_implements_a_logical_hadamard() {
1980        use crate::quantum::ONE;
1981        use crate::quantum_info::{expect_pauli, Pauli};
1982
1983        let code = CssCode::color_steane();
1984        let zl: Vec<(u8, Pauli)> = (0..7).map(|q| (q, Pauli::Z)).collect();
1985        let xl: Vec<(u8, Pauli)> = (0..7).map(|q| (q, Pauli::X)).collect();
1986        let near = |got: i64, want: i64, what: &str| {
1987            assert!((got - want).abs() < 1 << 13, "{what}: got {got}, want {want}");
1988        };
1989
1990        // |0>_L is the +1 eigenstate of the logical Z and carries no logical X.
1991        let mut c = code.encode_zero_circuit();
1992        let sv = c.simulate().unwrap();
1993        near(expect_pauli(&sv, &zl), ONE, "<Z_L> on |0>_L");
1994        near(expect_pauli(&sv, &xl), 0, "<X_L> on |0>_L");
1995
1996        // Transversal H: one H on every physical qubit, nothing else.
1997        for q in 0..7 {
1998            c.h(q);
1999        }
2000        let out = c.simulate().unwrap();
2001
2002        // The logical operators have swapped — the definition of a logical H.
2003        near(expect_pauli(&out, &xl), ONE, "<X_L> after transversal H");
2004        near(expect_pauli(&out, &zl), 0, "<Z_L> after transversal H");
2005
2006        // And it never left the code space: every stabilizer still reads +1.
2007        for chk in &code.checks {
2008            let zs: Vec<(u8, Pauli)> = chk.iter().map(|&q| (q as u8, Pauli::Z)).collect();
2009            let xs: Vec<(u8, Pauli)> = chk.iter().map(|&q| (q as u8, Pauli::X)).collect();
2010            near(expect_pauli(&out, &zs), ONE, "Z stabilizer after transversal H");
2011            near(expect_pauli(&out, &xs), ONE, "X stabilizer after transversal H");
2012        }
2013    }
2014
2015    /// The same move on a surface code does not work, so the previous test is
2016    /// measuring the colour code's structure and not an artefact of the check.
2017    #[cfg(feature = "quantum_info")]
2018    #[test]
2019    fn transversal_hadamard_breaks_the_surface_code() {
2020        use crate::quantum::ONE;
2021        use crate::quantum_info::{expect_pauli, Pauli};
2022
2023        let code = CssCode::surface(3);
2024        let mut c = code.encode_zero_circuit();
2025        for q in 0..code.n as u8 {
2026            c.h(q);
2027        }
2028        let out = c.simulate().unwrap();
2029        let violated = code.checks.iter().any(|chk| {
2030            let zs: Vec<(u8, Pauli)> = chk.iter().map(|&q| (q as u8, Pauli::Z)).collect();
2031            (expect_pauli(&out, &zs) - ONE).abs() >= 1 << 13
2032        });
2033        assert!(violated, "transversal H must knock the surface code out of its code space");
2034    }
2035
2036    #[test]
2037    fn gf2_membership() {
2038        // rows {0,1}, {1,2} span {0,1},{1,2},{0,2}; not {0},{2}.
2039        let basis = Gf2Basis::from_rows(&[vec![0, 1], vec![1, 2]], 3);
2040        assert_eq!(basis.rank(), 2);
2041        assert!(basis.contains(&[0, 1]));
2042        assert!(basis.contains(&[1, 2]));
2043        assert!(basis.contains(&[0, 2]));
2044        assert!(basis.contains(&[])); // zero vector always in the space
2045        assert!(!basis.contains(&[0]));
2046        assert!(!basis.contains(&[2]));
2047    }
2048
2049
2050    /// A CSS code is only a code if the two families commute: every X stabilizer
2051    /// must meet every Z check in an even number of qubits. This is the test that
2052    /// decides whether a boundary convention is right — the first parities I tried
2053    /// for the surface code failed it.
2054    #[test]
2055    fn surface_code_is_a_valid_css_code() {
2056        for d in [3usize, 5, 7] {
2057            let code = CssCode::surface(d);
2058            assert_eq!(code.n, d * d, "d={d} qubit count");
2059            assert_eq!(code.k, 1, "d={d} must encode exactly one logical qubit");
2060            let x: Vec<&Vec<usize>> = code.stabs.iter().collect();
2061            for zc in &code.checks {
2062                for xs in &x {
2063                    let overlap = zc.iter().filter(|q| xs.contains(q)).count();
2064                    assert_eq!(overlap % 2, 0, "d={d}: X{xs:?} and Z{zc:?} anticommute");
2065                }
2066            }
2067            // stabilizer counts: (d^2-1)/2 of each family
2068            assert_eq!(code.checks.len(), (d * d - 1) / 2, "d={d} Z-check count");
2069            assert_eq!(x.len(), (d * d - 1) / 2, "d={d} X-stabilizer count");
2070            // planarity: every stabilizer is weight 4 (bulk) or weight 2 (boundary)
2071            for v in code.checks.iter().chain(x.iter().copied()) {
2072                assert!(v.len() == 4 || v.len() == 2, "d={d}: weight {} stabilizer", v.len());
2073            }
2074        }
2075    }
2076
2077    /// The decoder must actually protect the code, and Relay-BP must earn its
2078    /// name: plain belief propagation degrades on quantum codes (degeneracy plus
2079    /// short cycles), which is the whole reason the relay legs exist.
2080    #[test]
2081    fn surface_code_decodes() {
2082        let d3 = CssCode::surface(3);
2083        let r = benchmark(&d3, 0.01, 500, 7, 4);
2084        let relay = r.relay_logical_failures as f64 / r.trials as f64;
2085        assert!(relay <= 0.05, "d=3 at p=0.01 should rarely fail: {relay}");
2086        assert!(
2087            r.relay_logical_failures <= r.plain_logical_failures,
2088            "relay {} must not do worse than plain BP {}",
2089            r.relay_logical_failures,
2090            r.plain_logical_failures
2091        );
2092    }
2093
2094    /// Relay-BP's actual claim, on the code family it was designed for: the relay
2095    /// legs must beat plain belief propagation on a qLDPC (bivariate-bicycle) code.
2096    ///
2097    /// Note what is deliberately NOT asserted here: that a larger *topological*
2098    /// code decodes better. Measured across p = 0.001…0.02, distance-5 loses to
2099    /// distance-3 on both the surface code and the toric code — see
2100    /// `probe_surface_threshold`. That is a property of BP-family decoding on
2101    /// degenerate topological codes, not of the code constructions, and inventing
2102    /// a passing threshold for it would be asserting something untrue.
2103    #[test]
2104    fn relay_beats_plain_bp_on_qldpc() {
2105        let bb = CssCode::gross();
2106        let r = benchmark(&bb, 0.06, 400, 11, 16);
2107        assert!(
2108            r.relay_logical_failures < r.plain_logical_failures,
2109            "relay {} should beat plain BP {} on a qLDPC code",
2110            r.relay_logical_failures,
2111            r.plain_logical_failures
2112        );
2113    }
2114
2115    #[test]
2116    #[ignore]
2117    fn probe_surface_threshold() {
2118        println!("\n-- relay legs on the code Relay-BP was designed for: BB [[72,12]] --");
2119        let bb = CssCode::gross();
2120        println!(" p       legs=1(plain)  legs=4    legs=16");
2121        for &pp in &[0.02f64, 0.04, 0.06] {
2122            let f = |legs: u32| {
2123                let r = benchmark(&bb, pp, 400, 11, legs);
2124                r.relay_logical_failures as f64 / r.trials as f64
2125            };
2126            let plain = {
2127                let r = benchmark(&bb, pp, 400, 11, 4);
2128                r.plain_logical_failures as f64 / r.trials as f64
2129            };
2130            println!(" {pp:<8} {plain:<13.4} {:<9.4} {:<9.4}", f(4), f(16));
2131        }
2132        println!("\n-- the same sweep on topological codes --");
2133        println!(" p       surf d=3  surf d=5  toric L=3 toric L=5");
2134        for &pp in &[0.001f64, 0.005, 0.01] {
2135            let sf = |d: usize| {
2136                let r = benchmark(&CssCode::surface(d), pp, 1500, 11, 16);
2137                r.relay_logical_failures as f64 / r.trials as f64
2138            };
2139            let tf = |l: usize| {
2140                let r = benchmark(&CssCode::toric(l), pp, 1500, 11, 16);
2141                r.relay_logical_failures as f64 / r.trials as f64
2142            };
2143            println!(" {pp:<8} {:<9.4} {:<9.4} {:<9.4} {:<9.4}", sf(3), sf(5), tf(3), tf(5));
2144        }
2145    }
2146
2147
2148
2149    /// The property BP could not deliver: with a matching decoder, distance counts.
2150    /// Under Relay-BP a distance-5 surface code decoded WORSE than distance-3;
2151    /// union-find inverts that decisively.
2152    #[test]
2153    fn union_find_makes_distance_count() {
2154        let p = 0.01;
2155        let rate = |d: usize| {
2156            let (t, f) = benchmark_uf(&CssCode::surface(d), p, 1500, 11);
2157            f as f64 / t as f64
2158        };
2159        let (r3, r5) = (rate(3), rate(5));
2160        assert!(r5 < r3, "d=5 ({r5}) must beat d=3 ({r3}) below threshold");
2161        assert!(r5 <= 0.005, "d=5 at p={p} should almost never fail: {r5}");
2162    }
2163
2164    /// And it must beat belief propagation on the codes BP is bad at.
2165    #[test]
2166    fn union_find_beats_bp_on_topological_codes() {
2167        let p = 0.02;
2168        for d in [5usize, 7] {
2169            let code = CssCode::surface(d);
2170            let bp = {
2171                let r = benchmark(&code, p, 1000, 11, 16);
2172                r.relay_logical_failures as f64 / r.trials as f64
2173            };
2174            let (t, f) = benchmark_uf(&code, p, 1000, 11);
2175            let uf = f as f64 / t as f64;
2176            assert!(uf < bp, "d={d}: union-find {uf} should beat Relay-BP {bp}");
2177        }
2178    }
2179
2180    /// A qubit in three checks is not a matching graph. Refuse, do not guess —
2181    /// Relay-BP is the decoder for those families.
2182    #[test]
2183    fn union_find_refuses_non_matching_codes() {
2184        assert!(
2185            union_find_correct(&CssCode::gross(), &vec![false; CssCode::gross().checks.len()]).is_none(),
2186            "a bivariate-bicycle qubit sits in three checks; UF must decline"
2187        );
2188        assert!(union_find_correct(&CssCode::surface(5), &vec![false; 12]).is_some());
2189    }
2190
2191    /// Ground truth: every single-qubit error must be corrected exactly.
2192    #[test]
2193    fn union_find_corrects_every_single_qubit_error() {
2194        for d in [3usize, 5] {
2195            let code = CssCode::surface(d);
2196            for q in 0..code.n {
2197                let mut err = vec![false; code.n];
2198                err[q] = true;
2199                let syn = syndrome_of(&code.checks, &err);
2200                let corr = union_find_correct(&code, &syn).expect("matching graph");
2201                let residual: Vec<usize> = (0..code.n).filter(|&i| err[i] ^ corr[i]).collect();
2202                assert!(
2203                    code.is_trivial(&residual),
2204                    "d={d}: single error on qubit {q} left a logical residual {residual:?}"
2205                );
2206            }
2207        }
2208    }
2209
2210
2211
2212    /// Measurement errors alone must never become a logical error. With no data
2213    /// noise the true error is empty, so any space edge the decoder picks IS the
2214    /// residual — this asserts it explains lying measurements with time edges
2215    /// instead of inventing data corrections.
2216    #[test]
2217    fn measurement_noise_alone_never_causes_a_logical_error() {
2218        for d in [3usize, 5] {
2219            let code = CssCode::surface(d);
2220            let cfg = PhenomConfig::from_p(d as u32, 0.0, 0.05);
2221            for t in 0..300u64 {
2222                let r = decode_phenomenological(&code, &cfg, t.wrapping_mul(0x9E37_79B9))
2223                    .expect("matching graph");
2224                assert!(
2225                    r.logical_success,
2226                    "d={d} seed={t}: pure measurement noise produced a logical failure \
2227                     (correction {:?} on an empty data error)",
2228                    r.correction
2229                );
2230            }
2231        }
2232    }
2233
2234    /// Below threshold, distance must still buy protection when the syndrome
2235    /// itself is unreliable — the property that matters for a real device.
2236    #[test]
2237    fn phenomenological_distance_helps() {
2238        let p = 0.005;
2239        let rate = |d: usize| {
2240            let cfg = PhenomConfig::from_p(d as u32, p, p);
2241            let (t, f) = benchmark_phenomenological(&CssCode::surface(d), &cfg, 1200, 11);
2242            f as f64 / t as f64
2243        };
2244        let (r3, r5) = (rate(3), rate(5));
2245        assert!(r5 < r3, "d=5 ({r5}) must beat d=3 ({r3}) under noisy measurement");
2246    }
2247
2248    /// With perfect measurement the space-time decoder must reduce to the
2249    /// single-shot one.
2250    #[test]
2251    fn perfect_measurement_reduces_to_single_shot() {
2252        for d in [3usize, 5] {
2253            let code = CssCode::surface(d);
2254            let cfg = PhenomConfig::from_p(1, 0.02, 0.0);
2255            let (t1, f1) = benchmark_phenomenological(&code, &cfg, 1200, 11);
2256            let (t2, f2) = benchmark_uf(&code, 0.02, 1200, 11);
2257            let (a, b) = (f1 as f64 / t1 as f64, f2 as f64 / t2 as f64);
2258            assert!((a - b).abs() < 0.02, "d={d}: spacetime {a} vs single-shot {b}");
2259        }
2260    }
2261
2262    #[test]
2263    fn phenomenological_refuses_non_matching_codes() {
2264        let cfg = PhenomConfig::from_p(3, 0.01, 0.01);
2265        assert!(decode_phenomenological(&CssCode::gross(), &cfg, 1).is_none());
2266    }
2267
2268
2269    /// The `d` in `[[d², 1, d]]` — verified, not asserted by naming. A code whose
2270    /// distance is smaller than advertised silently corrects less than it claims.
2271    #[test]
2272    fn surface_code_distance_is_d() {
2273        for d in [3usize, 5] {
2274            let code = CssCode::surface(d);
2275            assert_eq!(
2276                x_distance(&code, d + 1),
2277                Some(d),
2278                "surface d={d} should have X-distance exactly {d}"
2279            );
2280        }
2281    }
2282
2283    /// The toric code's distance is L, and its k is 2 — a different point in the
2284    /// same family, so this also guards the distance search itself.
2285    #[test]
2286    fn toric_code_distance_is_l() {
2287        let code = CssCode::toric(3);
2288        assert_eq!(code.k, 2, "toric encodes two logical qubits");
2289        assert_eq!(x_distance(&code, 4), Some(3));
2290    }
2291
2292    /// Below the distance, correction must be guaranteed — not merely likely.
2293    /// Every error of weight < d, exhaustively, must decode to a trivial residual.
2294    #[test]
2295    fn union_find_corrects_every_error_below_the_distance() {
2296        for d in [3usize, 5] {
2297            let code = CssCode::surface(d);
2298            let w = (d - 1) / 2; // guaranteed-correctable weight
2299            let mut combo: Vec<usize> = (0..w).collect();
2300            let mut checked = 0u32;
2301            loop {
2302                let mut err = vec![false; code.n];
2303                for &q in &combo {
2304                    err[q] = true;
2305                }
2306                let syn = syndrome_of(&code.checks, &err);
2307                let corr = union_find_correct(&code, &syn).expect("matching graph");
2308                let residual: Vec<usize> =
2309                    (0..code.n).filter(|&i| err[i] ^ corr[i]).collect();
2310                assert!(
2311                    code.is_trivial(&residual),
2312                    "d={d}: weight-{w} error {combo:?} decoded to a logical residual {residual:?}"
2313                );
2314                checked += 1;
2315                if !next_combination(&mut combo, code.n) {
2316                    break;
2317                }
2318            }
2319            assert!(checked > 0, "d={d}: nothing checked");
2320        }
2321    }
2322
2323
2324
2325    /// With no ancilla faults, circuit-level noise IS phenomenological noise. Not
2326    /// "statistically similar" — the same stream, the same failures, bit for bit.
2327    /// A model that only approximately reduces to its own limiting case is hiding
2328    /// a difference somewhere.
2329    #[test]
2330    fn hooks_off_is_exactly_phenomenological() {
2331        for &pp in &[0.005f64, 0.02] {
2332            for d in [3usize, 5] {
2333                let code = CssCode::surface(d);
2334                let ph = PhenomConfig::from_p(d as u32, pp, pp);
2335                let cl = CircuitConfig::from_p(d as u32, pp, pp, 0.0);
2336                let (_, a) = benchmark_phenomenological(&code, &ph, 600, 11);
2337                let (_, b) = benchmark_circuit_level(&code, &cl, 600, 11);
2338                assert_eq!(a, b, "p={pp} d={d}: hook-free circuit level must equal phenomenological");
2339            }
2340        }
2341    }
2342
2343    /// Hook errors must cost something — that is the entire point of modelling
2344    /// them. One ancilla fault becomes several correlated data errors the matching
2345    /// graph has no edge for.
2346    #[test]
2347    fn hook_errors_cost_accuracy() {
2348        let (p, d) = (0.005, 5usize);
2349        let code = CssCode::surface(d);
2350        let off = CircuitConfig::from_p(d as u32, p, p, 0.0);
2351        let on = CircuitConfig::from_p(d as u32, p, p, p);
2352        let (_, f_off) = benchmark_circuit_level(&code, &off, 1500, 11);
2353        let (_, f_on) = benchmark_circuit_level(&code, &on, 1500, 11);
2354        assert!(f_on > f_off, "hooks on ({f_on}) should fail more than hooks off ({f_off})");
2355    }
2356
2357    /// A hook lands on the CNOTs a circuit has not reached yet, so it can never be
2358    /// heavier than the stabilizer it came from — and never empty.
2359    #[test]
2360    fn hook_weight_is_bounded_by_the_stabilizer() {
2361        let code = CssCode::surface(5);
2362        let maxw = code.stabs.iter().map(|s| s.len()).max().unwrap();
2363        let cfg = CircuitConfig::from_p(5, 0.0, 0.0, 0.2);
2364        let mut seen = 0;
2365        for t in 0..200u64 {
2366            let r = decode_circuit_level(&code, &cfg, t.wrapping_mul(0x9E37_79B9)).unwrap();
2367            for &w in &r.hook_events {
2368                assert!(w >= 1 && w as usize <= maxw, "hook weight {w} outside 1..={maxw}");
2369                seen += 1;
2370            }
2371        }
2372        assert!(seen > 0, "the probe never fired a hook");
2373    }
2374
2375    /// A noiseless circuit must never fail. Trivial, and exactly the kind of thing
2376    /// that quietly breaks when a model grows a new fault channel.
2377    #[test]
2378    fn noiseless_circuit_never_fails() {
2379        let code = CssCode::surface(5);
2380        let cfg = CircuitConfig::from_p(5, 0.0, 0.0, 0.0);
2381        for t in 0..200u64 {
2382            let r = decode_circuit_level(&code, &cfg, t).unwrap();
2383            assert!(r.logical_success && r.data_error.is_empty() && r.correction.is_empty());
2384        }
2385    }
2386
2387    #[test]
2388    fn circuit_level_refuses_non_matching_codes() {
2389        let cfg = CircuitConfig::from_p(3, 0.01, 0.01, 0.01);
2390        assert!(decode_circuit_level(&CssCode::gross(), &cfg, 1).is_none());
2391    }
2392
2393
2394    /// The logical Z must actually be the ruler it claims: it commutes with every
2395    /// X stabilizer, is no product of Z checks, and — the property that makes it
2396    /// usable — meets a minimum-weight logical X in an ODD number of qubits. Even
2397    /// overlap would mean it cannot detect that logical at all.
2398    #[test]
2399    fn logical_z_is_a_valid_observable() {
2400        for d in [3usize, 5] {
2401            let code = CssCode::surface(d);
2402            let lz = logical_z_support(&code, d + 1).expect("a logical Z exists");
2403            assert_eq!(lz.len(), d, "d={d}: minimum-weight logical Z should have weight {d}");
2404            // commutes with all X stabilizers
2405            let mut bits = vec![false; code.n];
2406            for &q in &lz { bits[q] = true; }
2407            assert!(syndrome_of(&code.stabs, &bits).iter().all(|b| !b));
2408            // and anticommutes with a minimum-weight logical X
2409            let dx = x_distance(&code, d + 1).expect("a logical X exists");
2410            assert_eq!(dx, d);
2411            // find that logical X and check odd overlap
2412            let mut combo: Vec<usize> = (0..d).collect();
2413            let mut found = false;
2414            loop {
2415                let mut e = vec![false; code.n];
2416                for &q in &combo { e[q] = true; }
2417                if syndrome_of(&code.checks, &e).iter().all(|b| !b) && !code.is_trivial(&combo) {
2418                    let overlap = combo.iter().filter(|q| lz.contains(q)).count();
2419                    assert_eq!(overlap % 2, 1, "d={d}: logical Z {lz:?} misses logical X {combo:?}");
2420                    found = true;
2421                    break;
2422                }
2423                if !next_combination(&mut combo, code.n) { break; }
2424            }
2425            assert!(found, "d={d}: no minimum-weight logical X found");
2426        }
2427    }
2428
2429
2430    /// The defining property of a logical error, stated purely in DEM terms: take
2431    /// the mechanisms of a minimum-weight logical operator, and their combined
2432    /// effect must light NO detectors while flipping the observable an ODD number
2433    /// of times. Undetectable, yet it changes the answer.
2434    ///
2435    /// A DEM that gets this wrong is worse than useless — an external decoder
2436    /// would silently mis-score every shot — and it checks the detector sets and
2437    /// the observable flags against each other rather than against my own formula.
2438    #[test]
2439    fn dem_encodes_the_codes_logical_structure() {
2440        for d in [3usize, 5] {
2441            let code = CssCode::surface(d);
2442            let cfg = CircuitConfig::from_p(2, 0.001, 0.001, 0.0);
2443            let mechs = dem_mechanisms(&code, &cfg, d + 1).expect("matching graph");
2444
2445            // a minimum-weight logical X
2446            let mut combo: Vec<usize> = (0..d).collect();
2447            let logical_x = loop {
2448                let mut e = vec![false; code.n];
2449                for &q in &combo { e[q] = true; }
2450                if syndrome_of(&code.checks, &e).iter().all(|b| !b) && !code.is_trivial(&combo) {
2451                    break combo.clone();
2452                }
2453                assert!(next_combination(&mut combo, code.n), "d={d}: no logical X of weight {d}");
2454            };
2455
2456            // its data mechanisms at round 0 are the first `n` entries
2457            let mut detectors: Vec<u32> = Vec::new();
2458            let mut obs = false;
2459            for &q in &logical_x {
2460                let mech = &mechs[q]; // round 0, qubit q
2461                obs ^= mech.flips_observable;
2462                for &dt in &mech.detectors {
2463                    if let Some(pos) = detectors.iter().position(|&x| x == dt) {
2464                        detectors.remove(pos);
2465                    } else {
2466                        detectors.push(dt);
2467                    }
2468                }
2469            }
2470            assert!(detectors.is_empty(), "d={d}: logical X {logical_x:?} lit detectors {detectors:?}");
2471            assert!(obs, "d={d}: logical X {logical_x:?} did not flip the observable");
2472        }
2473    }
2474
2475    /// A stabilizer is the converse case: also undetectable, but it must NOT flip
2476    /// the observable. Getting only the first half right would still be wrong.
2477    #[test]
2478    fn dem_says_stabilizers_are_harmless() {
2479        let d = 5usize;
2480        let code = CssCode::surface(d);
2481        let cfg = CircuitConfig::from_p(2, 0.001, 0.001, 0.0);
2482        let mechs = dem_mechanisms(&code, &cfg, d + 1).unwrap();
2483        for stab in &code.stabs {
2484            let mut detectors: Vec<u32> = Vec::new();
2485            let mut obs = false;
2486            for &q in stab {
2487                let mech = &mechs[q];
2488                obs ^= mech.flips_observable;
2489                for &dt in &mech.detectors {
2490                    if let Some(pos) = detectors.iter().position(|&x| x == dt) {
2491                        detectors.remove(pos);
2492                    } else {
2493                        detectors.push(dt);
2494                    }
2495                }
2496            }
2497            assert!(detectors.is_empty(), "stabilizer {stab:?} lit detectors {detectors:?}");
2498            assert!(!obs, "stabilizer {stab:?} flipped the observable");
2499        }
2500    }
2501
2502    /// The invariant that actually matters for a DEM: no mechanism may be
2503    /// undetectable while flipping the observable. Such a fault is a logical error
2504    /// no decoder could ever see coming, and a DEM containing one would make every
2505    /// external decoder silently mis-score.
2506    ///
2507    /// Note what is NOT asserted: that hooks appear as hyperedges. Whether they do
2508    /// depends on the CNOT order — with the index order used here a partial hook on
2509    /// the rotated surface code lights at most two detectors, so this model is an
2510    /// ordinary graph. Asserting hyperedges would have been asserting an assumption.
2511    #[test]
2512    fn no_dem_mechanism_is_an_invisible_logical() {
2513        for d in [3usize, 5] {
2514            let code = CssCode::surface(d);
2515            for hook in [0.0, 0.002] {
2516                let cfg = CircuitConfig::from_p(2, 0.001, 0.001, hook);
2517                for m in dem_mechanisms(&code, &cfg, d + 1).unwrap() {
2518                    assert!(
2519                        !(m.detectors.is_empty() && m.flips_observable),
2520                        "d={d} hook={hook}: undetectable mechanism flips the observable"
2521                    );
2522                    assert!(
2523                        !m.detectors.is_empty(),
2524                        "d={d} hook={hook}: a mechanism with no detectors should have been dropped"
2525                    );
2526                }
2527            }
2528        }
2529    }
2530
2531    #[test]
2532    fn dem_text_is_wellformed() {
2533        let code = CssCode::surface(3);
2534        let cfg = CircuitConfig::from_p(2, 0.002, 0.002, 0.001);
2535        let dem = to_dem(&code, &cfg, 4).unwrap();
2536        let n_det = 3 * code.checks.len(); // rounds+1 layers
2537        assert_eq!(dem.lines().filter(|l| l.starts_with("detector(")).count(), n_det);
2538        let errs: Vec<&str> = dem.lines().filter(|l| l.starts_with("error(")).collect();
2539        assert!(!errs.is_empty());
2540        for l in &errs {
2541            let p: f64 = l[6..l.find(')').unwrap()].parse().expect("probability parses");
2542            assert!(p > 0.0 && p < 1.0, "probability {p} out of range in {l}");
2543            assert!(l.contains(" D"), "mechanism with no detectors: {l}");
2544            for tok in l.split_whitespace().skip(1) {
2545                let idx: u32 = tok.trim_start_matches(['D', 'L']).parse().expect("target index");
2546                if tok.starts_with('D') {
2547                    assert!((idx as usize) < n_det, "detector {idx} out of range");
2548                } else {
2549                    assert_eq!(idx, 0, "only one observable is defined");
2550                }
2551            }
2552        }
2553        assert!(dem.contains("logical") || dem.contains("L0"), "no observable referenced");
2554    }
2555
2556
2557
2558
2559    /// A format that only writes is half a format.
2560    #[test]
2561    fn dem_round_trips() {
2562        for d in [3usize, 5] {
2563            let code = CssCode::surface(d);
2564            let cfg = CircuitConfig::from_p(2, 0.002, 0.003, 0.0);
2565            let text = to_dem(&code, &cfg, d + 1).unwrap();
2566            let parsed = from_dem(&text).expect("our own output must parse");
2567            let native = dem_mechanisms(&code, &cfg, d + 1).unwrap();
2568            assert_eq!(parsed.mechanisms.len(), native.len(), "d={d}: mechanism count");
2569            assert_eq!(parsed.mechanisms, native, "d={d}: mechanisms changed through the text");
2570            assert_eq!(parsed.num_detectors, (cfg.rounds as usize + 1) * code.checks.len());
2571        }
2572    }
2573
2574    /// The test that decides whether the export is worth anything: decode the SAME
2575    /// shots through the DEM and natively, and the DEM must do as well. If it does
2576    /// not, the exported model is missing information the decoder needs, and every
2577    /// external consumer would inherit that loss.
2578    #[test]
2579    fn decoding_through_the_dem_matches_the_native_decoder() {
2580        for d in [3usize, 5] {
2581            let code = CssCode::surface(d);
2582            let cfg = CircuitConfig::from_p(d as u32, 0.01, 0.01, 0.0);
2583            let phen = PhenomConfig { rounds: cfg.rounds, p_data_fx: cfg.p_data_fx, q_meas_fx: cfg.q_meas_fx };
2584            let model = from_dem(&to_dem(&code, &cfg, d + 1).unwrap()).unwrap();
2585            let logical = logical_z_support(&code, d + 1).unwrap();
2586            let m = code.checks.len();
2587            let n_det = (cfg.rounds as usize + 1) * m;
2588
2589            let (mut native_fail, mut dem_fail) = (0u32, 0u32);
2590            let trials = 600u32;
2591            for tix in 0..trials {
2592                let seed = (tix as u64).wrapping_add(7).wrapping_mul(0x9E37_79B9_7F4A_7C15);
2593                let r = decode_phenomenological(&code, &phen, seed).unwrap();
2594                if !r.logical_success {
2595                    native_fail += 1;
2596                }
2597                let mut bits = vec![false; n_det];
2598                for &dd in &r.detectors {
2599                    bits[dd as usize] = true;
2600                }
2601                let truth = r.data_error.iter().filter(|q| logical.contains(&(**q as usize))).count() % 2 == 1;
2602                let pred = decode_dem(&model, &bits).unwrap().observable_flipped;
2603                if pred != truth {
2604                    dem_fail += 1;
2605                }
2606            }
2607            let (a, b) = (native_fail as f64 / trials as f64, dem_fail as f64 / trials as f64);
2608            assert!(
2609                b <= a + 0.02,
2610                "d={d}: decoding through the DEM ({b}) lost accuracy vs native ({a})"
2611            );
2612        }
2613    }
2614
2615    /// Refuse rather than quietly decode a different experiment.
2616    #[test]
2617    fn dem_ingest_refuses_what_it_cannot_honour() {
2618        // a hyperedge cannot be matched
2619        let hyper = "error(0.001) D0 D1 D2\n";
2620        let model = from_dem(hyper).unwrap();
2621        let e = decode_dem(&model, &[false; 3]).unwrap_err();
2622        assert!(e.contains("three or more") || e.contains("at most two"), "{e}");
2623
2624        // an instruction we do not implement must not be skipped
2625        assert!(from_dem("repeat 5 {\n").unwrap_err().contains("unsupported"));
2626        assert!(from_dem("error(2.0) D0\n").unwrap_err().contains("outside"));
2627        assert!(from_dem("error(0.1) X3\n").unwrap_err().contains("unexpected target"));
2628    }
2629
2630    /// A foreign DEM — not our text — must decode. Two detectors, one boundary
2631    /// edge, one observable-flipping mechanism.
2632    #[test]
2633    fn ingests_a_hand_written_dem() {
2634        let src = "\
2635# hand written
2636detector(0, 0) D0
2637detector(1, 0) D1
2638error(0.01) D0 D1
2639error(0.02) D0 L0
2640error(0.02) D1 L0
2641";
2642        let model = from_dem(src).expect("parses");
2643        assert_eq!(model.num_detectors, 2);
2644        assert_eq!(model.mechanisms.len(), 3);
2645        // lighting both detectors is explained by the single cheap D0 D1 edge,
2646        // which does not touch the observable
2647        let r = decode_dem(&model, &[true, true]).unwrap();
2648        assert!(!r.observable_flipped, "the D0-D1 mechanism should explain it");
2649        // lighting one detector must go to the boundary, flipping the observable
2650        let r = decode_dem(&model, &[true, false]).unwrap();
2651        assert!(r.observable_flipped);
2652    }
2653
2654
2655
2656    #[test]
2657    #[ignore]
2658    fn probe_perf() {
2659        use std::time::Instant;
2660        println!("\n  decoder throughput (surface code, p=0.01)");
2661        for d in [3usize, 5, 7, 9, 11] {
2662            let code = CssCode::surface(d);
2663            let t0 = Instant::now();
2664            let (trials, _) = benchmark_uf(&code, 0.01, 500, 11);
2665            let el = t0.elapsed();
2666            println!("   d={d:<3} n={:<4} checks={:<4} {:>8.1} shots/s   ({:?}/shot)",
2667                code.n, code.checks.len(),
2668                trials as f64 / el.as_secs_f64(), el / trials);
2669        }
2670        println!("\n  space-time decoder (d rounds, p=q=0.01)");
2671        for d in [3usize, 5, 7] {
2672            let code = CssCode::surface(d);
2673            let cfg = PhenomConfig::from_p(d as u32, 0.01, 0.01);
2674            let t0 = Instant::now();
2675            let (trials, _) = benchmark_phenomenological(&code, &cfg, 200, 11);
2676            let el = t0.elapsed();
2677            println!("   d={d:<3} detectors={:<5} {:>8.1} shots/s", (d + 1) * code.checks.len(),
2678                trials as f64 / el.as_secs_f64());
2679        }
2680        println!("\n  code construction + distance verification");
2681        for d in [3usize, 5] {
2682            let t0 = Instant::now(); let c = CssCode::surface(d); let build = t0.elapsed();
2683            let t1 = Instant::now(); let dd = x_distance(&c, d + 1); let dist = t1.elapsed();
2684            println!("   d={d}: build {build:?}, distance {dd:?} in {dist:?}");
2685        }
2686    }
2687
2688    #[test]
2689    #[ignore]
2690    fn probe_dem_equivalence() {
2691        println!("\n  same shots, decoded natively vs through the exported DEM");
2692        println!("  d   p       native   via DEM");
2693        for d in [3usize, 5] {
2694            for &pp in &[0.005f64, 0.01, 0.02] {
2695                let code = CssCode::surface(d);
2696                let cfg = CircuitConfig::from_p(d as u32, pp, pp, 0.0);
2697                let phen = PhenomConfig { rounds: cfg.rounds, p_data_fx: cfg.p_data_fx, q_meas_fx: cfg.q_meas_fx };
2698                let model = from_dem(&to_dem(&code, &cfg, d + 1).unwrap()).unwrap();
2699                let logical = logical_z_support(&code, d + 1).unwrap();
2700                let n_det = (cfg.rounds as usize + 1) * code.checks.len();
2701                let (mut a, mut b) = (0u32, 0u32);
2702                let trials = 1500u32;
2703                for tix in 0..trials {
2704                    let seed = (tix as u64).wrapping_add(7).wrapping_mul(0x9E37_79B9_7F4A_7C15);
2705                    let r = decode_phenomenological(&code, &phen, seed).unwrap();
2706                    if !r.logical_success { a += 1; }
2707                    let mut bits = vec![false; n_det];
2708                    for &dd in &r.detectors { bits[dd as usize] = true; }
2709                    let truth = r.data_error.iter().filter(|q| logical.contains(&(**q as usize))).count() % 2 == 1;
2710                    if decode_dem(&model, &bits).unwrap().observable_flipped != truth { b += 1; }
2711                }
2712                println!("  {d}   {pp:<7} {:<8.4} {:<8.4}", a as f64/trials as f64, b as f64/trials as f64);
2713            }
2714        }
2715    }
2716
2717    #[test]
2718    #[ignore]
2719    fn probe_print_dem() {
2720        let code = CssCode::surface(3);
2721        let cfg = CircuitConfig::from_p(1, 0.002, 0.002, 0.001);
2722        let dem = to_dem(&code, &cfg, 4).unwrap();
2723        for l in dem.lines().take(22) { println!("{l}"); }
2724        println!("... ({} lines total)", dem.lines().count());
2725    }
2726
2727    #[test]
2728    #[ignore]
2729    fn probe_dem_shapes() {
2730        let code = CssCode::surface(5);
2731        let cfg = CircuitConfig::from_p(2, 0.001, 0.001, 0.001);
2732        let mechs = dem_mechanisms(&code, &cfg, 6).unwrap();
2733        let mut hist = std::collections::BTreeMap::new();
2734        for m in &mechs { *hist.entry(m.detectors.len()).or_insert(0u32) += 1; }
2735        println!("\n  detector-set size histogram: {hist:?}");
2736        let empty = mechs.iter().filter(|m| m.detectors.is_empty()).count();
2737        println!("  mechanisms with NO detectors: {empty} (of {})", mechs.len());
2738        let empty_obs = mechs.iter().filter(|m| m.detectors.is_empty() && m.flips_observable).count();
2739        println!("  empty AND flips observable (would be a silent logical): {empty_obs}");
2740        println!("  X stabilizer weights: {:?}", {
2741            let mut w: Vec<usize> = code.stabs.iter().map(|s| s.len()).collect(); w.sort_unstable(); w.dedup(); w });
2742        // what do partial hooks look like?
2743        for stab in code.stabs.iter().take(3) {
2744            print!("  stab {stab:?}: ");
2745            for j in 0..stab.len() {
2746                let qs = &stab[j..];
2747                let mut set: Vec<usize> = Vec::new();
2748                for &q in qs {
2749                    for (ci, chk) in code.checks.iter().enumerate() {
2750                        if chk.contains(&q) {
2751                            if let Some(pos) = set.iter().position(|&x| x == ci) { set.remove(pos); } else { set.push(ci); }
2752                        }
2753                    }
2754                }
2755                print!("j={j}->{} ", set.len());
2756            }
2757            println!();
2758        }
2759    }
2760
2761    #[test]
2762    #[ignore]
2763    fn probe_circuit_level() {
2764        println!("\n  hook=0 must reproduce phenomenological exactly");
2765        for &pp in &[0.005f64, 0.02] {
2766            for d in [3usize, 5] {
2767                let ph = PhenomConfig::from_p(d as u32, pp, pp);
2768                let cl = CircuitConfig::from_p(d as u32, pp, pp, 0.0);
2769                let (_, a) = benchmark_phenomenological(&CssCode::surface(d), &ph, 800, 11);
2770                let (_, b) = benchmark_circuit_level(&CssCode::surface(d), &cl, 800, 11);
2771                println!("   p={pp} d={d}: phenom {a}  circuit(hook=0) {b}  {}",
2772                    if a == b { "IDENTICAL" } else { "DIFFER" });
2773            }
2774        }
2775        println!("\n  hook errors lower the threshold (surface code, q=p, hook=p)");
2776        println!("  p        d=3       d=5       d=7");
2777        for &pp in &[0.001f64, 0.002, 0.005, 0.01] {
2778            let r = |d: usize| {
2779                let cfg = CircuitConfig::from_p(d as u32, pp, pp, pp);
2780                let (t, f) = benchmark_circuit_level(&CssCode::surface(d), &cfg, 1200, 11);
2781                f as f64 / t as f64
2782            };
2783            println!("  {pp:<8} {:<9.4} {:<9.4} {:<9.4}", r(3), r(5), r(7));
2784        }
2785        println!("\n  same p, hooks off vs on (d=5)");
2786        for &pp in &[0.002f64, 0.005] {
2787            let off = { let c=CircuitConfig::from_p(5,pp,pp,0.0); let (t,f)=benchmark_circuit_level(&CssCode::surface(5),&c,1200,11); f as f64/t as f64 };
2788            let on  = { let c=CircuitConfig::from_p(5,pp,pp,pp);  let (t,f)=benchmark_circuit_level(&CssCode::surface(5),&c,1200,11); f as f64/t as f64 };
2789            println!("   p={pp:<7} hooks off {:<8.4} hooks on {:<8.4}", off, on);
2790        }
2791    }
2792
2793    #[test]
2794    #[ignore]
2795    fn probe_phenomenological() {
2796        println!("\n  surface code, phenomenological noise (data p, measurement q=p), d rounds");
2797        println!("  p        d=3       d=5       d=7");
2798        for &pp in &[0.001f64, 0.003, 0.005, 0.01, 0.02] {
2799            let r = |d: usize| {
2800                let cfg = PhenomConfig::from_p(d as u32, pp, pp);
2801                let (t, f) = benchmark_phenomenological(&CssCode::surface(d), &cfg, 1200, 11);
2802                f as f64 / t as f64
2803            };
2804            println!("  {pp:<8} {:<9.4} {:<9.4} {:<9.4}", r(3), r(5), r(7));
2805        }
2806        println!("\n  sanity: q=0 (perfect measurement) should track the single-shot decoder");
2807        for &pp in &[0.005f64, 0.02] {
2808            let st = |d: usize| {
2809                let cfg = PhenomConfig::from_p(1, pp, 0.0);
2810                let (t, f) = benchmark_phenomenological(&CssCode::surface(d), &cfg, 1200, 11);
2811                f as f64 / t as f64
2812            };
2813            let ss = |d: usize| { let (t,f)=benchmark_uf(&CssCode::surface(d), pp, 1200, 11); f as f64/t as f64 };
2814            println!("  p={pp:<7} d=3 spacetime {:<8.4} single-shot {:<8.4} | d=5 spacetime {:<8.4} single-shot {:<8.4}",
2815                st(3), ss(3), st(5), ss(5));
2816        }
2817    }
2818
2819    #[test]
2820    #[ignore]
2821    fn probe_union_find_vs_bp() {
2822        println!("\n  surface code — logical failure rate");
2823        println!("  p        d=3 BP    d=3 UF    d=5 BP    d=5 UF    d=7 BP    d=7 UF");
2824        for &pp in &[0.001f64, 0.005, 0.01, 0.02, 0.05] {
2825            let bp = |d: usize| {
2826                let r = benchmark(&CssCode::surface(d), pp, 2000, 11, 16);
2827                r.relay_logical_failures as f64 / r.trials as f64
2828            };
2829            let uf = |d: usize| {
2830                let (t, f) = benchmark_uf(&CssCode::surface(d), pp, 2000, 11);
2831                f as f64 / t as f64
2832            };
2833            println!("  {pp:<8} {:<9.4} {:<9.4} {:<9.4} {:<9.4} {:<9.4} {:<9.4}",
2834                bp(3), uf(3), bp(5), uf(5), bp(7), uf(7));
2835        }
2836        println!("\n  toric code — UF");
2837        for &pp in &[0.005f64, 0.02] {
2838            let uf = |l: usize| { let (t,f)=benchmark_uf(&CssCode::toric(l), pp, 2000, 11); f as f64/t as f64 };
2839            println!("  p={pp:<7} L=3 {:<8.4} L=5 {:<8.4}", uf(3), uf(5));
2840        }
2841    }
2842
2843    #[test]
2844    fn toric_code_shape() {
2845        let c = CssCode::toric(5);
2846        assert_eq!(c.n, 50);
2847        assert_eq!(c.checks.len(), 25);
2848        assert_eq!(c.k, 2, "toric code encodes 2 logical qubits");
2849        // every check has weight 4
2850        assert!(c.checks.iter().all(|r| r.len() == 4));
2851    }
2852
2853    #[test]
2854    fn gross_code_is_72_12() {
2855        let c = CssCode::gross();
2856        assert_eq!(c.n, 72);
2857        assert_eq!(c.k, 12, "the gross-style BB code encodes 12 logical qubits");
2858        // qLDPC: bounded check weight (6 here), independent of n
2859        assert!(c.checks.iter().all(|r| r.len() == 6));
2860    }
2861
2862    #[test]
2863    fn css_orthogonality_gross() {
2864        // H_X · H_Zᵀ = 0: every Z-check commutes with every X-stabilizer, i.e.
2865        // every decode check overlaps every stabilizer in an even number of qubits.
2866        let c = CssCode::gross();
2867        // stab_basis holds H_X's row space; reconstruct raw H_X rows to test.
2868        let hx = CssCode::bivariate_bicycle(6, 6, &[(3, 0), (0, 1), (0, 2)], &[(0, 3), (1, 0), (2, 0)]);
2869        for zc in &c.checks {
2870            let zset: std::collections::HashSet<usize> = zc.iter().copied().collect();
2871            for xs in raw_hx(&hx) {
2872                let overlap = xs.iter().filter(|q| zset.contains(q)).count();
2873                assert_eq!(overlap % 2, 0, "CSS orthogonality violated");
2874            }
2875        }
2876    }
2877
2878    // helper: recover raw H_X rows (stab side) for the orthogonality test
2879    fn raw_hx(_c: &CssCode) -> Vec<Vec<usize>> {
2880        // rebuild H_X = [A|B] directly
2881        let (l, m) = (6usize, 6usize);
2882        let a = [(3usize, 0usize), (0, 1), (0, 2)];
2883        let b = [(0usize, 3usize), (1, 0), (2, 0)];
2884        let lm = l * m;
2885        let cell = |i: usize, j: usize| (i % l) * m + (j % m);
2886        let build = |monos: &[(usize, usize)]| -> Vec<Vec<usize>> {
2887            let mut rows = vec![Vec::new(); lm];
2888            for i in 0..l {
2889                for j in 0..m {
2890                    let col = cell(i, j);
2891                    for &(px, py) in monos {
2892                        rows[cell(i + px, j + py)].push(col);
2893                    }
2894                }
2895            }
2896            rows
2897        };
2898        let ar = build(&a);
2899        let br = build(&b);
2900        (0..lm)
2901            .map(|r| {
2902                let mut row = ar[r].clone();
2903                row.extend(br[r].iter().map(|&c| c + lm));
2904                row.sort_unstable();
2905                row
2906            })
2907            .collect()
2908    }
2909
2910    #[test]
2911    fn decode_recovers_low_weight_errors() {
2912        // At a low error rate, decoding should almost always succeed logically.
2913        let c = CssCode::toric(5);
2914        let cfg = DecodeConfig::from_p(0.02);
2915        let mut fails = 0;
2916        for s in 0..200u64 {
2917            if !decode_once(&c, &cfg, s.wrapping_mul(0x9E37_79B9)).logical_success {
2918                fails += 1;
2919            }
2920        }
2921        assert!(fails < 20, "too many logical failures at p=0.02: {fails}/200");
2922    }
2923
2924    #[test]
2925    fn decode_is_deterministic() {
2926        let c = CssCode::gross();
2927        let cfg = DecodeConfig::from_p(0.03);
2928        let a = decode_once(&c, &cfg, 12345);
2929        let b = decode_once(&c, &cfg, 12345);
2930        assert_eq!(a.correction, b.correction);
2931        assert_eq!(a.messages, b.messages);
2932        assert_eq!(a.legs, b.legs);
2933    }
2934
2935    #[test]
2936    fn relay_beats_plain_bp() {
2937        // The whole point of Relay-BP: on a quantum code, relay legs recover
2938        // decodes that plain BP leaves failed. Relay failures ≤ plain failures.
2939        let c = CssCode::gross();
2940        let bench = benchmark(&c, 0.04, 150, 0xBEEF, 12);
2941        assert!(
2942            bench.relay_logical_failures <= bench.plain_logical_failures,
2943            "relay {} must not exceed plain {}",
2944            bench.relay_logical_failures, bench.plain_logical_failures
2945        );
2946    }
2947
2948    #[test]
2949    fn decode_seals_verifying_receipt() {
2950        let c = CssCode::gross();
2951        let cfg = DecodeConfig::from_p(0.03);
2952        let r = decode_once(&c, &cfg, 7);
2953        let rec = r.seal(&key(1), "did:key:lab", 500_000, GrantRef::unbounded("quantum.decode"));
2954        assert!(rec.verify());
2955        assert!(rec.correction_matches(&r.correction_bytes()));
2956        assert_eq!(rec.code_id, "bb:[[72,12]]");
2957        assert_eq!(rec.work_messages, r.messages);
2958    }
2959}
2960
2961