Stim emission
Stim is a fast stabilizer-circuit
simulator, and its .stim text format is a common way to exchange
quantum error-correction experiments. A .stim file holds the physical
operations and noise. It also holds annotations that say which measurement
parities signal errors and which result is the logical outcome. Exporting from
CUDA-Q Logical lets you analyze gadgets you built and checked here with
Stim and the tools around it, such as samplers, error-model extraction, and
decoders.
This page covers two ways to export:
From Python (the main tutorial). Assemble a surface-code memory experiment from built-in gadgets: prepare logical zero, store it for several rounds, and read out logical Z. Export it with detectors, a logical observable, and data noise. You end with
surface-memory.stimand, optionally, sampled detection events and logical-Z flips.From an MLIR module. Convert an existing gadget to its ideal circuit with
qlx-translate. See Native MLIR emission.
Export a surface-code memory with detector contracts
This tutorial uses ideal MPP stabilizer measurements and explicit data noise. For physical ancilla extraction with circuit-level gate, reset and measurement noise, start with Physical surface-code memory. The advanced reference comparison adds individual gadget-model exports and checks against Stim’s generated circuit.
With CUDA-Q Logical installed, run the surface-memory example from the repository root:
python3 preview/logical/examples/gadgets/02_stim_emission.py > surface-memory.stim
The default experiment uses a distance-three code and three rounds of
stabilizer measurements. Before each round, every data qubit receives an error
with probability 0.001. The script prints the circuit to stdout, so the
command saves it to surface-memory.stim. To also sample the circuit, see
Sample and interpret the results. The
sampling summary goes to stderr, so it stays out of the circuit file.
Read the exported circuit
An abridged surface-memory.stim looks like this:
QUBIT_COORDS(1, 1) 0
# ... coordinates for data qubits 1-8
R 0 1 2 3 4 5 6 7 8
DEPOLARIZE1(0.001) 0
# ... the same channel on data qubits 1-8
MPP X0*X1
MPP X1*X2*X4*X5
MPP X3*X4*X6*X7
MPP X7*X8
MPP Z0*Z1*Z3*Z4
MPP Z2*Z5
MPP Z3*Z6
MPP Z4*Z5*Z7*Z8
# ... two more rounds of noise and the same eight checks
MPP Z0*Z1*Z2
DETECTOR rec[-21]
# ... rec[-20] through rec[-18]
DETECTOR rec[-25] rec[-17]
# ... 15 more comparisons between consecutive rounds
OBSERVABLE_INCLUDE(0) rec[-1]
Reading from the top:
QUBIT_COORDSplaces the nine data qubits on the lattice for Stim’s diagrams.Rresets every data qubit to|0⟩.Each
DEPOLARIZE1(0.001)is an injected data error, applied before each round.Each
MPPis one ideal stabilizer measurement: a Pauli-product measurement made directly on the data qubits, with no ancilla qubits. A round measures four X-type and four Z-type checks, eight results in all.MPP Z0*Z1*Z2reads out logical Z.The
DETECTORandOBSERVABLE_INCLUDEannotations follow the last measurement.rec[-k]counts backkresults from the end. The circuit has 25 results (three rounds of eight plus the readout), sorec[-25]is the first check of round one andrec[-17]is the first check of round two.DETECTOR rec[-25] rec[-17]therefore says that those two results should agree.
The experiment has 20 detectors:
Four from round one. A reset to
|0…0⟩fixes every Z-type stabilizer to +1, so each first-round Z-check result (rec[-21]throughrec[-18]) is a detector by itself. X-type checks give random results on|0…0⟩, so in round one they only establish a baseline.Eight from round two, each comparing a check with its round-one result.
Eight from round three, each comparing a check with its round-two result.
The single OBSERVABLE_INCLUDE(0) marks the logical-Z readout.
The following steps follow the example’s # %% cells and show where each part
of the circuit comes from. You can run the full script, or work through its
cells interactively.
Choose the experiment size and noise
Start with the parameter cell:
distance = 3
rounds = 3
p = 0.001
# Only explicit data noise is enabled; the native MPP operations are ideal.
noise = cql.compiler.StandardDepolarizingNoise()
data_depolarization = p
# Set shots > 0 to sample with Stim (pip install 'stim>=1.16,<2'); exporting
# text does not require Stim.
shots = 0
seed = 7
distancesets the code size: distance three uses nine data qubits. Choose an odd integer of at least three.roundscounts stabilizer-measurement rounds, including preparation’s first round. Increasing it stores the qubit longer and exposes it to more noise.pis the probability of a data error on each qubit before each round. X, Y, and Z errors are equally likely. Set it to zero for a noiseless check.shotsis the number of sampled repetitions. The default, zero, only exports the circuit;seedmakes sampling reproducible for a fixed circuit.
The stabilizer measurements are ideal. With its defaults,
StandardDepolarizingNoise() adds no gate, reset, or measurement noise. The
example’s explicit data errors are the only noise in the circuit.
Build preparation, storage, and readout
Select the surface code at your chosen distance and its qubit coordinates:
implementation = surface.definitions(distance)
code = implementation.code
The experiment uses three gadgets, each a reusable operation on the encoded qubit:
prepare_zeroresets the data and measures the stabilizers once, which prepares logical zero.memory_roundmeasures the stabilizers again while preserving the stored logical value.measure_zreads out logical Z and finishes the experiment.
Stabilizer measurements check for errors without reading out the stored logical value. An error between two rounds changes some check results, and comparing consecutive rounds reveals the change.
Describe which results signal errors
A gadget specifies only quantum operations. To annotate the circuit, the exporter also needs to know which parities of results should be zero without errors and which result is the logical outcome. A gadget contract attaches these rules to a gadget:
Detectors are parities of measurement results that are zero when no error occurs. Each becomes a
DETECTOR, and a nonzero value in a shot is a detection event.Virtual syndromes are the stabilizer values a gadget expects at its boundary. They are inferred from earlier results, not measured at the boundary. A gadget reads an incoming virtual syndrome from the gadget before it and binds an outgoing one for the gadget after it. This lets a detector compare results measured by two different gadgets.
Logical observables are parities that form a logical measurement outcome, here the logical-Z readout. They come from the readout gadget’s outcome map; see Detector map and outcome map.
The memory round shows how these fit together. For each check i, it
declares the detector incoming[i] ^ round[i], because in an ideal round the
new result equals the expected value. It then binds its own results as the
outgoing virtual syndrome, so they become the next round’s baseline. In the
contract API, these steps are one builder.detector(incoming[i] ^ record) call
per check followed by builder.bind(outgoing, records).
Declare a contract
works through a complete Steane memory round.
The three surface contracts produce the detectors described earlier:
prepare_zerodeclares the four first-round Z-check detectors and binds all eight results as its outgoing virtual syndrome.memory_roundcompares each of its eight results with the incoming virtual syndrome and passes its own results on.measure_zdeclares the logical observable.
surface.gadget_contract(gadget) returns these contracts. It recognizes only
gadget objects created by surface.definitions. Any other gadget raises
ValueError, even one with the same name or code. See
Recipe-provided contracts.
The reference pages also use the term detector contract: a gadget’s
detectors and virtual-syndrome bindings, together with the code at each
boundary. GadgetContract holds these declarations together with observables
and success conditions.
Assemble the experiment and add data errors
The example assembles preparation, repeated SEC occurrences and direct logical readout explicitly:
prep = surface.gadget_contract(implementation.prepare_zero)
sec = surface.gadget_contract(implementation.memory_round)
readout = surface.gadget_contract(implementation.measure_z)
# Preparation contributes one round; this ideal-MPP readout contributes none.
profiles = (prep, *((sec,) * (rounds - 1)), readout)
Preparation contributes the first round, so rounds = 3 adds two memory rounds
before the readout.
The example then chooses which occurrences receive data depolarization.
surface.data_depol(contract, projection, probability) resolves the gadget’s
declared before_syndrome site into ordinary Depolarize1Placement objects.
The supplied projection must match the concrete realization. The helper does
not compile it again or choose an experiment sequence.
At this site each data qubit receives one categorical X/Y/Z channel of total
probability data_depolarization. Preparation’s site follows reset, so reset
does not erase the errors. The direct logical readout has no such site and
receives no additional noise in this example.
projections = {
contract.gadget:
cql.compiler.project_clifford(cql.compile(contract),
gadget=contract.gadget.name)
for contract in (prep, sec)
}
data_sites = {
contract.gadget:
surface.data_depol(contract, projections[contract.gadget],
data_depolarization) for contract in (prep, sec)
}
placements = {
index: data_sites[contract.gadget]
for index, contract in enumerate(profiles[:-1])
}
Save the Stim circuit
compiler.emit_stim combines the contracts into one annotated circuit. It
accepts a closed single-block chain:
the first contract prepares one encoded block from nothing;
each interior contract takes that block in and passes it on;
the last contract consumes it with a measurement.
Adjacent contracts must use the same code presentation, meaning the same code
and stabilizer ordering, so that each outgoing virtual syndrome lines up with
the next gadget’s incoming one. emit_stim substitutes the outgoing virtual
syndrome into the next gadget’s detectors. That substitution turns
incoming[i] ^ round[i] into a comparison of two concrete results, such as
DETECTOR rec[-25] rec[-17]. Only the last contract may declare observables, and
postselection is unsupported.
emit_stim checks the chain’s structure. It does not check that the declared
detectors are deterministic, so a contract you write yourself needs separate
verification (see Customize the experiment).
The example passes the contracts and noise placements to emit_stim and adds
qubit coordinates for visualization:
coordinates = "\n".join(f"QUBIT_COORDS({x}, {y}) {q}"
for q, (x, y) in enumerate(implementation.coordinates))
text = coordinates + "\n" + cql.compiler.emit_stim(
profiles, noise=noise, depolarizations=placements)
Sample and interpret the results
To enable the script’s sampling step, install Stim and set shots to a
positive number, such as 100:
python3 -m pip install 'stim>=1.16,<2'
Run the example again. Each shot simulates one complete repetition of the experiment. Stim returns two arrays:
detectorsrecords which detectors fired: one row per shot and one column per detector.observablesrecords whether the final logical-Z value flipped relative to the noiseless experiment: one row per shot and one column here.
With shots = 100, their shapes are (100, 20) and (100, 1). The sampling
step is:
if shots:
import stim
circuit = stim.Circuit(text)
sampler = circuit.compile_detector_sampler(seed=seed)
detectors, observables = sampler.sample(shots, separate_observables=True)
print(f"Sampled {shots} shots", file=sys.stderr)
print(f"Shots with detection events: {detectors.any(axis=1).sum()}/{shots}",
file=sys.stderr)
print(
"Shots with uncorrected observable flips: "
f"{observables.any(axis=1).sum()}/{shots}",
file=sys.stderr)
The summary counts shots with any detection event and shots with a logical-Z
flip. These results are uncorrected. To measure how well error correction
protects the stored qubit, pass the detection events to a decoder. Stim can
derive the detector error model that decoders take as input with
circuit.detector_error_model().
Try setting p = 0: both counts should be zero. Then restore the noise and
increase shots to explore its effects. Individual shots are random, so small
samples need not show every kind of error.
Customize the experiment
Change the parameters.
distance,rounds,p, andshotscan be changed independently. The detector count scales as 4 + 8 × (rounds− 1) at distance three. In general, it is the number of Z-type checks plusdistance² − 1comparisons for each memory round.Use your own gadgets. Recipe contracts exist only for the recipe’s gadgets, so use preparation, memory, and readout gadgets from the same
surface.definitions(distance)result. For a gadget you author, declare its contract before exporting. The sequence must still form a closed chain on one encoded block.Add circuit-level noise. The
StandardDepolarizingNoisefields add noise to physical gates, resets, and measurements. Two limits apply:The surface recipe measures stabilizers with
MPP. Settingbefore_measure_flip_probabilityraisesValueError, because measurement-flip noise on a Pauli-product measurement needs an explicit fault model.after_clifford_depolarizationalso adds noise afterSWAPs that come fromfabric.permute, even though those represent noiseless relabeling. This surface example has no permutations, but gadgets that use them receive extra noise.
Native MLIR emission
The Python export starts from gadget contracts. It knows which results to
compare, carries virtual syndromes from one gadget to the next, and adds noise.
qlx-translate --fabric-to-stim starts instead from an MLIR module, written by
hand or saved from an earlier compile, and converts one of its gadgets to Stim
directly. The output is the ideal circuit only: resets, Clifford gates,
and measurements, with no detectors, observables, or noise. Use it to inspect
what a gadget physically does, or as the start of a Stim analysis where you
add the annotations yourself.
This section uses a few terms from the compiler’s stages:
A P2 module is the QEC-realization stage, where codes and gadgets are chosen (see Concepts). An entry gadget is the gadget marked
{entry}, which the command converts.Carriers are the physical qubits of a realization. They become Stim qubit indices.
A code’s partitions name groups of its carriers, such as
data,sx(X-check ancillas), andsz(Z-check ancillas).
Emit from the command line
qlx-translate ships as a console script with the cudaq-logical wheel, so an
installed package is enough. This module holds one round of CSS syndrome
extraction on the Steane [[7,1,3]] code, followed by a destructive data readout:
qlx-translate preview/logical/examples/mlir/stim_steane_memory.mlir \
--fabric-to-stim
R 7 8 9
H 7 8 9
CX 7 0 7 1 7 2 7 3 8 0 8 1 8 4 8 5 9 0 9 2 9 4 9 6
H 7 8 9
R 10 11 12
CX 0 10 1 10 2 10 3 10 0 11 1 11 4 11 5 11 0 12 2 12 4 12 6 12
M 7 8 9
M 10 11 12
M 0 1 2 3 4 5 6
The circuit uses thirteen carriers, all of them explicit. They follow the code’s partitions:
Data qubits are 0–6.
X-check ancillas are 7–9.
Z-check ancillas are 10–12.
Each CX line expands the three stabilizer rows of hx or hz into carrier
pairs. Unlike the surface recipe’s ideal MPP checks, this module writes out
the syndrome-extraction circuit explicitly: the ancilla resets, the H and
CX gates, and the measurements.
The gadget takes its encoded patch as an entry argument and assumes the data carriers are already initialized. Its circuit resets only the ancillas. To simulate a particular encoded input state, prepend that state’s preparation to this circuit.
examples/mlir/stim_memory.mlir is the smallest case: a bare one-carrier
code whose whole circuit is R 0 then M 0.
Fail-closed boundaries
When the command can’t convert a module faithfully, it stops with an error. It never guesses a missing implementation. It requires that:
the module passes native MLIR verification;
the root is a legalized P2 entry gadget. Run
fabric-lower-protocolson protocols first;the module has no external resource requests (
fabric.resource_request). Stim text can’t name an incoming resource stream;fabric.prep_zandfabric.prep_xappear only on a verified trivial one-carrier code (n=1, k=1, r=0). Encoded preparation must be lowered to explicit physical operations, as the surface recipe in the Python tutorial does;the Fabric call graph has no recursion.
The command doesn’t derive an error model, sample, or decode. To get the same instructions as typed Python objects, with exact boundary and measurement records, see Project the realization.
Where to go next
To derive an error model directly from the gadgets and sample it in CUDA-Q QEC, continue with Derive an error model from gadgets.
To write and verify contracts for your own gadgets, read Detector contracts and Clifford projection.
To map common Stim workflows onto CUDA-Q Logical, see For Stim users.
Executing a supported P3 build on CUDA-Q’s Stim target uses cudaq.run; see
CUDA-Q Logical for Stim users.