Sampling and Sinter

MaestroBackend takes a Deltakit Circuit or a stim.Circuit and returns raw measurements, detection events, or Sinter statistics. A Deltakit circuit is converted with its as_stim_circuit() (see to_stim()), so everything below works the same on a plain Stim circuit.

from deltakit_maestro import MaestroBackend

backend = MaestroBackend()   # Stabilizer for Pauli runs, MPS (χ=32) for coherent ones

The Simulation types page says which of Maestro’s simulators runs each call, and Noise says what noise it runs with.

Measurements and detection events: simulate

result = backend.simulate(noisy, shots=1000)
result.measurements       # (1000, n_meas) uint8, Stim record order
result.detection_events   # (1000, n_detectors) bool
result.observable_flips   # (1000, n_observables) bool
result.metadata           # the backend, the shot count and the noise parameters used

simulate samples the measurement record and converts it to detection events and observable flips with Stim’s measurement-to-detection converter for the circuit that ran.

A compiled sampler: compile_sampler

compile_sampler returns a maestro.sinter.MaestroCompiledSampler for the circuit, to sample from as often as you like:

sampler = backend.compile_sampler(noisy, seed=1)
sampler.sample_measurements(1000)                    # measurements (bool)
dets, obs = sampler.sample_detection_events(1000)    # detection events, observable flips
stats = sampler.sample(1000)                         # sinter.AnonTaskStats, decoded

decoder= (default 'pymatching') is compiled against the task’s detector error model and used only by .sample(). decoder='vacuous' (or None) gives undecoded statistics. To decode detection events yourself, build your decoder from sampler.task.detector_error_model:

import numpy as np
import pymatching

matcher = pymatching.Matching.from_detector_error_model(sampler.task.detector_error_model)
failures = np.any(matcher.decode_batch(dets) != obs, axis=1)
print(f"logical error probability: {failures.mean():.4f}")

The decoder’s detector error model explains what that model contains.

Sinter: as_sinter_task and sinter_decoders

import sinter

stats = sinter.collect(
    num_workers=4,
    tasks=[backend.as_sinter_task(noisy, json_metadata={'p': 0.01})],
    custom_decoders=backend.sinter_decoders(decoder='pymatching'),
    max_shots=10_000,
)

as_sinter_task returns a sinter.Task holding the circuit Maestro runs, the matching detector error model, and the decoder name 'maestro' (SINTER_DECODER). Its json_metadata is yours plus a 'noise' entry with the noise layers, so each worker can rebuild them. sinter_decoders() maps 'maestro' to a sinter.Sampler that builds each task’s sampler as compile_sampler does, decoding with the decoder= you give it.

Under sinter.collect decoding takes most of the time: see Performance.

Seeds

simulate(..., seed=) and compile_sampler(..., seed=) make a run reproducible. Seeded sinter.collect is not supported by maestro.sinter, and MaestroBackend(seed=) is rejected, since every worker would draw the same shots. For reproducible runs, use simulate or compile_sampler.

Shot order

Shots come back in random order, so any slice of them is a fair sample. Every shot is a fresh noise realisation.