Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Simulating Error Syndromes and Pauli Errors in Stabilizer Codes

Hello! Welcome to the next lesson in our deep dive into quantum error correction.

In our last few lessons, we've explored the design of quantum error-correcting codes, culminating in the bias-tailored XZZX code. Designing a good code is a crucial first step, but it's only half the story. We also need an efficient algorithm—a decoder—to interpret the error syndromes and determine the correct recovery operation.

While classical algorithms like Minimum Weight Perfect Matching (MWPM) are workhorses for codes like the surface code, an exciting and modern approach is to frame decoding as a machine learning problem. Given your background as an ML research engineer, this intersection should be particularly interesting. Today, we'll take the first practical step in this direction.

This lesson addresses the learning outcome: Generate a training dataset of error syndromes and corresponding Pauli errors for a small stabilizer code under a depolarizing noise model. We will learn how to simulate a noisy quantum error correction circuit and extract the two key ingredients for an ML decoder: the features (syndromes) and the labels (the ground-truth errors that occurred).

1. Decoding as a Machine Learning Task

Before we generate data, let's briefly conceptualize the problem from an ML perspective. An error-correcting code is repeatedly probed by measuring its stabilizer operators. In an ideal, noiseless world, these measurements always yield the same outcome (e.g., +1). When a noisy event (a Pauli error) occurs, it flips the outcome of one or more adjacent stabilizer measurements.

The job of a decoder is to solve an inverse problem:

  • Input (Features): A bitstring representing the error syndrome. This is a record of which stabilizer measurements changed their outcome, indicating that an error has occurred nearby.
  • Output (Prediction): The most likely chain of Pauli errors that could have produced the observed syndrome.
  • Goal: The predicted error chain, when applied again, should cancel the actual error and return the system to the correct logical state.

The training data for such a model consists of many pairs of (syndrome, actual_error). Our goal today is to generate thousands of these pairs.

The paper "Resource-Efficient Decoding of Topological Color Codes" illustrates this approach, using a Recurrent Neural Network (RNN) to predict error probabilities from syndrome data.

Resource-Efficient Decoding of Topological Color Codes ...

To see how a machine learning model is structured for this task, please read this section from 'Resource-Efficient Decoding of Topological Color Codes...'. It describes how an RNN is used and what the training labels represent.

Please read Section 3.4, 'Training Dataset and Learning Methodology'. Focus on the descriptions of the model architecture, the training labels ('ground-truth Pauli error positions'), and the overall training setup. You don't need to dive deep into the LSTM equations, but rather get a high-level view of how the problem is framed for an ML pipeline.

As the paper discusses, the core task is to create a dataset where "ground-truth Pauli error positions" are the labels. We will now learn how to generate these features and labels using a powerful, high-performance tool built for this exact purpose: Stim.

2. Introducing Stim: A Quantum Stabilizer Simulator

Stim is a C++-based Python package developed by Craig Gidney at Google Quantum AI. It is not a general-purpose quantum computer simulator; instead, it is hyper-optimized for simulating Clifford circuits, noise, and stabilizer measurements. This makes it exceptionally fast for the large-scale simulations needed in quantum error correction research.

We will walk through the process of using Stim to generate our dataset. A fantastic introduction and tutorial is provided by Craig Gidney himself.

Tutorial - Simulating Quantum Stabilizer Circuits in Python with Stim

This video, 'Simulating Quantum Stabilizer Circuits in Python with Stim', provides a practical walkthrough of the key concepts we'll be using. We will focus on defining a circuit, adding noise, and measuring syndromes.

Please watch the segment from 11:56 to 27:10. Don't worry about understanding every line of code immediately. Focus on these three core ideas: Adding Noise: How noise channels like X_ERROR and DEPOLARIZE1/DEPOLARIZE2 are added to the circuit definition (timestamp 15:39). Defining Detectors: The concept of a DETECTOR, which compares measurement outcomes from different times to identify syndrome 'events' (timestamp 20:53). The Output: How the simulation produces streams of detection events (syndromes), which look like 'spackle' rather than the 'streaks' of raw measurement data (timestamp 25:00).

3. Generating a Labeled Dataset: The Workflow

Now, let's synthesize the ideas from the paper and the video into a concrete workflow for generating our (syndrome, error) pairs. We will use a simple repetition code as our example stabilizer code.

Step 1: Define the Noisy Circuit

First, we define the circuit for a distance-3 repetition code over several rounds. Crucially, we insert noise channels into the circuit definition. We'll use a depolarizing noise model, as specified by our learning outcome. In Stim, this is done with DEPOLARIZE1 for single-qubit errors and DEPOLARIZE2 for two-qubit gate errors. This channel applies a random Pauli error (, , or ) with a certain probability.

The paper "MITS: A Quantum Sorcerer's Stone..." provides a clear summary of how different noise sources are modeled in Stim simulations, which is a common practice in QEC research.

MITS: A Quantum Sorcerer's Stone for Designing Surface ...

This paper details the noise models used in their Stim-based dataset generation. It gives a concise overview of the different error types.

Please read Section 3.1, 'Modeling Noise for Experimental Setup'. This will reinforce your understanding of what depolarizing noise and other common errors represent in the simulation.

Here's how you might define a noisy repetition code circuit in Stim:

import stim

# Parameters for our simulation
distance = 3
rounds = 4
noise_prob = 0.01

# Qubit indices
data_qubits = [0, 1, 2]
measure_qubits = [3, 4]

circuit = stim.Circuit()

# Initialize data qubits
circuit.append("R", data_qubits) # R is Reset

# Add detectors for comparing measurements across rounds
for r in range(rounds):
    # Add depolarizing noise at the start of each round
    circuit.append("DEPOLARIZE1", data_qubits, noise_prob)

    # Measure stabilizers
    circuit.append("CNOT", [data_qubits[0], measure_qubits[0]])
    circuit.append("CNOT", [data_qubits[1], measure_qubits[0]])
    circuit.append("CNOT", [data_qubits[1], measure_qubits[1]])
    circuit.append("CNOT", [data_qubits[2], measure_qubits[1]])
    
    # Add depolarizing noise to the CNOT gates
    circuit.append("DEPOLARIZE2", [data_qubits[0], measure_qubits[0]], noise_prob)
    circuit.append("DEPOLARIZE2", [data_qubits[1], measure_qubits[0]], noise_prob)
    circuit.append("DEPOLARIZE2", [data_qubits[1], measure_qubits[1]], noise_prob)
    circuit.append("DEPOLARIZE2", [data_qubits[2], measure_qubits[1]], noise_prob)

    # Measure the ancillary qubits
    circuit.append("MR", measure_qubits) # MR is Measure and Reset
    
    # Define detectors
    # A detector compares a measurement with a previous one.
    # The record_target is relative: -1 is the last measurement, -2 is the one before that.
    # The number of measurements per round is len(measure_qubits).
    num_measurements = len(measure_qubits)
    circuit.append("DETECTOR", [
        stim.target_rec(-1), # Latest measurement on measure_qubit[1]
        stim.target_rec(-1 - num_measurements) # Measurement on same qubit, previous round
    ])
    circuit.append("DETECTOR", [
        stim.target_rec(-2), # Latest measurement on measure_qubit[0]
        stim.target_rec(-2 - num_measurements) # Measurement on same qubit, previous round
    ])

# Final measurement of data qubits to check for logical errors
circuit.append("M", data_qubits)
circuit.append("OBSERVABLE_INCLUDE", [stim.target_rec(-1), stim.target_rec(-2), stim.target_rec(-3)], 0)

print(circuit)

Step 2: Generate Syndromes (Features)

As you saw in the video, the DETECTOR instruction is the key to getting syndromes. It compares measurement results that are expected to be identical and flags a "detection event" if they are not. Running the sampler on a circuit with detectors gives us our syndrome data.

# Compile a sampler optimized for getting detector data
sampler = circuit.compile_detector_sampler()
syndromes, logical_flips = sampler.sample(shots=1000, bit_packed=False)

print("Shape of syndrome data:", syndromes.shape)
# Output: Shape of syndrome data: (1000, 8) 
# 1000 shots, 8 total detectors (2 detectors per round for 4 rounds)

print("\nFirst 5 syndrome samples:")
print(syndromes[:5, :])

This syndromes array is the feature set for our ML model. Each row is a syndrome graph from one run of the experiment.

Test your understanding!

In the code above, we define 2 detectors per round for 4 rounds, for a total of 8 detectors. If you run sampler.sample(shots=10), what will be the shape of the resulting syndromes numpy array? What does each element in that array represent?

Show answer

The shape of the array will be (10, 8). There will be 10 rows, one for each shot (experimental run). There will be 8 columns, one for each detector defined in the circuit. Each element will be a boolean (0 or 1), where 1 means that the corresponding detector was "triggered," indicating a likely error nearby.

Step 3: Generate Pauli Errors (Labels)

This is the most critical part for creating a labeled dataset. We need the ground-truth Pauli error that caused each syndrome. Stim provides a powerful abstraction for this: the Detector Error Model (DEM).

A DEM is a graph that connects fundamental error sources (like a Pauli on qubit 1) to their consequences (detectors D3 and D5 firing). We can generate this model from our circuit and then sample from it.

The key is to use circuit.detector_error_model(decompose_errors=True). The decompose_errors=True flag tells Stim to create a DEM where each fundamental error corresponds to a single, physical Pauli error.

# 1. Generate the Detector Error Model (DEM)
# decompose_errors=True is crucial! It makes each error in the DEM a single physical Pauli error.
dem = circuit.detector_error_model(decompose_errors=True)

# 2. Sample errors and their corresponding syndromes from the DEM
# This simulates the noise process directly and tells us BOTH the cause and effect.
num_samples = 1000
pauli_errors, syndromes = dem.sample(shots=num_samples, return_errors=True)

print("Shape of Pauli error data (labels):", pauli_errors.shape)
print("Shape of syndrome data (features):", syndromes.shape)

# The number of possible single-qubit Pauli errors is the number of error instructions in the DEM
num_possible_errors = dem.num_errors
print(f"\nThere are {num_possible_errors} possible single-Pauli errors in this circuit's noise model.")
print(f"The pauli_errors array has shape ({num_samples}, {num_possible_errors})")

print("\nExample of a sampled error and its syndrome:")
# Find a shot where at least one error occurred
shot_with_errors = -1
for i in range(num_samples):
    if pauli_errors[i].any():
        shot_with_errors = i
        break

if shot_with_errors != -1:
    print(f"Sample shot: {shot_with_errors}")
    error_indices = [i for i, val in enumerate(pauli_errors[shot_with_errors]) if val]
    print(f"  - Pauli errors occurred at indices: {error_indices}")
    print(f"  - Resulting syndrome: {syndromes[shot_with_errors].astype(int)}")

The pauli_errors array is our set of labels. Each row is a multi-hot encoded vector indicating which of the possible physical Pauli errors occurred in that shot. The syndromes array contains the corresponding features. You now have a complete, labeled dataset ready for training a machine learning model.

Conclusion

In this lesson, we bridged the gap between quantum error correction theory and machine learning practice. We've established a concrete, programmatic workflow for generating the essential data needed to train a neural network decoder.

Key Takeaways:

  • Decoding as ML: The QEC decoding problem can be framed as a supervised learning task where the model learns to map syndromes (features) to Pauli errors (labels).
  • Stim for Data Generation: Stim is a high-performance tool perfectly suited for this task. It allows for the definition of noisy stabilizer circuits and efficient sampling.
  • Syndromes via Detectors: Stim's DETECTOR instruction is the mechanism for extracting error syndromes by comparing measurement outcomes across time.
  • Labels via Detector Error Model (DEM): By converting a circuit to a DEM with decompose_errors=True and using dem.sample(return_errors=True), we can simultaneously generate syndromes and the corresponding ground-truth Pauli errors that caused them.

Preview of the next lesson:
Now that we can generate a labeled dataset, the next logical step is to use it. In the next lesson, we will implement, train, and evaluate a simple feed-forward neural network to predict the most likely Pauli error from a given error syndrome. This will complete our first full cycle of building and testing an ML-based quantum error decoder.

Can't find a good explanation? Sign up and we'll make it for you

Sign up