QEC sampling with Stim and Sinter¶
Sample Stim circuits through Maestro, and plug Maestro into Sinter as a sampler for error-correction benchmarks.
import stim
from maestro.sinter import MaestroCompiledSampler
circuit = stim.Circuit.generated(
"repetition_code:memory", rounds=3, distance=5, after_clifford_depolarization=0.01
)
maestro_events, maestro_flips = MaestroCompiledSampler(circuit).sample_detection_events(shots=5000)
stim_events, stim_flips = circuit.compile_detector_sampler(seed=1).sample(5000, separate_observables=True)
print("detection events:", maestro_events.shape)
print(f"detection event rate: Maestro {maestro_events.mean():.4f}, Stim {stim_events.mean():.4f}")
detection events: (5000, 16)
detection event rate: Maestro 0.0204, Stim 0.0221
Maestro runs the circuit gate by gate with Stim’s own noise, so its
detection-event rate matches Stim’s sampler. For Clifford circuits with Pauli
noise, Stim’s own sampler is the natural choice. Maestro covers what a
stabilizer simulator can’t: non-Clifford gates, coherent or device-level noise
from a NoiseModel, and MPS or density-matrix simulation.
Install¶
pip install "qoro-maestro[sinter]" pymatching
The sinter extra installs stim and sinter. The decoding examples below also
need PyMatching.
Classes and functions¶
Class or function |
Description |
|---|---|
|
A |
|
A |
|
Converts a clean Stim circuit, extended gate string or instruction tuples into |
Both samplers accept a SimulatorConfig (config=) and a NoiseModel
(noise_model=). MaestroCompiledSampler also returns raw measurement
records:
bell = stim.Circuit("""
H 0
CX 0 1
M 0 1
DETECTOR(0, 0) rec[-1] rec[-2]
""")
records = MaestroCompiledSampler(bell).sample_measurements(shots=1000)
print(records.shape, "bits always agree:", bool((records[:, 0] == records[:, 1]).all()))
(1000, 2) bits always agree: True
Requires GPU access
MaestroCompiledSampler(..., use_gpu=True) and GPU configs need the GPU
plugin. See Get GPU access.
Using Maestro in sinter collect¶
Pass MaestroSinterSampler to sinter.collect through custom_decoders. Here
the decoder prior carries Stim’s noise, while Maestro executes the clean circuit
on an MPS with its own NoiseModel:
import maestro
import sinter
from maestro.sinter import MaestroSinterSampler
prior = stim.Circuit.generated(
"surface_code:rotated_memory_z", rounds=3, distance=3, after_clifford_depolarization=0.001
)
task = sinter.Task(
circuit=prior.without_noise(),
detector_error_model=prior.detector_error_model(decompose_errors=True),
json_metadata={"d": 3, "r": 3},
)
mps_config = maestro.SimulatorConfig(
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
)
noise = maestro.NoiseModel()
for q in range(prior.num_qubits):
noise.set_depolarizing(q, 0.001)
stats = sinter.collect(
num_workers=2,
max_shots=2_000,
max_errors=50,
tasks=[task],
decoders=["maestro"],
custom_decoders={
"maestro": MaestroSinterSampler(config=mps_config, noise_model=noise, decoder="pymatching"),
},
)
for sample in stats:
print(f"{sample.errors} logical errors in {sample.shots} shots")
2 logical errors in 2000 shots
decoder= is required. Under sinter collect, task.decoder is the
custom_decoders key ("maestro" above), not a decoder, and an unresolved key
raises ValueError. Use decoder="vacuous" for the undecoded logical error
rate. noise_model can also be a picklable task -> NoiseModel function
defined at module level, so one sampler can serve a sweep whose noise strength
is stored in task.json_metadata.
The noise here is a native physical model chosen independently of the decoder
prior, not an exact execution of the prior’s noise. To run the prior’s own
noise, pass the noisy circuit as the task circuit and omit noise_model; see
How Stim circuits are translated.
How Stim circuits are translated¶
CLEAN_CIRCUIT_API_VERSION = 2 identifies translation with explicit initial
resets, measurement and reset basis handling, classical record ordering,
optional qubit allocation order and exact Stim Pauli noise. Measurement
character k is classical bit k. Correlated (E) and heralded noise,
measurement inversion and record-controlled operations raise ValueError
instead of silently changing their meaning.
Stim’s own noise is carried exactly, instruction by instruction:
DEPOLARIZE1/2, X/Y/Z_ERROR and PAULI_CHANNEL_1/2 become
QuantumCircuit.pauli_channel operations, and measurement flips such as
M(p) become per-measurement readout errors. A noisy Stim circuit (SI1000,
for example) therefore runs with Stim’s noise and needs no NoiseModel.
Statevector and MPS backends draw one Pauli per channel per shot; density
matrix and MPO backends apply the channels exactly.
A NoiseModel, if given, adds its hardware noise on top. It attaches noise
after gates only, and the basis changes that translation inserts for RX,
RY, MX, MY, MRX and MRY are marked noiseless, so the model adds gate
noise exactly where Stim has gates. Its readout error composes with any M(p)
flip.
Counts must contain exactly the requested positive number of shots.
Unexpected record widths and nonzero padding raise ValueError. A caller’s
SimulatorConfig is copied before sampling. Direct compiled samplers accept a
seed (or config.seed); successive calls advance their random stream, and
an explicit per-call seed replays that call without consuming the stream.
MaestroSinterSampler rejects seed and config.seed, because collect
worker identities aren’t available for reproducible stream partitioning.
Histograms don’t preserve the chronological order of shots.