Q# Intermediate Free 6/7 in series 25 min read

Quantum Error Correction in Q#

Implement the 3-qubit bit flip code and phase flip code in Q#, covering encoding, error injection, syndrome measurement, and correction.

What you'll learn

  • error correction
  • Q#
  • stabilizer codes
  • bit flip code
  • phase flip code
  • syndrome measurement

Prerequisites

  • Python proficiency
  • Beginner quantum computing concepts (superposition, entanglement)
  • Linear algebra basics

Quantum error correction is essential for building reliable quantum computers. Physical qubits are fragile; interactions with the environment cause decoherence, and gate operations introduce small errors that accumulate over a computation. Without error correction, large-scale quantum algorithms like Shor’s factoring or fault-tolerant chemistry simulations would be impossible.

Why Classical Repetition Fails for Qubits

In classical computing, the simplest error correction strategy is repetition: encode a bit 0 as 000 and a bit 1 as 111, then take a majority vote. This works because you can freely copy bits and inspect them.

Quantum mechanics forbids both of these strategies. The no-cloning theorem states that an arbitrary unknown quantum state cannot be copied. Furthermore, measuring a qubit collapses its superposition, destroying the information you are trying to protect. Quantum error correction must detect errors without learning anything about the encoded state itself. This is done through syndrome measurements on ancilla qubits that reveal which error occurred (if any) without revealing the logical state.

The 3-Qubit Bit Flip Code

The simplest quantum error correcting code protects against single bit flip (X) errors. It encodes one logical qubit into three physical qubits:

  • |0⟩_L = |000⟩
  • |1⟩_L = |111⟩

A general state α|0⟩ + β|1⟩ becomes α|000⟩ + β|111⟩. If a single X error hits any one of the three qubits, syndrome measurements on pairs of qubits can identify which qubit was flipped without revealing α or β.

BitFlipCode.qs

namespace ErrorCorrection {
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Measurement;

    /// Encodes a single logical qubit into 3 physical qubits
    /// using the bit flip repetition code.
    /// The source qubit state is spread across all three qubits.
    operation Encode(source : Qubit, ancilla1 : Qubit, ancilla2 : Qubit) : Unit {
        CNOT(source, ancilla1);   // Copy |1⟩ component to ancilla1
        CNOT(source, ancilla2);   // Copy |1⟩ component to ancilla2
        // |ψ⟩ = α|0⟩ + β|1⟩  becomes  α|000⟩ + β|111⟩
    }

    /// Measures the syndrome to detect which qubit (if any) has flipped.
    /// Returns a pair of Results indicating parity checks:
    ///   (q0 XOR q1, q1 XOR q2)
    /// Syndrome (Zero,Zero) = no error
    /// Syndrome (One, Zero) = error on q0
    /// Syndrome (One, One)  = error on q1
    /// Syndrome (Zero, One) = error on q2
    operation MeasureSyndrome(
        q0 : Qubit, q1 : Qubit, q2 : Qubit
    ) : (Result, Result) {
        use s0 = Qubit();
        use s1 = Qubit();

        // Parity of q0 and q1
        CNOT(q0, s0);
        CNOT(q1, s0);
        let syndrome0 = M(s0);
        Reset(s0);

        // Parity of q1 and q2
        CNOT(q1, s1);
        CNOT(q2, s1);
        let syndrome1 = M(s1);
        Reset(s1);

        return (syndrome0, syndrome1);
    }

    /// Corrects a single bit flip error based on the syndrome.
    operation CorrectBitFlip(
        q0 : Qubit, q1 : Qubit, q2 : Qubit,
        syndrome : (Result, Result)
    ) : Unit {
        let (s0, s1) = syndrome;

        if s0 == One and s1 == Zero {
            X(q0);   // Error was on qubit 0
        } elif s0 == One and s1 == One {
            X(q1);   // Error was on qubit 1
        } elif s0 == Zero and s1 == One {
            X(q2);   // Error was on qubit 2
        }
        // (Zero, Zero) means no error; do nothing
    }

    /// Decodes the 3-qubit code back to a single qubit.
    operation Decode(source : Qubit, ancilla1 : Qubit, ancilla2 : Qubit) : Unit {
        CNOT(source, ancilla2);
        CNOT(source, ancilla1);
    }

    /// Full demonstration: encode, inject error, measure syndrome,
    /// correct, decode, and verify.
    operation DemoBitFlipCode(errorQubit : Int) : Result {
        use q = Qubit[3];

        // Prepare a test state: |+⟩ on the logical qubit
        H(q[0]);

        // Encode
        Encode(q[0], q[1], q[2]);

        // Inject a single bit flip error on the specified qubit
        if errorQubit >= 0 and errorQubit <= 2 {
            X(q[errorQubit]);
        }

        // Detect error via syndrome measurement
        let syndrome = MeasureSyndrome(q[0], q[1], q[2]);

        // Correct the error
        CorrectBitFlip(q[0], q[1], q[2], syndrome);

        // Decode back to single qubit
        Decode(q[0], q[1], q[2]);

        // Measure in the X basis to verify we recovered |+⟩
        H(q[0]);
        let result = M(q[0]);
        ResetAll(q);

        return result;
    }
}

Python host (run_bit_flip.py)

import qsharp
from ErrorCorrection import DemoBitFlipCode

# Test with error on each qubit position
for error_pos in [0, 1, 2]:
    results = [DemoBitFlipCode.simulate(errorQubit=error_pos) for _ in range(20)]
    zeros = results.count(0)
    print(f"Error on qubit {error_pos}: measured Zero {zeros}/20 times")

# Test with no error (errorQubit = -1)
results = [DemoBitFlipCode.simulate(errorQubit=-1) for _ in range(20)]
zeros = results.count(0)
print(f"No error: measured Zero {zeros}/20 times")

Expected Output

Error on qubit 0: measured Zero 20/20 times
Error on qubit 1: measured Zero 20/20 times
Error on qubit 2: measured Zero 20/20 times
No error: measured Zero 20/20 times

We always measure Zero because the original state was |+⟩, and after H the |+⟩ state maps back to |0⟩. If correction failed, we would see a mix of Zero and One outcomes.

Understanding the Syndrome Table

The syndrome measurement checks parity between pairs of qubits without revealing their individual values. This is the key insight that makes quantum error correction possible. The two syndrome bits form a lookup table:

Syndrome (s0, s1)Interpretation
(Zero, Zero)No error
(One, Zero)Qubit 0 flipped
(One, One)Qubit 1 flipped
(Zero, One)Qubit 2 flipped

Because the ancilla qubits only measure parity (XOR of pairs), they learn whether two qubits agree or disagree, not what values they hold. The encoded superposition α|000⟩ + β|111⟩ is preserved.

The 3-Qubit Phase Flip Code

The bit flip code corrects X errors but is completely vulnerable to phase flip (Z) errors. A Z error transforms α|0⟩ + β|1⟩ into α|0⟩ - β|1⟩, which flips the relative phase rather than the bit value.

The phase flip code uses the same structure as the bit flip code, but works in the Hadamard basis. By encoding in the |+⟩/|-⟩ basis instead of the |0⟩/|1⟩ basis, a Z error looks like an X error in the rotated frame.

PhaseFlipCode.qs

namespace ErrorCorrection {
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Measurement;

    /// Encodes a logical qubit using the phase flip code.
    /// Logical |0⟩_L = |+++⟩, Logical |1⟩_L = |---⟩
    operation EncodePhaseFlip(
        source : Qubit, ancilla1 : Qubit, ancilla2 : Qubit
    ) : Unit {
        CNOT(source, ancilla1);
        CNOT(source, ancilla2);
        // Now in bit flip encoding. Rotate to Hadamard basis.
        H(source);
        H(ancilla1);
        H(ancilla2);
    }

    /// Decodes the phase flip code.
    operation DecodePhaseFlip(
        source : Qubit, ancilla1 : Qubit, ancilla2 : Qubit
    ) : Unit {
        H(source);
        H(ancilla1);
        H(ancilla2);
        CNOT(source, ancilla2);
        CNOT(source, ancilla1);
    }

    /// Detects and corrects a single Z error on 3 qubits
    /// encoded in the phase flip code.
    operation CorrectPhaseFlip(q0 : Qubit, q1 : Qubit, q2 : Qubit) : Unit {
        // Rotate to computational basis where Z errors become X errors
        H(q0);
        H(q1);
        H(q2);

        // Now use the same syndrome measurement as the bit flip code
        let syndrome = MeasureSyndrome(q0, q1, q2);
        CorrectBitFlip(q0, q1, q2, syndrome);

        // Rotate back to Hadamard basis
        H(q0);
        H(q1);
        H(q2);
    }

    /// Demonstrates the phase flip code with an injected Z error.
    operation DemoPhaseFlipCode(errorQubit : Int) : Result {
        use q = Qubit[3];

        // Prepare initial state |1⟩
        X(q[0]);

        // Encode using phase flip code
        EncodePhaseFlip(q[0], q[1], q[2]);

        // Inject a phase flip error
        if errorQubit >= 0 and errorQubit <= 2 {
            Z(q[errorQubit]);
        }

        // Correct the phase flip
        CorrectPhaseFlip(q[0], q[1], q[2]);

        // Decode
        DecodePhaseFlip(q[0], q[1], q[2]);

        // Measure to verify recovery
        let result = M(q[0]);
        ResetAll(q);
        return result;
    }
}

Expected Output

Error on qubit 0: measured One 20/20 times
Error on qubit 1: measured One 20/20 times
Error on qubit 2: measured One 20/20 times

We always recover the original |1⟩ state, confirming that the Z error was successfully corrected.

From 3-Qubit Codes to the Shor Code

Neither the bit flip code nor the phase flip code alone can handle arbitrary single-qubit errors. A general error on a qubit is a combination of X, Z, and Y = iXZ errors. Peter Shor’s 9-qubit code concatenates both codes: it first encodes against phase flips (using 3 qubits), then encodes each of those against bit flips (using 3 more qubits each), yielding 9 physical qubits per logical qubit. This was the first quantum code proven to correct arbitrary single-qubit errors.

Modern codes like the surface code and color codes achieve much better encoding rates and are designed to match the connectivity constraints of real hardware. Q# and the Azure Quantum ecosystem provide tools for experimenting with these advanced codes as the field progresses toward fault-tolerant quantum computing.

Was this tutorial helpful?