Skip to main content

oxicuda_quantum/error_correction/
surface_code.rs

1//! Rotated surface code with a minimum-weight perfect-matching (MWPM) decoder.
2//!
3//! This module implements a distance-`d` **rotated** surface code (Fowler et al.,
4//! *"Surface codes: Towards practical large-scale quantum computation"*, Phys.
5//! Rev. A 86, 032324, 2012) together with a syndrome decoder, in the **classical
6//! Pauli-frame** picture used by virtually every surface-code threshold study.
7//!
8//! ## Scope and conventions
9//!
10//! Rather than carrying a 2^{d²}-amplitude state vector through the stabilizer
11//! measurement schedule (which is unnecessary for tracking how Pauli errors map
12//! to logical errors), we track an error as an independent pair of `X`/`Z` bit
13//! masks over the `d²` data qubits — a [`PauliError`]. This is exact for Pauli
14//! noise: a stabilizer's syndrome bit is just the parity of the overlap between
15//! the stabilizer support and the offending error component, and a residual
16//! error commutes with every stabilizer iff it lies in the stabilizer group
17//! (no logical error). All correctness statements below are made in this frame.
18//!
19//! * **Layout.** `d²` data qubits sit on a `d × d` grid (index `r·d + c`).
20//!   The `d²−1` stabilizers form the rotated checkerboard: `(d−1)²` weight-4
21//!   bulk plaquettes plus `2(d−1)` weight-2 boundary plaquettes. `Z`-type
22//!   plaquettes detect `X` errors; `X`-type plaquettes detect `Z` errors. There
23//!   are exactly `(d²−1)/2` of each.
24//! * **Decoder.** For each error species we build the matching graph whose
25//!   nodes are the stabilizers of the detecting type plus one virtual boundary
26//!   node, and whose edges are data qubits. Decoding is an *exact* minimum-weight
27//!   perfect matching solved by subset (bitmask) dynamic programming over the
28//!   **defect set** — not the full stabilizer set — so the cost is `O(2^{k}·k)`
29//!   in the number of fired stabilizers `k`, which is tiny for the low-weight
30//!   errors a distance-`d` code is meant to handle. (A Blossom-V matching would
31//!   be the drop-in replacement for high-weight syndromes at large `d`; the DP
32//!   here is the same minimum-weight matching, just enumerated exactly.)
33//!
34//! For a distance-`d` code this corrects every error of weight `≤ ⌊(d−1)/2⌋`:
35//! the residual `error ∘ correction` is a cycle of weight below `d`, hence a
36//! product of stabilizers, hence logically trivial.
37
38use std::collections::VecDeque;
39
40use crate::error::{QuantumError, QuantumResult};
41
42/// Stabilizer species of a rotated-surface-code plaquette.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum StabKind {
45    /// `X`-type plaquette (product of `X` on its support); detects `Z` errors.
46    X,
47    /// `Z`-type plaquette (product of `Z` on its support); detects `X` errors.
48    Z,
49}
50
51/// A single stabilizer generator: its species and the data qubits it acts on.
52#[derive(Debug, Clone)]
53pub struct Stabilizer {
54    /// `X`- or `Z`-type.
55    pub kind: StabKind,
56    /// Indices (`r·d + c`) of the data qubits in the plaquette support.
57    pub support: Vec<usize>,
58}
59
60/// A Pauli error over the data qubits: independent `X` (bit-flip) and `Z`
61/// (phase-flip) components. `y = x · z` on a qubit is represented by both bits
62/// being set.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct PauliError {
65    /// `x[q] = true` ⇔ an `X` acts on data qubit `q`.
66    pub x: Vec<bool>,
67    /// `z[q] = true` ⇔ a `Z` acts on data qubit `q`.
68    pub z: Vec<bool>,
69}
70
71impl PauliError {
72    /// The trivial (identity) error on `num_qubits` data qubits.
73    #[must_use]
74    pub fn identity(num_qubits: usize) -> Self {
75        Self {
76            x: vec![false; num_qubits],
77            z: vec![false; num_qubits],
78        }
79    }
80
81    /// A single `X` error on data qubit `q`.
82    ///
83    /// # Errors
84    /// [`QuantumError::QubitIndexOutOfRange`] if `q >= num_qubits`.
85    pub fn single_x(num_qubits: usize, q: usize) -> QuantumResult<Self> {
86        if q >= num_qubits {
87            return Err(QuantumError::QubitIndexOutOfRange {
88                index: q,
89                n_qubits: num_qubits,
90            });
91        }
92        let mut e = Self::identity(num_qubits);
93        e.x[q] = true;
94        Ok(e)
95    }
96
97    /// A single `Z` error on data qubit `q`.
98    ///
99    /// # Errors
100    /// [`QuantumError::QubitIndexOutOfRange`] if `q >= num_qubits`.
101    pub fn single_z(num_qubits: usize, q: usize) -> QuantumResult<Self> {
102        if q >= num_qubits {
103            return Err(QuantumError::QubitIndexOutOfRange {
104                index: q,
105                n_qubits: num_qubits,
106            });
107        }
108        let mut e = Self::identity(num_qubits);
109        e.z[q] = true;
110        Ok(e)
111    }
112
113    /// Number of data qubits carrying a non-identity Pauli.
114    #[must_use]
115    pub fn weight(&self) -> usize {
116        self.x
117            .iter()
118            .zip(self.z.iter())
119            .filter(|(a, b)| **a || **b)
120            .count()
121    }
122
123    /// Compose two errors (qubit-wise XOR of `X` and `Z` parts).
124    ///
125    /// # Errors
126    /// [`QuantumError::DimensionMismatch`] if the operands span different qubit
127    /// counts.
128    pub fn compose(&self, other: &Self) -> QuantumResult<Self> {
129        if self.x.len() != other.x.len() {
130            return Err(QuantumError::DimensionMismatch {
131                expected: self.x.len(),
132                got: other.x.len(),
133            });
134        }
135        let x = self
136            .x
137            .iter()
138            .zip(other.x.iter())
139            .map(|(a, b)| a ^ b)
140            .collect();
141        let z = self
142            .z
143            .iter()
144            .zip(other.z.iter())
145            .map(|(a, b)| a ^ b)
146            .collect();
147        Ok(Self { x, z })
148    }
149}
150
151/// The measured syndrome: one bit per stabilizer (`true` = defect / fired).
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct Syndrome {
154    /// Indexed by stabilizer index (matching [`SurfaceCode::stabilizers`]).
155    pub bits: Vec<bool>,
156}
157
158impl Syndrome {
159    /// `true` when no stabilizer fired.
160    #[must_use]
161    pub fn is_trivial(&self) -> bool {
162        self.bits.iter().all(|b| !b)
163    }
164
165    /// Number of fired stabilizers (defects).
166    #[must_use]
167    pub fn weight(&self) -> usize {
168        self.bits.iter().filter(|b| **b).count()
169    }
170}
171
172/// Configuration for a [`SurfaceCode`].
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub struct SurfaceCodeConfig {
175    /// Code distance `d` (must be a positive odd integer).
176    pub distance: usize,
177}
178
179/// Matching graph for one error species: stabilizer nodes `0..num`, a single
180/// virtual boundary node at index `num`, and qubit-labelled edges.
181#[derive(Debug, Clone)]
182struct MatchingGraph {
183    num_nodes: usize,
184    boundary: usize,
185    /// `adj[node] = [(neighbour, data_qubit_label), …]`.
186    adj: Vec<Vec<(usize, usize)>>,
187}
188
189/// A distance-`d` rotated surface code with an MWPM decoder.
190#[derive(Debug, Clone)]
191pub struct SurfaceCode {
192    distance: usize,
193    num_data: usize,
194    stabilizers: Vec<Stabilizer>,
195    /// Stabilizer indices (into `stabilizers`) of each species, in order.
196    x_stab_indices: Vec<usize>,
197    z_stab_indices: Vec<usize>,
198    /// Support of a logical-`X` operator representative (column 0).
199    logical_x_support: Vec<usize>,
200    /// Support of a logical-`Z` operator representative (row 0).
201    logical_z_support: Vec<usize>,
202    /// Matching graph over `Z`-stabilizers (decodes `X` errors).
203    graph_z: MatchingGraph,
204    /// Matching graph over `X`-stabilizers (decodes `Z` errors).
205    graph_x: MatchingGraph,
206}
207
208impl SurfaceCode {
209    /// Build a distance-`d` rotated surface code.
210    ///
211    /// # Errors
212    /// [`QuantumError::InvalidParameter`] if `distance` is `0` or even.
213    pub fn new(distance: usize) -> QuantumResult<Self> {
214        if distance == 0 || distance.is_multiple_of(2) {
215            return Err(QuantumError::InvalidParameter {
216                name: "distance must be a positive odd integer".into(),
217            });
218        }
219        let d = distance;
220        let num_data = d * d;
221        let stabilizers = build_stabilizers(d);
222
223        let x_stab_indices: Vec<usize> = stabilizers
224            .iter()
225            .enumerate()
226            .filter(|(_, s)| s.kind == StabKind::X)
227            .map(|(i, _)| i)
228            .collect();
229        let z_stab_indices: Vec<usize> = stabilizers
230            .iter()
231            .enumerate()
232            .filter(|(_, s)| s.kind == StabKind::Z)
233            .map(|(i, _)| i)
234            .collect();
235
236        // Logical-Z = Z along the top row (qubits 0..d).
237        let logical_z_support: Vec<usize> = (0..d).collect();
238        // Logical-X = X along the left column (qubits 0, d, 2d, …).
239        let logical_x_support: Vec<usize> = (0..d).map(|r| r * d).collect();
240
241        let graph_z = build_matching_graph(&stabilizers, &z_stab_indices, num_data);
242        let graph_x = build_matching_graph(&stabilizers, &x_stab_indices, num_data);
243
244        Ok(Self {
245            distance: d,
246            num_data,
247            stabilizers,
248            x_stab_indices,
249            z_stab_indices,
250            logical_x_support,
251            logical_z_support,
252            graph_z,
253            graph_x,
254        })
255    }
256
257    /// Build from a [`SurfaceCodeConfig`].
258    ///
259    /// # Errors
260    /// As [`SurfaceCode::new`].
261    pub fn from_config(cfg: SurfaceCodeConfig) -> QuantumResult<Self> {
262        Self::new(cfg.distance)
263    }
264
265    /// Code distance `d`.
266    #[must_use]
267    pub fn distance(&self) -> usize {
268        self.distance
269    }
270
271    /// Number of physical data qubits (`d²`).
272    #[must_use]
273    pub fn num_data_qubits(&self) -> usize {
274        self.num_data
275    }
276
277    /// Total number of stabilizer generators (`d²−1`).
278    #[must_use]
279    pub fn num_stabilizers(&self) -> usize {
280        self.stabilizers.len()
281    }
282
283    /// Number of `X`-type stabilizers.
284    #[must_use]
285    pub fn num_x_stabilizers(&self) -> usize {
286        self.x_stab_indices.len()
287    }
288
289    /// Number of `Z`-type stabilizers.
290    #[must_use]
291    pub fn num_z_stabilizers(&self) -> usize {
292        self.z_stab_indices.len()
293    }
294
295    /// Read-only view of the stabilizer generators.
296    #[must_use]
297    pub fn stabilizers(&self) -> &[Stabilizer] {
298        &self.stabilizers
299    }
300
301    /// Support of the logical-`X` representative used by [`Self::logical_error`].
302    #[must_use]
303    pub fn logical_x_support(&self) -> &[usize] {
304        &self.logical_x_support
305    }
306
307    /// Support of the logical-`Z` representative used by [`Self::logical_error`].
308    #[must_use]
309    pub fn logical_z_support(&self) -> &[usize] {
310        &self.logical_z_support
311    }
312
313    /// Extract the stabilizer syndrome produced by a Pauli error.
314    ///
315    /// A `Z`-type stabilizer fires when its support overlaps the `X` component
316    /// of the error in an odd number of qubits; an `X`-type stabilizer fires on
317    /// odd overlap with the `Z` component.
318    ///
319    /// # Errors
320    /// [`QuantumError::DimensionMismatch`] if `error` spans the wrong number of
321    /// data qubits.
322    pub fn syndrome(&self, error: &PauliError) -> QuantumResult<Syndrome> {
323        if error.x.len() != self.num_data || error.z.len() != self.num_data {
324            return Err(QuantumError::DimensionMismatch {
325                expected: self.num_data,
326                got: error.x.len(),
327            });
328        }
329        let bits = self
330            .stabilizers
331            .iter()
332            .map(|s| {
333                let component = match s.kind {
334                    StabKind::Z => &error.x,
335                    StabKind::X => &error.z,
336                };
337                let overlap = s.support.iter().filter(|&&q| component[q]).count();
338                !overlap.is_multiple_of(2)
339            })
340            .collect();
341        Ok(Syndrome { bits })
342    }
343
344    /// Decode a syndrome into a correction [`PauliError`].
345    ///
346    /// `X` corrections come from matching the `Z`-stabilizer defects; `Z`
347    /// corrections from the `X`-stabilizer defects.
348    ///
349    /// # Errors
350    /// [`QuantumError::DimensionMismatch`] if `syndrome` has the wrong length.
351    pub fn decode(&self, syndrome: &Syndrome) -> QuantumResult<PauliError> {
352        if syndrome.bits.len() != self.stabilizers.len() {
353            return Err(QuantumError::DimensionMismatch {
354                expected: self.stabilizers.len(),
355                got: syndrome.bits.len(),
356            });
357        }
358
359        // Local (graph) indices of the fired stabilizers of each species.
360        let z_defects: Vec<usize> = self
361            .z_stab_indices
362            .iter()
363            .enumerate()
364            .filter(|&(_, &g)| syndrome.bits[g])
365            .map(|(l, _)| l)
366            .collect();
367        let x_defects: Vec<usize> = self
368            .x_stab_indices
369            .iter()
370            .enumerate()
371            .filter(|&(_, &g)| syndrome.bits[g])
372            .map(|(l, _)| l)
373            .collect();
374
375        let x_correction = match_defects(&self.graph_z, &z_defects, self.num_data);
376        let z_correction = match_defects(&self.graph_x, &x_defects, self.num_data);
377
378        Ok(PauliError {
379            x: x_correction,
380            z: z_correction,
381        })
382    }
383
384    /// Decode `error`'s syndrome and return the residual `error ∘ correction`.
385    ///
386    /// In the Pauli frame, the decode succeeded iff the residual has no logical
387    /// component (see [`Self::logical_error`]).
388    ///
389    /// # Errors
390    /// As [`Self::syndrome`].
391    pub fn residual_after_correction(&self, error: &PauliError) -> QuantumResult<PauliError> {
392        let syndrome = self.syndrome(error)?;
393        let correction = self.decode(&syndrome)?;
394        error.compose(&correction)
395    }
396
397    /// Whether a (residual) error implements a non-trivial logical operation,
398    /// returned as `(logical_x_flipped, logical_z_flipped)`.
399    ///
400    /// A residual `X`-string flips the logical state by logical-`X` iff it
401    /// anticommutes with the logical-`Z` operator, i.e. it overlaps the
402    /// logical-`Z` support in an odd number of qubits (and symmetrically for
403    /// the `Z` component against the logical-`X` support).
404    ///
405    /// # Errors
406    /// [`QuantumError::DimensionMismatch`] if `error` spans the wrong number of
407    /// data qubits.
408    pub fn logical_error(&self, error: &PauliError) -> QuantumResult<(bool, bool)> {
409        if error.x.len() != self.num_data || error.z.len() != self.num_data {
410            return Err(QuantumError::DimensionMismatch {
411                expected: self.num_data,
412                got: error.x.len(),
413            });
414        }
415        let x_flip = !self
416            .logical_z_support
417            .iter()
418            .filter(|&&q| error.x[q])
419            .count()
420            .is_multiple_of(2);
421        let z_flip = !self
422            .logical_x_support
423            .iter()
424            .filter(|&&q| error.z[q])
425            .count()
426            .is_multiple_of(2);
427        Ok((x_flip, z_flip))
428    }
429}
430
431/// Construct the rotated-code stabilizer list for distance `d`.
432fn build_stabilizers(d: usize) -> Vec<Stabilizer> {
433    let mut stabs = Vec::new();
434    // Plaquettes are anchored at dual-lattice corners (a, b) ∈ {0..=d}².
435    for a in 0..=d {
436        for b in 0..=d {
437            let mut support = Vec::new();
438            for (dr, dc) in [(-1i64, -1i64), (-1, 0), (0, -1), (0, 0)] {
439                let r = a as i64 + dr;
440                let c = b as i64 + dc;
441                if r >= 0 && r < d as i64 && c >= 0 && c < d as i64 {
442                    support.push((r as usize) * d + (c as usize));
443                }
444            }
445            let kind = if (a + b).is_multiple_of(2) {
446                StabKind::Z
447            } else {
448                StabKind::X
449            };
450            let keep = match support.len() {
451                // Weight-4 bulk plaquette: always kept.
452                4 => true,
453                // Weight-2 boundary plaquette: X on top/bottom, Z on left/right.
454                2 => {
455                    let on_top_or_bottom = a == 0 || a == d;
456                    let on_left_or_right = b == 0 || b == d;
457                    match kind {
458                        StabKind::X => on_top_or_bottom,
459                        StabKind::Z => on_left_or_right,
460                    }
461                }
462                // Weight-1 corners / empty: discarded.
463                _ => false,
464            };
465            if keep {
466                stabs.push(Stabilizer { kind, support });
467            }
468        }
469    }
470    stabs
471}
472
473/// Build the matching graph for one stabilizer species.
474fn build_matching_graph(
475    stabilizers: &[Stabilizer],
476    local_indices: &[usize],
477    num_data: usize,
478) -> MatchingGraph {
479    let num = local_indices.len();
480    let boundary = num;
481    let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); num + 1];
482
483    for q in 0..num_data {
484        // Local indices of same-species stabilizers whose support contains q.
485        let touching: Vec<usize> = local_indices
486            .iter()
487            .enumerate()
488            .filter(|&(_, &g)| stabilizers[g].support.contains(&q))
489            .map(|(l, _)| l)
490            .collect();
491        match touching.as_slice() {
492            [s, t] => {
493                adj[*s].push((*t, q));
494                adj[*t].push((*s, q));
495            }
496            [s] => {
497                adj[*s].push((boundary, q));
498                adj[boundary].push((*s, q));
499            }
500            // A qubit untouched (or, impossibly, touched 3+ times) by this
501            // species contributes no edge to this graph.
502            _ => {}
503        }
504    }
505
506    MatchingGraph {
507        num_nodes: num + 1,
508        boundary,
509        adj,
510    }
511}
512
513/// Result of [`bfs`]: per-node distances and predecessor `(node, qubit_label)`
514/// links used to reconstruct chains.
515type BfsResult = (Vec<usize>, Vec<Option<(usize, usize)>>);
516
517/// Breadth-first search from `source` that never expands *out of* the boundary
518/// node, so boundary-to-boundary chains are not produced. Returns distances and
519/// predecessor `(node, qubit_label)` links for path reconstruction.
520fn bfs(graph: &MatchingGraph, source: usize) -> BfsResult {
521    let n = graph.num_nodes;
522    let mut dist = vec![usize::MAX; n];
523    let mut pred: Vec<Option<(usize, usize)>> = vec![None; n];
524    let mut queue = VecDeque::new();
525    dist[source] = 0;
526    queue.push_back(source);
527    while let Some(u) = queue.pop_front() {
528        if u == graph.boundary {
529            continue; // absorbing: never route a chain *through* the boundary
530        }
531        for &(v, q) in &graph.adj[u] {
532            if dist[v] == usize::MAX {
533                dist[v] = dist[u] + 1;
534                pred[v] = Some((u, q));
535                queue.push_back(v);
536            }
537        }
538    }
539    (dist, pred)
540}
541
542/// Reconstruct the data-qubit labels along the BFS path `source → target`.
543fn path_qubits(pred: &[Option<(usize, usize)>], source: usize, target: usize) -> Vec<usize> {
544    let mut qubits = Vec::new();
545    let mut cur = target;
546    while cur != source {
547        match pred[cur] {
548            Some((prev, q)) => {
549                qubits.push(q);
550                cur = prev;
551            }
552            None => break, // unreachable; leave partial (won't occur for valid codes)
553        }
554    }
555    qubits
556}
557
558/// Exact minimum-weight perfect matching of `defects` (local node indices) to
559/// each other or to the boundary, returning the `num_data`-length correction
560/// mask (qubits to flip).
561fn match_defects(graph: &MatchingGraph, defects: &[usize], num_data: usize) -> Vec<bool> {
562    let mut correction = vec![false; num_data];
563    if defects.is_empty() {
564        return correction;
565    }
566    let k = defects.len();
567
568    // One BFS per defect gives all pairwise distances + boundary distance.
569    let searches: Vec<BfsResult> = defects.iter().map(|&d| bfs(graph, d)).collect();
570
571    // Safety valve: an enormous syndrome would make the 2^k DP explode. Such
572    // syndromes never arise from the low-weight errors this code corrects, but
573    // guard anyway by greedily matching every defect to the boundary (always a
574    // valid, if non-minimal, correction) — this cannot loop or hang.
575    if k > 20 {
576        for (i, &_d) in defects.iter().enumerate() {
577            let path = path_qubits(&searches[i].1, defects[i], graph.boundary);
578            for q in path {
579                correction[q] ^= true;
580            }
581        }
582        return correction;
583    }
584
585    let pair_dist = |i: usize, j: usize| searches[i].0[defects[j]];
586    let bnd_dist = |i: usize| searches[i].0[graph.boundary];
587
588    let size = 1usize << k;
589    let mut dp = vec![usize::MAX; size];
590    // partner[mask]: what the lowest set bit of `mask` matched to — usize::MAX
591    // for the boundary, else the defect index it paired with.
592    let mut partner = vec![usize::MAX; size];
593    dp[0] = 0;
594
595    for mask in 1..size {
596        let i = mask.trailing_zeros() as usize;
597        let rest = mask & !(1 << i);
598
599        // Option 1: send defect i to the boundary.
600        let bd = bnd_dist(i);
601        if bd != usize::MAX && dp[rest] != usize::MAX {
602            let cost = dp[rest] + bd;
603            if cost < dp[mask] {
604                dp[mask] = cost;
605                partner[mask] = usize::MAX;
606            }
607        }
608
609        // Option 2: pair defect i with another defect j still in the mask.
610        let mut jbits = rest;
611        while jbits != 0 {
612            let j = jbits.trailing_zeros() as usize;
613            jbits &= jbits - 1;
614            let pd = pair_dist(i, j);
615            let sub = rest & !(1 << j);
616            if pd != usize::MAX && dp[sub] != usize::MAX {
617                let cost = dp[sub] + pd;
618                if cost < dp[mask] {
619                    dp[mask] = cost;
620                    partner[mask] = j;
621                }
622            }
623        }
624    }
625
626    // Reconstruct the matching and accumulate the (XOR of) chains.
627    let mut mask = size - 1;
628    while mask != 0 {
629        let i = mask.trailing_zeros() as usize;
630        let p = partner[mask];
631        if p == usize::MAX {
632            let path = path_qubits(&searches[i].1, defects[i], graph.boundary);
633            for q in path {
634                correction[q] ^= true;
635            }
636            mask &= !(1 << i);
637        } else {
638            let path = path_qubits(&searches[i].1, defects[i], defects[p]);
639            for q in path {
640                correction[q] ^= true;
641            }
642            mask &= !((1 << i) | (1 << p));
643        }
644    }
645
646    correction
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652
653    #[test]
654    fn distance_three_layout_counts() {
655        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
656        assert_eq!(code.distance(), 3);
657        assert_eq!(code.num_data_qubits(), 9); // d²
658        assert_eq!(code.num_stabilizers(), 8); // d² − 1
659        assert_eq!(code.num_x_stabilizers(), 4); // (d²−1)/2
660        assert_eq!(code.num_z_stabilizers(), 4);
661        // Bulk plaquettes have weight 4, boundary plaquettes weight 2.
662        let weight4 = code
663            .stabilizers()
664            .iter()
665            .filter(|s| s.support.len() == 4)
666            .count();
667        let weight2 = code
668            .stabilizers()
669            .iter()
670            .filter(|s| s.support.len() == 2)
671            .count();
672        assert_eq!(weight4, 4); // (d−1)²
673        assert_eq!(weight2, 4); // 2(d−1)
674    }
675
676    #[test]
677    fn css_stabilizers_commute() {
678        // Every X-type and Z-type stabilizer must share an even number of qubits.
679        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
680        for sx in code.stabilizers().iter().filter(|s| s.kind == StabKind::X) {
681            for sz in code.stabilizers().iter().filter(|s| s.kind == StabKind::Z) {
682                let overlap = sx.support.iter().filter(|q| sz.support.contains(q)).count();
683                assert!(overlap.is_multiple_of(2), "overlap={overlap}");
684            }
685        }
686    }
687
688    #[test]
689    fn logical_operators_anticommute() {
690        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
691        // Logical X (col 0) and logical Z (row 0) share exactly one qubit.
692        let overlap = code
693            .logical_x_support()
694            .iter()
695            .filter(|q| code.logical_z_support().contains(q))
696            .count();
697        assert_eq!(overlap, 1);
698        // Both have weight equal to the distance.
699        assert_eq!(code.logical_x_support().len(), 3);
700        assert_eq!(code.logical_z_support().len(), 3);
701    }
702
703    #[test]
704    fn invalid_distance_rejected() {
705        assert!(SurfaceCode::new(0).is_err()); // zero
706        assert!(SurfaceCode::new(2).is_err()); // even
707        assert!(SurfaceCode::new(4).is_err()); // even
708        assert!(SurfaceCode::new(3).is_ok()); // odd
709    }
710
711    #[test]
712    fn zero_error_gives_empty_syndrome_and_no_correction() {
713        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
714        let err = PauliError::identity(code.num_data_qubits());
715        let syn = code.syndrome(&err).expect("syndrome for identity error");
716        assert!(syn.is_trivial());
717        let corr = code.decode(&syn).expect("decode trivial syndrome");
718        assert_eq!(corr.weight(), 0);
719    }
720
721    #[test]
722    fn single_x_on_bulk_qubit_fires_two_z_stabilizers() {
723        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
724        // Qubit 4 is the central qubit, in two Z-type plaquettes.
725        let err = PauliError::single_x(code.num_data_qubits(), 4)
726            .expect("valid single-X error on qubit 4");
727        let syn = code
728            .syndrome(&err)
729            .expect("syndrome for single-X error on qubit 4");
730        assert_eq!(syn.weight(), 2, "central X error should fire two defects");
731        // The two defects must both be Z-type stabilizers (they detect X).
732        for (i, &fired) in syn.bits.iter().enumerate() {
733            if fired {
734                assert_eq!(code.stabilizers()[i].kind, StabKind::Z);
735            }
736        }
737        // …and the decoder corrects it with no logical error.
738        let residual = code
739            .residual_after_correction(&err)
740            .expect("residual after correcting X error on qubit 4");
741        assert_eq!(
742            code.logical_error(&residual)
743                .expect("logical error check on residual after X correction"),
744            (false, false)
745        );
746    }
747
748    #[test]
749    fn single_z_on_bulk_qubit_fires_two_x_stabilizers() {
750        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
751        let err = PauliError::single_z(code.num_data_qubits(), 4)
752            .expect("valid single-Z error on qubit 4");
753        let syn = code
754            .syndrome(&err)
755            .expect("syndrome for single-Z error on qubit 4");
756        assert_eq!(syn.weight(), 2);
757        for (i, &fired) in syn.bits.iter().enumerate() {
758            if fired {
759                assert_eq!(code.stabilizers()[i].kind, StabKind::X);
760            }
761        }
762        let residual = code
763            .residual_after_correction(&err)
764            .expect("residual after correcting Z error on qubit 4");
765        assert_eq!(
766            code.logical_error(&residual)
767                .expect("logical error check on residual after Z correction"),
768            (false, false)
769        );
770    }
771
772    #[test]
773    fn every_weight_one_error_is_corrected() {
774        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
775        let n = code.num_data_qubits();
776        for q in 0..n {
777            // Single X.
778            let ex = PauliError::single_x(n, q).expect("valid single-X error on qubit q");
779            let rx = code
780                .residual_after_correction(&ex)
781                .expect("residual after correcting single-X error");
782            assert_eq!(
783                code.logical_error(&rx)
784                    .expect("logical error check on single-X residual"),
785                (false, false),
786                "X error on qubit {q} not corrected"
787            );
788            // Single Z.
789            let ez = PauliError::single_z(n, q).expect("valid single-Z error on qubit q");
790            let rz = code
791                .residual_after_correction(&ez)
792                .expect("residual after correcting single-Z error");
793            assert_eq!(
794                code.logical_error(&rz)
795                    .expect("logical error check on single-Z residual"),
796                (false, false),
797                "Z error on qubit {q} not corrected"
798            );
799            // Single Y (= X·Z) is also weight one and must be corrected.
800            let mut ey =
801                PauliError::single_x(n, q).expect("valid single-X component of Y error on qubit q");
802            ey.z[q] = true;
803            let ry = code
804                .residual_after_correction(&ey)
805                .expect("residual after correcting single-Y error");
806            assert_eq!(
807                code.logical_error(&ry)
808                    .expect("logical error check on single-Y residual"),
809                (false, false),
810                "Y error on qubit {q} not corrected"
811            );
812        }
813    }
814
815    #[test]
816    fn corrected_residual_has_trivial_syndrome() {
817        // After correction the residual must commute with every stabilizer.
818        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
819        let n = code.num_data_qubits();
820        for q in 0..n {
821            let err = PauliError::single_x(n, q).expect("valid single-X error on qubit q");
822            let residual = code
823                .residual_after_correction(&err)
824                .expect("residual after correcting error on qubit q");
825            let syn = code
826                .syndrome(&residual)
827                .expect("syndrome for residual error");
828            assert!(syn.is_trivial(), "residual syndrome non-trivial for q={q}");
829        }
830    }
831
832    #[test]
833    fn boundary_x_error_fires_single_defect() {
834        // Corner qubit 0 lies in a single Z-type (weight-2) boundary plaquette,
835        // so a single X there produces exactly one defect, matched to the
836        // boundary by the decoder.
837        let code = SurfaceCode::new(3).expect("valid distance-3 surface code");
838        let err = PauliError::single_x(code.num_data_qubits(), 0)
839            .expect("valid single-X error on corner qubit 0");
840        let syn = code
841            .syndrome(&err)
842            .expect("syndrome for boundary X error on qubit 0");
843        assert_eq!(syn.weight(), 1);
844        let residual = code
845            .residual_after_correction(&err)
846            .expect("residual after correcting boundary X error");
847        assert_eq!(
848            code.logical_error(&residual)
849                .expect("logical error check on boundary residual"),
850            (false, false)
851        );
852    }
853
854    #[test]
855    fn distance_five_layout_counts() {
856        // The construction generalises: d=5 ⇒ 25 data, 24 stabilizers (12/12).
857        let code = SurfaceCode::new(5).expect("valid distance-5 surface code");
858        assert_eq!(code.num_data_qubits(), 25);
859        assert_eq!(code.num_stabilizers(), 24);
860        assert_eq!(code.num_x_stabilizers(), 12);
861        assert_eq!(code.num_z_stabilizers(), 12);
862    }
863}