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}