python v1.14.x

Stim

Craig Gidney's high-performance stabilizer circuit simulator for quantum error correction

Quick install

pip install stim

Background and History

Stim is a high-performance stabilizer circuit simulator created by Craig Gidney at Google Quantum AI, first released in 2021. Gidney published the paper “Stim: a fast stabilizer circuit simulator” (Quantum, 2021) describing the architecture and benchmarks. The tool was built to solve a specific problem: simulating quantum error correction circuits at the scale needed for serious research, where general-purpose state vector simulators are far too slow.

The core insight behind Stim is that quantum error correction codes (surface codes, color codes, repetition codes) use only Clifford gates and Pauli measurements. This restricted gate set can be simulated classically in polynomial time using the stabilizer formalism (the Gottesman-Knill theorem). Stim exploits this by representing the quantum state as a stabilizer tableau and tracking Pauli frames through the circuit, using SIMD (Single Instruction, Multiple Data) vectorization to process many shots in parallel. The result is a simulator that is 100x to 1000x faster than general-purpose simulators for Clifford circuits, capable of sampling billions of measurement shots from circuits with thousands of qubits.

Stim was used in Google’s landmark 2022 paper demonstrating that increasing surface code distance reduces logical error rates on their Sycamore processor. It has since become the standard simulation tool in the quantum error correction research community, used by groups at Google, IBM, AWS, Delft, ETH Zurich, and many universities. Stim is open-source under the Apache 2.0 license, hosted in the quantumlib GitHub organization alongside Cirq and other Google quantum tools.

Beyond raw simulation speed, Stim introduced an important abstraction: the detector error model (DEM). A DEM extracts the noise-relevant structure from a noisy Clifford circuit into a compact graph that decoders can consume directly. This separation of “circuit simulation” from “decoding” has become a standard architectural pattern in QEC research pipelines.

Installation

pip install stim

For the latest development version directly from source:

pip install git+https://github.com/quantumlib/Stim.git

Stim is also available as a C++ library for embedding into high-performance applications. The C++ headers are included in the repository and can be built with CMake:

git clone https://github.com/quantumlib/Stim.git
cd Stim
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

Stim also ships a command-line tool. After pip install stim, the stim command is available in your shell.

Key Concepts

Stim is organized around four core abstractions:

  • Circuit: a sequence of Clifford gates, noise channels, measurements, detectors, and observable annotations. Circuits are the primary input to Stim.
  • Tableau: the internal stabilizer representation of a quantum state. A tableau tracks how each stabilizer generator transforms under circuit operations.
  • Detector Error Model (DEM): a noise-level summary of a circuit, describing which errors can flip which detectors and observables. DEMs are the bridge between simulation and decoding.
  • Sampler: a compiled object that draws measurement samples (or detection event samples) from a circuit at high speed.

Stim operates exclusively within the Clifford group. It cannot simulate T gates, arbitrary rotations, or any non-Clifford operation. This restriction is fundamental to its performance; it is not a limitation that will be removed in future versions.

Core Circuit Operations

import stim

# Build a circuit from a string (Stim's native circuit format)
circuit = stim.Circuit("""
    H 0
    CNOT 0 1
    M 0 1
""")

# Build a circuit programmatically
circuit = stim.Circuit()
circuit.append("H", [0])
circuit.append("CNOT", [0, 1])
circuit.append("M", [0, 1])

# View the circuit
print(circuit)
# Output:
# H 0
# CX 0 1
# M 0 1

# Get circuit properties
print(circuit.num_qubits)        # 2
print(circuit.num_measurements)  # 2

Gate Reference

Clifford Gates (Single-Qubit)

circuit.append("H", [0])          # Hadamard
circuit.append("S", [0])          # S gate (sqrt(Z), phase gate)
circuit.append("S_DAG", [0])      # S-dagger (inverse of S)
circuit.append("SQRT_X", [0])     # sqrt(X) rotation
circuit.append("SQRT_X_DAG", [0]) # sqrt(X)-dagger
circuit.append("SQRT_Y", [0])     # sqrt(Y) rotation
circuit.append("SQRT_Y_DAG", [0]) # sqrt(Y)-dagger
circuit.append("SQRT_Z", [0])     # same as S
circuit.append("SQRT_Z_DAG", [0]) # same as S_DAG
circuit.append("X", [0])          # Pauli-X
circuit.append("Y", [0])          # Pauli-Y
circuit.append("Z", [0])          # Pauli-Z
circuit.append("I", [0])          # Identity (useful for timing alignment)

Clifford Gates (Two-Qubit)

circuit.append("CNOT", [0, 1])    # CNOT (CX), control=0, target=1
circuit.append("CX", [0, 1])      # synonym for CNOT
circuit.append("CY", [0, 1])      # Controlled-Y
circuit.append("CZ", [0, 1])      # Controlled-Z
circuit.append("SWAP", [0, 1])    # SWAP
circuit.append("ISWAP", [0, 1])   # iSWAP
circuit.append("ISWAP_DAG", [0, 1])  # iSWAP-dagger
circuit.append("XCX", [0, 1])     # X-controlled-X
circuit.append("XCY", [0, 1])     # X-controlled-Y
circuit.append("XCZ", [0, 1])     # X-controlled-Z
circuit.append("YCX", [0, 1])     # Y-controlled-X
circuit.append("YCY", [0, 1])     # Y-controlled-Y
circuit.append("YCZ", [0, 1])     # Y-controlled-Z

Measurement and Reset

circuit.append("M", [0, 1])       # Measure qubits in Z basis
circuit.append("MX", [0])         # Measure in X basis
circuit.append("MY", [0])         # Measure in Y basis
circuit.append("MR", [0, 1])      # Measure in Z basis, then reset to |0>
circuit.append("MRX", [0])        # Measure in X basis, then reset to |+>
circuit.append("MRY", [0])        # Measure in Y basis, then reset
circuit.append("R", [0, 1])       # Reset to |0> (without measurement)
circuit.append("RX", [0])         # Reset to |+>
circuit.append("RY", [0])         # Reset to |+i>

Noise Channels

# Single-qubit depolarizing noise (probability p of random Pauli)
circuit.append("DEPOLARIZE1", [0], 0.001)

# Two-qubit depolarizing noise (probability p of random two-qubit Pauli)
circuit.append("DEPOLARIZE2", [0, 1], 0.01)

# Pauli error channels
circuit.append("X_ERROR", [0], 0.01)      # X error with probability 0.01
circuit.append("Z_ERROR", [0], 0.01)      # Z error with probability 0.01
circuit.append("Y_ERROR", [0], 0.001)     # Y error with probability 0.001

# General single-qubit Pauli channel: P(X)=px, P(Y)=py, P(Z)=pz
circuit.append("PAULI_CHANNEL_1", [0], [0.01, 0.002, 0.01])

# General two-qubit Pauli channel (15 probabilities for IX, IY, IZ, XI, ...)
circuit.append("PAULI_CHANNEL_2", [0, 1], [0.001] * 15)

# Erasure error (heralded loss)
circuit.append("E", [stim.target_x(0)], 0.01)

Annotations (Detectors and Observables)

# TICK marks a visual layer boundary (no physical effect)
circuit.append("TICK")

# DETECTOR declares a parity check: product of specified measurement
# results should be deterministic absent errors.
# Arguments are relative measurement indices (negative = count back from current)
circuit.append("DETECTOR", [stim.target_rec(-1), stim.target_rec(-2)])

# DETECTOR with coordinate annotation (for visualization and matching)
circuit.append("DETECTOR", [stim.target_rec(-1), stim.target_rec(-2)], [3.0, 2.0, 0.0])

# OBSERVABLE_INCLUDE marks which measurements contribute to a logical observable
circuit.append("OBSERVABLE_INCLUDE", [stim.target_rec(-1)], 0)  # observable index 0

Repeat Blocks

# REPEAT compresses repeated circuit structure (essential for QEC circuits)
circuit_with_repeat = stim.Circuit("""
    REPEAT 100 {
        H 0
        CNOT 0 1
        DEPOLARIZE2(0.01) 0 1
        M 0 1
        DETECTOR rec[-1] rec[-3]
        DETECTOR rec[-2] rec[-4]
        R 0 1
    }
""")

Sampling Measurements

import stim
import numpy as np

circuit = stim.Circuit("""
    H 0
    CNOT 0 1
    M 0 1
""")

# Compile a sampler for fast repeated sampling
sampler = circuit.compile_sampler()

# Draw 10000 shots; result is a numpy bool array of shape (shots, num_measurements)
results = sampler.sample(shots=10000)
print(results.shape)  # (10000, 2)

# Check Bell state correlations: measurements should always agree
mismatches = np.sum(results[:, 0] != results[:, 1])
print(f"Mismatches: {mismatches}")  # 0 (no noise in this circuit)

# Write samples to a file in a specific format
sampler.sample_write(
    shots=100000,
    filepath="measurements.01",
    format="01",   # options: "01", "b8", "r8", "ptb64", "hits", "dets"
)

Detector Error Models

A detector error model (DEM) is the noise-relevant summary of a circuit. It lists the independent error mechanisms in the circuit and specifies which detectors and logical observables each error can flip. Decoders consume DEMs, not circuits.

import stim

# A simple repetition code circuit with noise
circuit = stim.Circuit("""
    X_ERROR(0.1) 0 1 2
    M 0 1 2
    DETECTOR rec[-1] rec[-2]
    DETECTOR rec[-2] rec[-3]
    OBSERVABLE_INCLUDE rec[-1] 0
""")

# Extract the detector error model
dem = circuit.detector_error_model()
print(dem)
# Output shows error mechanisms like:
# error(0.1) D0
# error(0.1) D0 D1
# error(0.1) D1 L0

# DEM properties
print(dem.num_detectors)     # number of detector nodes
print(dem.num_observables)   # number of logical observables
print(dem.num_errors)        # number of error mechanisms

# Round-trip: save and load DEMs
dem_string = str(dem)
dem_reloaded = stim.DetectorErrorModel(dem_string)

The DEM is the key interface between Stim and external decoders. PyMatching, Fusion Blossom, and other minimum-weight perfect matching (MWPM) decoders accept DEMs as input.

Detection Event Sampling

Detection events (the parity of detector outcomes relative to the noiseless case) are more useful than raw measurements for decoding:

import stim

# Noisy repetition code
circuit = stim.Circuit("""
    REPEAT 10 {
        X_ERROR(0.05) 0 1 2
        M 0 1 2
        DETECTOR rec[-1] rec[-4]
        DETECTOR rec[-2] rec[-5]
        DETECTOR rec[-3] rec[-6]
        R 0 1 2
    }
""")

# Compile a detector sampler
detector_sampler = circuit.compile_detector_sampler()

# Sample detection events and observable flips together
detection_events, observable_flips = detector_sampler.sample(
    shots=10000,
    separate_observables=True,
)

# detection_events: bool array (shots, num_detectors)
# observable_flips: bool array (shots, num_observables)
print(detection_events.shape)
print(observable_flips.shape)

Surface Code Example

A distance-3 rotated surface code memory experiment with depolarizing noise:

import stim

def make_surface_code_circuit(distance: int, rounds: int, noise: float) -> stim.Circuit:
    """Generate a noisy rotated surface code memory circuit using Stim's built-in generator."""
    circuit = stim.Circuit.generated(
        "surface_code:rotated_memory_z",
        rounds=rounds,
        distance=distance,
        after_clifford_depolarization=noise,
        after_reset_flip_probability=noise,
        before_measure_flip_probability=noise,
        before_round_data_depolarization=noise,
    )
    return circuit

# Distance-3 surface code, 10 rounds of stabilizer measurements, 0.1% noise
circuit = make_surface_code_circuit(distance=3, rounds=10, noise=0.001)

print(f"Qubits: {circuit.num_qubits}")
print(f"Measurements: {circuit.num_measurements}")
print(f"Detectors: {circuit.num_detectors}")
print(f"Observables: {circuit.num_observables}")

Stim’s Circuit.generated() method supports several built-in code families:

  • "repetition_code:memory" for repetition codes
  • "surface_code:rotated_memory_z" and "surface_code:rotated_memory_x" for rotated surface codes
  • "surface_code:unrotated_memory_z" for unrotated surface codes
  • "color_code:memory_xyz" for color codes

Full Decoding Pipeline with PyMatching

The standard Stim workflow pairs simulation with a decoder. PyMatching is the most common choice for MWPM decoding:

pip install pymatching
import stim
import numpy as np
import pymatching

# 1. Generate a noisy surface code circuit
circuit = stim.Circuit.generated(
    "surface_code:rotated_memory_z",
    rounds=10,
    distance=5,
    after_clifford_depolarization=0.001,
    before_round_data_depolarization=0.001,
    before_measure_flip_probability=0.001,
    after_reset_flip_probability=0.001,
)

# 2. Extract the detector error model
dem = circuit.detector_error_model(decompose_errors=True)

# 3. Build a MWPM decoder from the DEM
matcher = pymatching.Matching.from_detector_error_model(dem)

# 4. Sample detection events and observable flips
sampler = circuit.compile_detector_sampler()
detection_events, observable_flips = sampler.sample(
    shots=10000,
    separate_observables=True,
)

# 5. Decode each shot
predicted_observables = matcher.decode_batch(detection_events)

# 6. Count logical errors
num_errors = np.sum(np.any(predicted_observables != observable_flips, axis=1))
logical_error_rate = num_errors / detection_events.shape[0]
print(f"Logical error rate: {logical_error_rate:.4f}")

Stim Command-Line Interface

Stim includes a CLI for quick tasks without writing Python:

# Generate a surface code circuit
stim --gen surface_code:rotated_memory_z \
    --rounds 10 --distance 3 \
    --after_clifford_depolarization 0.001 \
    > circuit.stim

# Sample measurements
stim --sample 10000 < circuit.stim > measurements.01

# Sample detection events
stim --detect 10000 < circuit.stim > detections.01

# Extract detector error model
stim --analyze_errors < circuit.stim > dem.dem

Tableau Operations

Stim exposes the stabilizer tableau directly for low-level manipulation:

import stim

# Create a tableau for a known gate
cnot = stim.Tableau.from_named_gate("CNOT")
hadamard = stim.Tableau.from_named_gate("H")

# Compose tableaux
combined = hadamard.then(cnot)

# Create a tableau from a circuit
circuit = stim.Circuit("""
    H 0
    CNOT 0 1
    S 1
""")
tableau = circuit.to_tableau()

# Inspect stabilizer generators
print(tableau)

# Create random Clifford tableaux (useful for randomized benchmarking)
random_clifford = stim.Tableau.random(num_qubits=3)

Performance Notes

Stim achieves its speed through several design choices:

  • Pauli frame tracking: rather than updating the full stabilizer tableau for each gate, Stim tracks a “Pauli frame” that records how errors propagate. This is O(1) per gate per shot for most operations.
  • SIMD vectorization: Stim processes 256 shots simultaneously using AVX2 instructions (or 128 with SSE, 512 with AVX-512), packing Pauli data into bit vectors.
  • Compiled samplers: compile_sampler() and compile_detector_sampler() pre-compute the circuit’s structure once, then sample from a streamlined representation.
  • Reference sample caching: Stim computes one noiseless reference sample and then only tracks deviations from it.

Typical benchmarks for a distance-11 surface code circuit (hundreds of qubits, thousands of gates):

  • Stim: ~1 billion shots per hour
  • General stabilizer simulator (e.g., Qiskit Aer stabilizer): ~1 million shots per hour
  • State vector simulator: impractical at this scale

The fundamental limitation is that Stim can only simulate Clifford circuits. Any non-Clifford gate (T, Toffoli, arbitrary rotations) is unsupported. This is not a bug; it is the architectural tradeoff that makes the speed possible. For circuits that mix Clifford and non-Clifford gates, use a general simulator like Qiskit Aer or Cirq.

Comparison Table

FeatureStimQiskit Aer (stabilizer)Cirq
Primary purposeQEC simulationGeneral simulationGeneral simulation
Simulation methodPauli frame trackingStabilizer tableauState vector / density matrix
Clifford performanceExtremely fast (SIMD)ModerateModerate
Non-Clifford gatesNot supportedNot in stabilizer modeSupported
Max qubits (Clifford)ThousandsHundredsTens (state vector)
Detector error modelsBuilt-inNot availableNot available
QEC circuit generatorsBuilt-inNoNo
Decoder integrationPyMatching, Fusion BlossomNoneNone
Shot throughput~10^9/hour~10^6/hour~10^4/hour (state vector)
Output formats01, b8, r8, ptb64, hits, detsCounts dictMeasurement results
Circuit formatNative .stim text formatQASM / Qiskit IRCirq IR
  • PyMatching: Oscar Higgott’s minimum-weight perfect matching decoder. Consumes Stim DEMs directly. The standard decoder used alongside Stim in QEC research. Install with pip install pymatching.
  • Sinter: a benchmarking and statistics collection tool built on Stim. Automates the process of sweeping over noise rates, code distances, and decoder choices to produce threshold plots. Included as pip install sinter (or bundled with Stim’s extras).
  • Stimcirq: a bridge between Stim and Cirq, allowing Stim operations to be embedded in Cirq circuits and vice versa. Useful when you need Cirq’s visualization or other features alongside Stim’s speed. Install with pip install stimcirq.
  • Fusion Blossom: Yue Wu’s parallel MWPM decoder, compatible with Stim DEMs, designed for real-time decoding at scale.
  • Chromobius: a color code decoder built on top of Stim and PyMatching, also developed by Craig Gidney.
  • Crumble: Stim’s built-in circuit editor and visualizer, available as a web tool for building and inspecting QEC circuits interactively.