Skip to main content

ruqu_algorithms/
surface_code.rs

1//! Surface Code Error Correction Simulation
2//!
3//! Simulates a **distance-3 rotated surface code** with:
4//!
5//! - 9 data qubits (3 x 3 grid)
6//! - 4 X-type stabilizers (plaquettes, detect Z errors)
7//! - 4 Z-type stabilizers (vertices, detect X errors)
8//! - 8 ancilla qubits (one per stabilizer)
9//!
10//! Each QEC cycle performs:
11//! 1. **Noise injection** -- random Pauli errors on data qubits.
12//! 2. **Stabilizer measurement** -- entangle ancillas with data qubits and
13//!    measure the ancillas to extract the error syndrome.
14//! 3. **Decoding** -- a simple lookup decoder maps the syndrome to a
15//!    correction (placeholder; production systems would use MWPM).
16//! 4. **Correction** -- apply compensating Pauli gates.
17//!
18//! # Qubit layout (distance 3)
19//!
20//! ```text
21//! Data qubits:        Ancilla assignment:
22//!   d0  d1  d2          X-anc: 9, 10, 11, 12
23//!   d3  d4  d5          Z-anc: 13, 14, 15, 16
24//!   d6  d7  d8
25//! ```
26//!
27//! X stabilizers (plaquettes):
28//! - X0 (anc 9):  {d0, d1, d3, d4}
29//! - X1 (anc 10): {d1, d2, d4, d5}
30//! - X2 (anc 11): {d3, d4, d6, d7}
31//! - X3 (anc 12): {d4, d5, d7, d8}
32//!
33//! Z stabilizers (boundary vertices):
34//! - Z0 (anc 13): {d0, d1}
35//! - Z1 (anc 14): {d2, d5}
36//! - Z2 (anc 15): {d3, d6}
37//! - Z3 (anc 16): {d7, d8}
38
39use rand::rngs::StdRng;
40use rand::{Rng, SeedableRng};
41use ruqu_core::gate::Gate;
42use ruqu_core::state::QuantumState;
43use ruqu_core::types::QubitIndex;
44
45// ---------------------------------------------------------------------------
46// Configuration and result types
47// ---------------------------------------------------------------------------
48
49/// Configuration for a surface code error correction simulation.
50pub struct SurfaceCodeConfig {
51    /// Code distance (currently only 3 is supported).
52    pub distance: u32,
53    /// Number of QEC syndrome-extraction cycles to run.
54    pub num_cycles: u32,
55    /// Physical error rate per data qubit per cycle. Each data qubit
56    /// independently suffers a Pauli-X with probability `noise_rate` and a
57    /// Pauli-Z with probability `noise_rate` (simplified depolarizing model).
58    pub noise_rate: f64,
59    /// Optional RNG seed for reproducibility.
60    pub seed: Option<u64>,
61}
62
63/// Result of a surface code simulation.
64pub struct SurfaceCodeResult {
65    /// Number of detected logical errors (simplified check).
66    pub logical_errors: u32,
67    /// Total QEC cycles executed.
68    pub total_cycles: u32,
69    /// Logical error rate = `logical_errors / total_cycles`.
70    pub logical_error_rate: f64,
71    /// Syndrome bit-vector for each cycle. Each inner `Vec<bool>` has
72    /// `num_x_stabilizers + num_z_stabilizers` entries.
73    pub syndrome_history: Vec<Vec<bool>>,
74}
75
76// ---------------------------------------------------------------------------
77// Surface code layout
78// ---------------------------------------------------------------------------
79
80/// Physical layout of a surface code: which data qubits participate in each
81/// stabilizer, and the ancilla qubit assigned to each stabilizer.
82pub struct SurfaceCodeLayout {
83    /// Indices of data qubits.
84    pub data_qubits: Vec<QubitIndex>,
85    /// Indices of X-type (plaquette) ancilla qubits.
86    pub x_ancillas: Vec<QubitIndex>,
87    /// Indices of Z-type (vertex) ancilla qubits.
88    pub z_ancillas: Vec<QubitIndex>,
89    /// For each X stabilizer, the data qubits it acts on.
90    pub x_stabilizers: Vec<Vec<QubitIndex>>,
91    /// For each Z stabilizer, the data qubits it acts on.
92    pub z_stabilizers: Vec<Vec<QubitIndex>>,
93}
94
95impl SurfaceCodeLayout {
96    /// Create the layout for a distance-3 rotated surface code.
97    ///
98    /// Total qubits: 9 data (indices 0..8) + 4 X-ancillas (9..12) +
99    /// 4 Z-ancillas (13..16) = 17.
100    pub fn distance_3() -> Self {
101        Self {
102            data_qubits: (0..9).collect(),
103            x_ancillas: vec![9, 10, 11, 12],
104            z_ancillas: vec![13, 14, 15, 16],
105            x_stabilizers: vec![
106                vec![0, 1, 3, 4], // X0: top-left plaquette
107                vec![1, 2, 4, 5], // X1: top-right plaquette
108                vec![3, 4, 6, 7], // X2: bottom-left plaquette
109                vec![4, 5, 7, 8], // X3: bottom-right plaquette
110            ],
111            z_stabilizers: vec![
112                vec![0, 1], // Z0: top boundary
113                vec![2, 5], // Z1: right boundary
114                vec![3, 6], // Z2: left boundary
115                vec![7, 8], // Z3: bottom boundary
116            ],
117        }
118    }
119
120    /// Total number of physical qubits (data + ancilla).
121    pub fn total_qubits(&self) -> u32 {
122        (self.data_qubits.len() + self.x_ancillas.len() + self.z_ancillas.len()) as u32
123    }
124
125    /// Total number of stabilizers (X + Z).
126    pub fn num_stabilizers(&self) -> usize {
127        self.x_stabilizers.len() + self.z_stabilizers.len()
128    }
129}
130
131// ---------------------------------------------------------------------------
132// Noise injection
133// ---------------------------------------------------------------------------
134
135/// Inject simplified depolarizing noise on each data qubit.
136///
137/// For each data qubit, independently:
138/// - With probability `noise_rate`: apply X (bit flip)
139/// - With probability `noise_rate`: apply Z (phase flip)
140/// - Otherwise: no error
141///
142/// The two error channels are independent (a qubit can get both X and Z = Y).
143fn inject_noise(
144    state: &mut QuantumState,
145    data_qubits: &[QubitIndex],
146    noise_rate: f64,
147    rng: &mut StdRng,
148) -> ruqu_core::error::Result<()> {
149    for &q in data_qubits {
150        let r: f64 = rng.gen();
151        if r < noise_rate {
152            state.apply_gate(&Gate::X(q))?;
153        } else if r < 2.0 * noise_rate {
154            state.apply_gate(&Gate::Z(q))?;
155        }
156        // else: no error on this qubit in this channel
157    }
158    Ok(())
159}
160
161// ---------------------------------------------------------------------------
162// Stabilizer measurement (one QEC cycle)
163// ---------------------------------------------------------------------------
164
165/// Execute one QEC cycle: reset ancillas, entangle with data qubits via
166/// stabilizer circuits, and measure ancillas.
167///
168/// Returns the syndrome vector (one bool per stabilizer, X-stabilizers first,
169/// then Z-stabilizers). A `true` entry means the stabilizer measured -1
170/// (error detected).
171fn run_cycle(
172    state: &mut QuantumState,
173    layout: &SurfaceCodeLayout,
174) -> ruqu_core::error::Result<Vec<bool>> {
175    // Reset all ancilla qubits to |0>.
176    for &a in layout.x_ancillas.iter().chain(layout.z_ancillas.iter()) {
177        state.reset_qubit(a)?;
178    }
179
180    // ---- X-stabilizer measurement circuits ----
181    // To measure the product X_a X_b X_c X_d:
182    //   1. H(ancilla)
183    //   2. CNOT(ancilla, data_a), ..., CNOT(ancilla, data_d)
184    //   3. H(ancilla)
185    //   4. Measure ancilla
186    for (i, stabilizer) in layout.x_stabilizers.iter().enumerate() {
187        let ancilla = layout.x_ancillas[i];
188        state.apply_gate(&Gate::H(ancilla))?;
189        for &data in stabilizer {
190            state.apply_gate(&Gate::CNOT(ancilla, data))?;
191        }
192        state.apply_gate(&Gate::H(ancilla))?;
193    }
194
195    // ---- Z-stabilizer measurement circuits ----
196    // To measure the product Z_a Z_b Z_c Z_d:
197    //   1. CNOT(data_a, ancilla), ..., CNOT(data_d, ancilla)
198    //   2. Measure ancilla
199    for (i, stabilizer) in layout.z_stabilizers.iter().enumerate() {
200        let ancilla = layout.z_ancillas[i];
201        for &data in stabilizer {
202            state.apply_gate(&Gate::CNOT(data, ancilla))?;
203        }
204    }
205
206    // Measure all ancillas and collect syndrome bits.
207    let mut syndrome = Vec::with_capacity(layout.num_stabilizers());
208    for &a in layout.x_ancillas.iter().chain(layout.z_ancillas.iter()) {
209        let outcome = state.measure(a)?;
210        syndrome.push(outcome.result);
211    }
212
213    Ok(syndrome)
214}
215
216// ---------------------------------------------------------------------------
217// Syndrome decoder
218// ---------------------------------------------------------------------------
219
220/// Simple lookup decoder for the distance-3 surface code.
221///
222/// This is a **placeholder** decoder that applies a single-qubit X correction
223/// on the data qubit most likely responsible for the detected syndrome
224/// pattern. A production implementation would use Minimum Weight Perfect
225/// Matching (MWPM) via e.g. `fusion-blossom`.
226///
227/// # Decoding strategy
228///
229/// The syndrome has 8 bits (4 X-stabilizer + 4 Z-stabilizer). The decoder
230/// only looks at the X-stabilizer syndrome (bits 0..3) to correct Z errors
231/// and the Z-stabilizer syndrome (bits 4..7) to correct X errors.
232///
233/// For each stabilizer group, if exactly one stabilizer fires, apply a
234/// correction on the first data qubit of that stabilizer. If multiple fire,
235/// correct the data qubit shared by the most triggered stabilizers (heuristic).
236fn decode_syndrome(syndrome: &[bool], layout: &SurfaceCodeLayout) -> Vec<Gate> {
237    let mut corrections = Vec::new();
238    let n_x = layout.x_stabilizers.len();
239
240    // ---- Correct Z errors using X-stabilizer syndrome (bits 0..n_x) ----
241    let x_syndrome = &syndrome[..n_x];
242    let x_triggered: Vec<usize> = x_syndrome
243        .iter()
244        .enumerate()
245        .filter(|(_, &s)| s)
246        .map(|(i, _)| i)
247        .collect();
248
249    if x_triggered.len() == 1 {
250        // Single stabilizer fired: correct its first data qubit with Z.
251        let data_q = layout.x_stabilizers[x_triggered[0]][0];
252        corrections.push(Gate::Z(data_q));
253    } else if x_triggered.len() >= 2 {
254        // Multiple stabilizers fired: find the data qubit that appears in
255        // the most triggered stabilizers and correct it.
256        if let Some(q) = most_common_data_qubit(&layout.x_stabilizers, &x_triggered) {
257            corrections.push(Gate::Z(q));
258        }
259    }
260
261    // ---- Correct X errors using Z-stabilizer syndrome (bits n_x..) ----
262    let z_syndrome = &syndrome[n_x..];
263    let z_triggered: Vec<usize> = z_syndrome
264        .iter()
265        .enumerate()
266        .filter(|(_, &s)| s)
267        .map(|(i, _)| i)
268        .collect();
269
270    if z_triggered.len() == 1 {
271        let data_q = layout.z_stabilizers[z_triggered[0]][0];
272        corrections.push(Gate::X(data_q));
273    } else if z_triggered.len() >= 2 {
274        if let Some(q) = most_common_data_qubit(&layout.z_stabilizers, &z_triggered) {
275            corrections.push(Gate::X(q));
276        }
277    }
278
279    corrections
280}
281
282/// Find the data qubit that appears in the most stabilizers among the
283/// triggered set. Returns `None` if the triggered list is empty.
284fn most_common_data_qubit(
285    stabilizers: &[Vec<QubitIndex>],
286    triggered_indices: &[usize],
287) -> Option<QubitIndex> {
288    // Count how many triggered stabilizers each data qubit participates in.
289    let mut counts: std::collections::HashMap<QubitIndex, usize> = std::collections::HashMap::new();
290    for &idx in triggered_indices {
291        for &dq in &stabilizers[idx] {
292            *counts.entry(dq).or_insert(0) += 1;
293        }
294    }
295    counts
296        .into_iter()
297        .max_by_key(|&(_, count)| count)
298        .map(|(qubit, _)| qubit)
299}
300
301// ---------------------------------------------------------------------------
302// Main entry point
303// ---------------------------------------------------------------------------
304
305/// Run a surface code error correction simulation.
306///
307/// Currently only **distance 3** is supported. The simulation:
308/// 1. Initializes all qubits in |0> (the logical |0_L> state).
309/// 2. For each cycle: injects noise, extracts the syndrome, decodes, and
310///    applies corrections.
311/// 3. After all cycles, returns the syndrome history and error statistics.
312///
313/// # Logical error detection (simplified)
314///
315/// A logical Z error is detected by checking the parity of a representative
316/// row of data qubits. If the initial logical state was |0_L>, a flipped
317/// parity indicates a logical error. This is a coarse approximation; a full
318/// implementation would track the Pauli frame.
319///
320/// # Errors
321///
322/// Returns a [`ruqu_core::error::QuantumError`] on simulator failures.
323pub fn run_surface_code(
324    config: &SurfaceCodeConfig,
325) -> ruqu_core::error::Result<SurfaceCodeResult> {
326    assert_eq!(
327        config.distance, 3,
328        "Only distance-3 surface codes are currently supported"
329    );
330
331    let layout = SurfaceCodeLayout::distance_3();
332    let total_qubits = layout.total_qubits();
333
334    let mut state = match config.seed {
335        Some(s) => QuantumState::new_with_seed(total_qubits, s)?,
336        None => QuantumState::new(total_qubits)?,
337    };
338
339    // Seeded RNG for noise injection.
340    let mut rng = match config.seed {
341        Some(s) => StdRng::seed_from_u64(s),
342        None => StdRng::from_entropy(),
343    };
344
345    let mut logical_errors = 0u32;
346    let mut syndrome_history = Vec::with_capacity(config.num_cycles as usize);
347
348    // Record the initial parity of the top row (d0, d1, d2) for logical
349    // error detection. For |0_L>, this parity should be even (all |0>).
350    // After each cycle we compare against this baseline.
351    let logical_row: [QubitIndex; 3] = [0, 1, 2];
352
353    for _cycle in 0..config.num_cycles {
354        // 1. Inject noise on data qubits.
355        inject_noise(&mut state, &layout.data_qubits, config.noise_rate, &mut rng)?;
356
357        // 2. Syndrome extraction.
358        let syndrome = run_cycle(&mut state, &layout)?;
359        syndrome_history.push(syndrome.clone());
360
361        // 3. Decode and apply corrections.
362        let corrections = decode_syndrome(&syndrome, &layout);
363        for gate in &corrections {
364            state.apply_gate(gate)?;
365        }
366
367        // 4. Simplified logical error check.
368        //    Measure Z-parity of the top-row data qubits non-destructively
369        //    by reading expectation values. If <Z_0 Z_1 Z_2> < 0, the
370        //    row has odd parity -> logical error.
371        let mut row_parity = 1.0_f64;
372        for &q in &logical_row {
373            let z_exp = state.expectation_value(
374                &ruqu_core::types::PauliString {
375                    ops: vec![(q, ruqu_core::types::PauliOp::Z)],
376                },
377            );
378            // Each Z expectation is in [-1, 1]. For a computational basis
379            // state, it is exactly +1 (|0>) or -1 (|1>). For superpositions
380            // we approximate: sign of the product captures parity.
381            row_parity *= z_exp;
382        }
383        if row_parity < 0.0 {
384            logical_errors += 1;
385        }
386    }
387
388    let logical_error_rate = if config.num_cycles > 0 {
389        logical_errors as f64 / config.num_cycles as f64
390    } else {
391        0.0
392    };
393
394    Ok(SurfaceCodeResult {
395        logical_errors,
396        total_cycles: config.num_cycles,
397        logical_error_rate,
398        syndrome_history,
399    })
400}
401
402// ---------------------------------------------------------------------------
403// Tests
404// ---------------------------------------------------------------------------
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_layout_distance_3() {
412        let layout = SurfaceCodeLayout::distance_3();
413        assert_eq!(layout.data_qubits.len(), 9);
414        assert_eq!(layout.x_ancillas.len(), 4);
415        assert_eq!(layout.z_ancillas.len(), 4);
416        assert_eq!(layout.total_qubits(), 17);
417        assert_eq!(layout.num_stabilizers(), 8);
418    }
419
420    #[test]
421    fn test_x_stabilizers_cover_all_data() {
422        let layout = SurfaceCodeLayout::distance_3();
423        let mut covered: std::collections::HashSet<QubitIndex> = std::collections::HashSet::new();
424        for stab in &layout.x_stabilizers {
425            for &q in stab {
426                covered.insert(q);
427            }
428        }
429        // All 9 data qubits should be covered by X stabilizers.
430        for q in 0..9u32 {
431            assert!(covered.contains(&q), "data qubit {} not covered by X stabilizers", q);
432        }
433    }
434
435    #[test]
436    fn test_z_stabilizers_boundary() {
437        let layout = SurfaceCodeLayout::distance_3();
438        // Z stabilizers are weight-2 boundary stabilizers for d=3.
439        for stab in &layout.z_stabilizers {
440            assert_eq!(stab.len(), 2, "Z stabilizer should have weight 2");
441        }
442    }
443
444    #[test]
445    fn test_decode_syndrome_no_error() {
446        let layout = SurfaceCodeLayout::distance_3();
447        let syndrome = vec![false; 8];
448        let corrections = decode_syndrome(&syndrome, &layout);
449        assert!(corrections.is_empty(), "no corrections when syndrome is trivial");
450    }
451
452    #[test]
453    fn test_decode_syndrome_single_x_stabilizer() {
454        let layout = SurfaceCodeLayout::distance_3();
455        // Only X0 fires -> correct data qubit 0 with Z.
456        let mut syndrome = vec![false; 8];
457        syndrome[0] = true;
458        let corrections = decode_syndrome(&syndrome, &layout);
459        assert_eq!(corrections.len(), 1);
460    }
461
462    #[test]
463    fn test_decode_syndrome_single_z_stabilizer() {
464        let layout = SurfaceCodeLayout::distance_3();
465        // Only Z0 fires (index 4 in syndrome vector).
466        let mut syndrome = vec![false; 8];
467        syndrome[4] = true;
468        let corrections = decode_syndrome(&syndrome, &layout);
469        assert_eq!(corrections.len(), 1);
470    }
471
472    #[test]
473    fn test_most_common_data_qubit() {
474        let stabilizers = vec![
475            vec![0, 1, 3, 4],
476            vec![1, 2, 4, 5],
477        ];
478        // Both stabilizers 0 and 1 triggered: qubit 1 and 4 appear in both.
479        let result = most_common_data_qubit(&stabilizers, &[0, 1]);
480        assert!(result == Some(1) || result == Some(4));
481    }
482
483    #[test]
484    #[should_panic(expected = "Only distance-3")]
485    fn test_unsupported_distance() {
486        let config = SurfaceCodeConfig {
487            distance: 5,
488            num_cycles: 1,
489            noise_rate: 0.01,
490            seed: Some(42),
491        };
492        let _ = run_surface_code(&config);
493    }
494}